Skip to content
This repository has been archived by the owner on Oct 2, 2019. It is now read-only.

Commit

Permalink
Merge pull request #658 from 18F/98157690-client-side-validation
Browse files Browse the repository at this point in the history
[Delivers #98157690] add client-side form validation
  • Loading branch information
pkarman committed Oct 27, 2015
2 parents 998c641 + d872a35 commit 62d16bd
Show file tree
Hide file tree
Showing 18 changed files with 281 additions and 46 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ gem 'workflow'
group :test, :development do
gem 'bullet', require: false # use BULLET_ENABLED=true
gem 'database_cleaner'
gem 'konacha'
gem 'pry-byebug'
gem 'pry-rails'
gem 'rspec-rails'
Expand All @@ -61,6 +62,7 @@ end

group :development do
gem 'guard-rspec', require: false
gem 'guard-shell', require: false
gem 'mail_view'
gem 'railroady'
gem 'letter_opener'
Expand Down
13 changes: 13 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.9.1.1)
colorize (0.7.7)
columnize (0.9.0)
css_parser (1.3.7)
addressable
Expand Down Expand Up @@ -180,6 +181,9 @@ GEM
guard (~> 2.1)
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
guard-shell (0.7.1)
guard (>= 2.0.0)
guard-compat (~> 1.0)
haml (4.0.7)
tilt
has_scope (0.6.0)
Expand Down Expand Up @@ -213,6 +217,13 @@ GEM
kaminari (0.16.3)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
konacha (3.7.0)
actionpack (>= 3.1, < 5)
capybara
colorize
railties (>= 3.1, < 5)
sprockets (>= 2, < 4)
tilt
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.4.1)
Expand Down Expand Up @@ -466,11 +477,13 @@ DEPENDENCIES
font-awesome-sass
foreman
guard-rspec
guard-shell
haml
hashdiff
html_pipeline_rails
jquery-rails
jquery-turbolinks
konacha
letter_opener
letter_opener_web
mail_view
Expand Down
5 changes: 5 additions & 0 deletions Guardfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ guard :rspec, cmd: 'bin/rspec' do
# Capybara features specs
watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
end

guard :shell do
watch(%r{^app/assets/javascripts/(.+)\.js(\.coffee)?$}) { |m| `bin/rake konacha:run SPEC=#{m[1]}_spec` }
watch(%r{^spec/javascripts/(.+)\.js(\.coffee)?$}) { |m| `bin/rake konacha:run SPEC=#{m[1]}` }
end
4 changes: 3 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ require File.expand_path('../config/application', __FILE__)
C2::Application.load_tasks

desc 'Run the test suite'
task default: "./bin/rspec"
# RSpec task gets included automatically
# http://stackoverflow.com/a/28886514/358804
task default: 'konacha:run'
30 changes: 30 additions & 0 deletions app/assets/javascripts/field_filter.js.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class @FieldFilter
constructor: (@$fieldOrWrappers) ->

isInput: ->
@$fieldOrWrappers.is(':input')

toggleVisibility: (showOrHide) ->
# https://www.paciellogroup.com/blog/2012/05/html5-accessibility-chops-hidden-and-aria-hidden/
@$fieldOrWrappers.attr('aria-hidden', !showOrHide)

toggleEnabled: (enableOrDisable) ->
@$fieldOrWrappers.attr('disabled', !enableOrDisable)

toggleChildInputs: (enableOrDisable) ->
@$fieldOrWrappers.find(':input').attr('disabled', !enableOrDisable)

toggle: (showOrHide) ->
@toggleVisibility(showOrHide)

# hidden inputs need to be disabled, so they aren't submitted with the form
if @isInput()
@toggleEnabled(showOrHide)
else
@toggleChildInputs(showOrHide)

show: ->
@toggle(true)

hide: ->
@toggle(false)
57 changes: 20 additions & 37 deletions app/assets/javascripts/filter.js.coffee
Original file line number Diff line number Diff line change
@@ -1,45 +1,28 @@
class Filter
constructor: ($root, @key) ->
@$ = (selector) -> $root.find(selector)
class @Filter
constructor: (@$root, @$control) ->
key = @$control.data('filter-control')
val = @$control.val()
@set = new FilterSet(@$root, key, val)

addInput: ($el) ->
$el.click () => @filter($el)
# Initial state
if $el.is(":checked")
@filter($el)
isSelected: ->
@$control.is(':checked')

addRadios: () ->
@$("input:radio[data-filter-control=#{ @key }]").each (idx, control) =>
@addInput($(control))
update: ->
if @isSelected()
@set.show()
else
@set.hide()

addChkBoxes: () ->
@$("input:checkbox[data-filter-control=#{ @key }]").each (idx, control) =>
@addInput($(control))

filter: ($el) ->
value = $el.val()
if !$el.is(":checked")
value = "!" + value
@$("[data-filter-key=#{ @key }]").each (idx, el) ->
hidden = el.getAttribute("data-filter-value") != value
el.setAttribute("aria-hidden", hidden.toString())

hideAll: () ->
@$("[data-filter-key=#{ @key }]").attr("aria-hidden", true)
enable: ->
@update()
@$control.change => @update()

@generateIn = ($scope) ->
filters = {}
$scope.find("[data-filter-control]").each (idx, el) ->
key = el.getAttribute('data-filter-control')
if !filters.hasOwnProperty(key)
filters[key] = new Filter($scope, key)
filters
$scope.find('[data-filter-control]').map (idx, control) ->
new Filter($scope, $(control))

$ ->
# @todo - better scope
$scope = $(document)
$scope = $(document.body)
filters = Filter.generateIn($scope)
for key, filter of filters
filter.hideAll()
filter.addRadios()
filter.addChkBoxes()
for filter in filters
filter.enable()
27 changes: 27 additions & 0 deletions app/assets/javascripts/filter_set.js.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class @FilterSet
constructor: (@$root, @key, @val) ->

$: (selector) ->
@$root.find(selector)

children: ->
@$("[data-filter-key=#{ @key }][data-filter-value=#{ @val }]")

cousins: ->
@$("[data-filter-key=#{ @key }][data-filter-value!=#{ @val }]")

showChildren: ->
filter = new FieldFilter(@children())
filter.show()

hideCousins: ->
filter = new FieldFilter(@cousins())
filter.hide()

show: ->
@showChildren()
@hideCousins()

hide: ->
filter = new FieldFilter(@children())
filter.hide()
3 changes: 1 addition & 2 deletions app/assets/javascripts/required_for_submit.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class RequiredForSubmit
@$submit.prop 'disabled', !@$controller.val()

$ ->
# @todo - better scope
$scope = $(document)
$scope = $(document.body)
$scope.find("[data-disable-if-empty]").each (idx, el) ->
new RequiredForSubmit($scope, $(el))
4 changes: 4 additions & 0 deletions app/assets/javascripts/selectizer.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ class Selectizer
@$el = $(el)
@dataAttr = @$el.attr('data-attr') || 'default_field'

# `required` inputs don't work with Selectize
# https://github.com/brianreavis/selectize.js/issues/733
@$el.removeAttr('required')

isFreeForm: ->
@$el.is('input')

Expand Down
2 changes: 1 addition & 1 deletion app/helpers/ncr/work_orders_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def vendor_options(vendor = nil)
def expense_type_radio_button(form, expense_type)
content_tag :div, class: 'radio' do
form.label :expense_type, value: expense_type do
radio = form.radio_button(:expense_type, expense_type, 'data-filter-control' => 'expense-type')
radio = form.radio_button(:expense_type, expense_type, 'data-filter-control' => 'expense-type', required: true)
radio + expense_type
end
end
Expand Down
4 changes: 2 additions & 2 deletions app/views/ncr/work_orders/form.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
= simple_form_for @model_instance, html: { multipart: true } do |f|
= f.input :project_title
= f.input :description
= field_set_tag "Expense type", class: "required" do
= field_set_tag "Expense type", class: 'required' do
= expense_type_radio_button(f, 'BA60')
= expense_type_radio_button(f, 'BA61')
= f.input :emergency, disabled: @model_instance.persisted?, wrapper_html: { data: { filter_key: 'expense-type', filter_value: 'BA61' } }
Expand All @@ -21,7 +21,7 @@
= f.input :amount, as: :currency, label_html: { class: 'sr-only' }, input_html: { data: popover_data_attrs('ncr_amount') }
= f.input :not_to_exceed, as: :radio_buttons, collection: [['Exact', false], ['Not to exceed', true]], label: false
= f.input :direct_pay
= f.input :approving_official_email, collection: approver_options, include_blank: true, disabled: @model_instance.approver_email_frozen?, prompt: :translate, input_html: { class: 'js-selectize' }
= f.input :approving_official_email, collection: approver_options, include_blank: true, disabled: @model_instance.approver_email_frozen?, prompt: :translate, input_html: { class: 'js-selectize' }, required: true
- if @model_instance.new_record?
= render partial: 'attachments'
- else
Expand Down
6 changes: 6 additions & 0 deletions config/initializers/konacha.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
if defined?(Konacha)
Konacha.configure do |config|
require 'capybara/poltergeist'
config.driver = :poltergeist
end
end
3 changes: 1 addition & 2 deletions config/initializers/simple_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,5 @@
# The asterisk for required fields is added by CSS - make it simply be the label text
config.label_text = ->(label, _required, _explicit_label) { label }

# TODO enable, but make sure the filtered fields are only required when visible
config.browser_validations = false
config.browser_validations = true
end
2 changes: 1 addition & 1 deletion doc/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ have it in your PATH. This is used for javascript and interface testing.
### Running the entire suite once
```bash
rake
./bin/rake
```
### Running tests as corresponding files are changed
Expand Down
73 changes: 73 additions & 0 deletions spec/javascripts/field_filter_spec.js.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#= require jquery
#= require field_filter

describe 'FieldFilter', ->
describe '#show()', ->
it "enables inputs", ->
$input = $('<input disabled="disabled">')

filter = new FieldFilter($input)
filter.show()

expect($input.is(':disabled')).to.be.false

it "enables text areas", ->
$textarea = $('<textarea disabled="disabled">')

filter = new FieldFilter($textarea)
filter.show()

expect($textarea.is(':disabled')).to.be.false

it "enables nested inputs", ->
$content = $('<div><input disabled="disabled"></div>')

filter = new FieldFilter($content)
filter.show()

$input = $content.find('input')
expect($input.is(':disabled')).to.be.false

it "enables nested text areas", ->
$content = $('<div><textarea disabled="disabled"></div>')

filter = new FieldFilter($content)
filter.show()

$textarea = $content.find('textarea')
expect($textarea.is(':disabled')).to.be.false

describe '#hide()', ->
it "disables inputs", ->
$input = $('<input>')

filter = new FieldFilter($input)
filter.hide()

expect($input.is(':disabled')).to.be.true

it "disables text areas", ->
$textarea = $('<textarea>')

filter = new FieldFilter($textarea)
filter.hide()

expect($textarea.is(':disabled')).to.be.true

it "disables nested inputs", ->
$content = $('<div><input></div>')

filter = new FieldFilter($content)
filter.hide()

$input = $content.find('input')
expect($input.is(':disabled')).to.be.true

it "disables nested text areas", ->
$content = $('<div><textarea></div>')

filter = new FieldFilter($content)
filter.hide()

$textarea = $content.find('textarea')
expect($textarea.is(':disabled')).to.be.true
Loading

0 comments on commit 62d16bd

Please sign in to comment.