Skip to content
This repository has been archived by the owner on Jan 5, 2021. It is now read-only.

Commit

Permalink
Implement multiple fields using Stimulus and Jest
Browse files Browse the repository at this point in the history
Re-implements the UI for managing multiple fields using the simulus.js
package and Jest for testing. Accomplishes the same feature set with
less code and without any reliance on additional css classes. The only
classes used are from Bootstrap.

JS linting and testing is also done during the Travis build.
  • Loading branch information
awead committed Jun 25, 2019
1 parent 7989cc4 commit f0d6537
Show file tree
Hide file tree
Showing 24 changed files with 2,012 additions and 197 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"env": {
"browser": true,
"es6": true
"es6": true,
"jest": true
},
"extends": "standard",
"globals": {
Expand Down
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ env:
- NOKOGIRI_USE_SYSTEM_LIBRARIES=true
stages:
- niftany
- jest
- test
jobs:
include:
- script: ./travis/test.sh
- stage: niftany
script: bundle exec niftany
- stage: jest
script: yarn lint && yarn jest
14 changes: 7 additions & 7 deletions app/cho/schema/input_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,6 @@ def partial
end
end

def multiple_class
return 'ff-multiple' if multiple?

'ff-single'
end

def values
return empty_values if value_set.empty?

Expand All @@ -44,7 +38,13 @@ def multiple?
end

def options
{ required: required?, 'aria-required': required?, class: 'form-control ff-control' }
{ required: required?, 'aria-required': required?, class: 'form-control' }
end

def data_attributes
return {} unless multiple?

{ controller: 'fields' }
end

def datalist(component: nil)
Expand Down
73 changes: 73 additions & 0 deletions app/javascript/controllers/fields_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Implements multiple fields in our forms by adding DOM elements to add and remove cloned fields
// in the form. Relies heavily on Bootstrap's form elements. Including the proper data attributes
// will trigger the insertion of the buttons to manage the additional fields. Certain html elements
// are required, such as a label and the input-group css class.
// @example
// <div class="form-group" data-controller="fields">
// <!-- A label is required in order for the buttons to display correctly -->
// <label>
// Identifier
// </label>
// <!-- The input-goup and form=control classes are required in order for new fields to be added -->
// <div class="input-group">
// <input class="form-control" />
// </div>

import { Controller } from 'stimulus'

export default class extends Controller {
connect () {
let label = this.element.getElementsByTagName('label').item(0).innerText
for (let count = 1; count < this.inputGroups.length; count++) {
this.inputGroups.item(count).appendChild(this.removeButton)
}
this.element.appendChild(this.addButton(label))
}

// By default, Stimulus listens to click events on buttons and will execute this method.
add (event) {
event.preventDefault()
this.element.insertBefore(this.newField, this.element.lastElementChild)
}

// By default, Stimulus listens to click events on buttons and will execute this method.
remove (event) {
event.preventDefault()
event.target.parentElement.remove()
}

// @return [HTMLElement] cloned field taken from the first input group.
get newField () {
let clone = this.inputGroups.item(0).cloneNode(true)
Array.from(clone.getElementsByClassName('form-control')).forEach((input) => {
input.value = ''
})
clone.appendChild(this.removeButton)
return clone
}

// @return [HTMLElement]
addButton (label) {
let node = document.createElement('button')
node.setAttribute('data-action', 'fields#add')
node.classList.add('btn', 'btn-outline-success', 'btn-sm')
let content = document.createTextNode('Add another ' + label)
node.appendChild(content)
return node
}

// @return [HTMLElement]
get removeButton () {
let node = document.createElement('button')
node.setAttribute('data-action', 'fields#remove')
node.classList.add('btn', 'btn-outline-danger', 'btn-sm')
let content = document.createTextNode('Remove')
node.appendChild(content)
return node
}

// @return [HTMLCollection]
get inputGroups () {
return this.element.getElementsByClassName('input-group')
}
}
9 changes: 9 additions & 0 deletions app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Load all the controllers within this directory and all subdirectories.
// Controller files must be named *_controller.js.

import { Application } from 'stimulus'
import { definitionsFromContext } from 'stimulus/webpack-helpers'

const application = Application.start()
const context = require.context('controllers', true, /_controller\.js$/)
application.load(definitionsFromContext(context))
37 changes: 0 additions & 37 deletions app/javascript/form_fields/field.js

This file was deleted.

43 changes: 0 additions & 43 deletions app/javascript/form_fields/field_set.js

This file was deleted.

12 changes: 0 additions & 12 deletions app/javascript/form_fields/styles.css

This file was deleted.

1 change: 1 addition & 0 deletions app/javascript/packs/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import 'controllers'
9 changes: 0 additions & 9 deletions app/javascript/packs/form_fields.js

This file was deleted.

4 changes: 1 addition & 3 deletions app/views/shared/_form_fields.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<% end %>
<%- resource.input_fields(form).each do |input_field| %>
<%= content_tag :div, class: "form-group #{input_field.multiple_class} #{input_field.label}" do -%>
<%= content_tag :div, class: 'form-group', data: input_field.data_attributes do -%>
<%= form.label input_field.label do %>
<%= input_field.display_label %>
<% if input_field.required? %>
Expand All @@ -14,5 +14,3 @@
<%= render "/shared/input_partials/#{input_field.partial}", field: input_field, form: form %>
<%- end %>
<%- end %>
<%= javascript_pack_tag 'form_fields' %>
1 change: 1 addition & 0 deletions app/views/shared/_head_content.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
<%= javascript_pack_tag 'application' %>
<%= javascript_include_tag 'matomo' if Rails.env.production? %>
<%= csrf_meta_tags %>
<%= content_for(:head) %>
Expand Down
43 changes: 20 additions & 23 deletions app/views/shared/input_partials/_creator.html.erb
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
<%- field.values.each do |value| %>

<div class="container ff-group">
<div class="row">
<div class="col-md-12 col-lg-3">
<%= label_tag :role %>
<%= text_field_tag "#{form.object_name}[creator][][role]", value.fetch(:role, nil),
field.options.merge!(list: "#{field.label}_role") %>
<datalist id="<%= "#{field.label}_role" %>">
<% field.datalist(component: :roles).each do |item| %>
<option value="<%= item %>"><%= item.label %></option>
<% end %>
</datalist>
</div>

<div class="col-md-12 col-lg-3">
<%= label_tag :agent %>
<%= text_field_tag "#{form.object_name}[creator][][agent]", value.fetch(:agent, nil),
field.options.merge!(list: "#{field.label}_agent") %>
<datalist id="<%= "#{field.label}_agent" %>">
<% field.datalist(component: :agents).each do |item| %>
<option value="<%= item.id %>"><%= item %></option>
<% end %>
</datalist>
</div>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><%= t('cho.field_label.name_and_role') %></span>
</div>

<%= text_field_tag "#{form.object_name}[creator][][agent]", value.fetch(:agent, nil),
field.options.merge!(list: "#{field.label}_agent") %>
<datalist id="<%= "#{field.label}_agent" %>">
<% field.datalist(component: :agents).each do |item| %>
<option value="<%= item.id %>"><%= item %></option>
<% end %>
</datalist>

<%= text_field_tag "#{form.object_name}[creator][][role]", value.fetch(:role, nil),
field.options.merge!(list: "#{field.label}_role") %>
<datalist id="<%= "#{field.label}_role" %>">
<% field.datalist(component: :roles).each do |item| %>
<option value="<%= item %>"><%= item.label %></option>
<% end %>
</datalist>

</div>

<% end %>
2 changes: 1 addition & 1 deletion app/views/shared/input_partials/_string.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<% field.values.each do |value| %>
<%= content_tag :div, class: 'input-group ff-group' do %>
<%= content_tag :div, class: 'input-group' do %>
<%= form.text_field field.label, field.options.merge(multiple: field.multiple?, value: value) %>
<% end %>
<% end %>
2 changes: 1 addition & 1 deletion app/views/shared/input_partials/_text.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<% field.values.each do |value| %>
<%= content_tag :div, class: 'input-group ff-group' do %>
<%= content_tag :div, class: 'input-group' do %>
<%= form.text_area field.label, field.options.merge(multiple: field.multiple?, value: value) %>
<% end %>
<% end %>
1 change: 1 addition & 0 deletions config/locales/cho.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ en:
required: "required"
member_of_collections: "Collections"
role: "Role"
name_and_role: "Name and Role"
header_links:
alt_banner_image: "Penn State University Libraries"
collection:
Expand Down
28 changes: 26 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,43 @@
"name": "cho",
"private": true,
"dependencies": {
"@rails/webpacker": "^4.0.2"
"@rails/webpacker": "^4.0.2",
"stimulus": "^1.1.1"
},
"devDependencies": {
"babel-jest": "^24.8.0",
"babel-preset-es2015": "^6.24.1",
"eslint": "^5.16.0",
"eslint-config-standard": "^12.0.0",
"eslint-loader": "^2.1.2",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.1.1",
"eslint-plugin-standard": "^4.0.0",
"jest": "^24.8.0",
"mutationobserver-shim": "^0.3.3",
"webpack-dev-server": "^3.2.1"
},
"scripts": {
"lint": "yarn run eslint --ext .js app/javascript"
"lint": "yarn run eslint --ext .js app/javascript spec/javascript",
"test": "jest",
"debug": "echo \"Goto chrome://inspect and click 'Open dedicated DevTools for Node'\" && node --inspect-brk node_modules/.bin/jest --runInBand"
},
"jest": {
"setupFiles": [
"./spec/javascript/setup.js"
],
"testRegex": ".*_spec.js",
"roots": [
"spec/javascript"
],
"moduleDirectories": [
"node_modules",
"app/javascript/controllers"
],
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/spec/javascript/mocks/fileMock.js",
"\\.(css|less)$": "<rootDir>/spec/javascript/mocks/styleMock.js"
}
}
}

0 comments on commit f0d6537

Please sign in to comment.