From f59d88a812a8255f417bcd620862166c6941d291 Mon Sep 17 00:00:00 2001 From: Michal Remis Date: Tue, 14 Apr 2020 16:35:34 +0200 Subject: [PATCH 1/6] Implemented field's specific wrapper_names to be passed to and used by CSV Javascript form builder for adding/removing of error messages. Before CSV used only form-wide wrapper but field may use custom wrapper specified: - by field's `wrapper` attribute - by field's type and `wrapper_mappings` attribute - by field's type and SimpleForm config `wrapper_mappings` --- dist/simple-form.bootstrap4.esm.js | 7 +++-- dist/simple-form.bootstrap4.js | 7 +++-- dist/simple-form.esm.js | 7 +++-- dist/simple-form.js | 7 +++-- .../simple_form/form_builder.rb | 10 +++++++ src/main.bootstrap4.js | 9 ++++-- src/main.js | 9 ++++-- .../test/form_builders/validateSimpleForm.js | 28 ++++++++++++++++++- test/simple_form/cases/test_form_helpers.rb | 24 ++++++++++++++++ test/test_loader.rb | 5 +++- ...ails.validations.simple_form.bootstrap4.js | 7 +++-- .../rails.validations.simple_form.js | 7 +++-- 12 files changed, 107 insertions(+), 20 deletions(-) diff --git a/dist/simple-form.bootstrap4.esm.js b/dist/simple-form.bootstrap4.esm.js index 5cf2108..2f2ca70 100644 --- a/dist/simple-form.bootstrap4.esm.js +++ b/dist/simple-form.bootstrap4.esm.js @@ -9,14 +9,17 @@ import ClientSideValidations from '@client-side-validations/client-side-validati ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function add(element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, element, settings, message); + this.wrapper(this.wrapperName(element, settings)).add.call(this, element, settings, message); }, remove: function remove(element, settings) { - this.wrapper(settings.wrapper).remove.call(this, element, settings); + this.wrapper(this.wrapperName(element, settings)).remove.call(this, element, settings); }, wrapper: function wrapper(name) { return this.wrappers[name] || this.wrappers["default"]; }, + wrapperName: function wrapperName(element, settings) { + return element.data('clientSideValidationsWrapper') || settings.wrapper; + }, wrappers: { "default": { add: function add(element, settings, message) { diff --git a/dist/simple-form.bootstrap4.js b/dist/simple-form.bootstrap4.js index e5bd428..d934fa1 100644 --- a/dist/simple-form.bootstrap4.js +++ b/dist/simple-form.bootstrap4.js @@ -15,14 +15,17 @@ ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function add(element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, element, settings, message); + this.wrapper(this.wrapperName(element, settings)).add.call(this, element, settings, message); }, remove: function remove(element, settings) { - this.wrapper(settings.wrapper).remove.call(this, element, settings); + this.wrapper(this.wrapperName(element, settings)).remove.call(this, element, settings); }, wrapper: function wrapper(name) { return this.wrappers[name] || this.wrappers["default"]; }, + wrapperName: function wrapperName(element, settings) { + return element.data('clientSideValidationsWrapper') || settings.wrapper; + }, wrappers: { "default": { add: function add(element, settings, message) { diff --git a/dist/simple-form.esm.js b/dist/simple-form.esm.js index 22b06e5..74ca450 100644 --- a/dist/simple-form.esm.js +++ b/dist/simple-form.esm.js @@ -9,14 +9,17 @@ import ClientSideValidations from '@client-side-validations/client-side-validati ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function add(element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, element, settings, message); + this.wrapper(this.wrapperName(element, settings)).add.call(this, element, settings, message); }, remove: function remove(element, settings) { - this.wrapper(settings.wrapper).remove.call(this, element, settings); + this.wrapper(this.wrapperName(element, settings)).remove.call(this, element, settings); }, wrapper: function wrapper(name) { return this.wrappers[name] || this.wrappers["default"]; }, + wrapperName: function wrapperName(element, settings) { + return element.data('clientSideValidationsWrapper') || settings.wrapper; + }, wrappers: { "default": { add: function add(element, settings, message) { diff --git a/dist/simple-form.js b/dist/simple-form.js index 0a24645..62decb9 100644 --- a/dist/simple-form.js +++ b/dist/simple-form.js @@ -15,14 +15,17 @@ ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function add(element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, element, settings, message); + this.wrapper(this.wrapperName(element, settings)).add.call(this, element, settings, message); }, remove: function remove(element, settings) { - this.wrapper(settings.wrapper).remove.call(this, element, settings); + this.wrapper(this.wrapperName(element, settings)).remove.call(this, element, settings); }, wrapper: function wrapper(name) { return this.wrappers[name] || this.wrappers["default"]; }, + wrapperName: function wrapperName(element, settings) { + return element.data('clientSideValidationsWrapper') || settings.wrapper; + }, wrappers: { "default": { add: function add(element, settings, message) { diff --git a/lib/client_side_validations/simple_form/form_builder.rb b/lib/client_side_validations/simple_form/form_builder.rb index 807622a..e23bce3 100644 --- a/lib/client_side_validations/simple_form/form_builder.rb +++ b/lib/client_side_validations/simple_form/form_builder.rb @@ -22,6 +22,8 @@ def input(attribute_name, options = {}, &block) options.delete(:validate) end + add_field_specific_wrapper_name_to_field_options(attribute_name, options, &block) + super(attribute_name, options, &block) end @@ -34,6 +36,14 @@ def wrapper_error_component wrapper.find(:full_error) end end + + def add_field_specific_wrapper_name_to_field_options(attribute_name, options, &block) + wrapper_name = options[:wrapper] || find_wrapper_mapping(find_input(attribute_name, options, &block).input_type) + return if wrapper_name.nil? + + options[:input_html] ||= {} + options[:input_html][:'data-client-side-validations-wrapper'] = wrapper_name + end end end end diff --git a/src/main.bootstrap4.js b/src/main.bootstrap4.js index dbb7e3f..6044c0e 100644 --- a/src/main.bootstrap4.js +++ b/src/main.bootstrap4.js @@ -3,20 +3,23 @@ import ClientSideValidations from '@client-side-validations/client-side-validati ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function (element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, element, settings, message) + this.wrapper(this.wrapperName(element, settings)).add.call(this, element, settings, message) }, remove: function (element, settings) { - this.wrapper(settings.wrapper).remove.call(this, element, settings) + this.wrapper(this.wrapperName(element, settings)).remove.call(this, element, settings) }, wrapper: function (name) { return this.wrappers[name] || this.wrappers.default }, + wrapperName: function (element, settings) { + return element.data('clientSideValidationsWrapper') || settings.wrapper + }, wrappers: { default: { add (element, settings, message) { const wrapperElement = element.parent() - let errorElement = wrapperElement.find(settings.error_tag + '.invalid-feedback') + var errorElement = wrapperElement.find(settings.error_tag + '.invalid-feedback') if (!errorElement.length) { errorElement = $('<' + settings.error_tag + '>', { class: 'invalid-feedback', text: message }) diff --git a/src/main.js b/src/main.js index 0b27184..b3e9bf7 100644 --- a/src/main.js +++ b/src/main.js @@ -3,20 +3,23 @@ import ClientSideValidations from '@client-side-validations/client-side-validati ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function (element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, element, settings, message) + this.wrapper(this.wrapperName(element, settings)).add.call(this, element, settings, message) }, remove: function (element, settings) { - this.wrapper(settings.wrapper).remove.call(this, element, settings) + this.wrapper(this.wrapperName(element, settings)).remove.call(this, element, settings) }, wrapper: function (name) { return this.wrappers[name] || this.wrappers.default }, + wrapperName: function (element, settings) { + return element.data('clientSideValidationsWrapper') || settings.wrapper + }, wrappers: { default: { add (element, settings, message) { const wrapper = element.closest(settings.wrapper_tag + '.' + settings.wrapper_class.replace(/ /g, '.')) - let errorElement = wrapper.find(settings.error_tag + '.' + settings.error_class.replace(/ /g, '.')) + var errorElement = wrapper.find(settings.error_tag + '.' + settings.error_class.replace(/ /g, '.')) if (!errorElement.length) { errorElement = $('<' + settings.error_tag + '>', { class: settings.error_class, text: message }) diff --git a/test/javascript/public/test/form_builders/validateSimpleForm.js b/test/javascript/public/test/form_builders/validateSimpleForm.js index a7d1c1b..3643175 100644 --- a/test/javascript/public/test/form_builders/validateSimpleForm.js +++ b/test/javascript/public/test/form_builders/validateSimpleForm.js @@ -20,7 +20,8 @@ QUnit.module('Validate SimpleForm', { wrapper: 'default' }, validators: { - 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] } + 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] }, + 'user[date_of_birth]': { presence: [{ message: 'must be present' }] } } } @@ -40,6 +41,12 @@ QUnit.module('Validate SimpleForm', { type: 'text' })) .append($('')) + .append($('', { + name: 'user[date_of_birth]', + id: 'date_of_birth', + type: 'text', + 'data-client-side-validations-wrapper': 'custom_date_wrapper' + })) $('form#new_user').validate() } }) @@ -82,3 +89,22 @@ QUnit.test('Validate pre-existing error blocks are re-used', function (assert) { assert.ok(input.parent().find('span.error:contains("is invalid")').length === 1) assert.ok(form.find('span.error').length === 1) }) + +QUnit.test('Validate correct JS Builder\'s wrapper is called for custom_wrapper', function (assert) { + const oldWrappers = $.extend({}, ClientSideValidations.formBuilders['SimpleForm::FormBuilder'].wrappers) + + // It would be probably better to use some stub library but I want to keep it simple + let customWrapperCalled = false; + + ClientSideValidations.formBuilders['SimpleForm::FormBuilder'].wrappers['custom_date_wrapper'] = { + add: function(element, settings, message) { customWrapperCalled=true; }, + remove: function(element, settings) {} + } + + var form = $('form#new_user'); + var input = form.find('input#date_of_birth') + input.trigger('focusout') + + assert.ok(customWrapperCalled); + ClientSideValidations.formBuilders['SimpleForm::FormBuilder'].wrappers = oldWrappers +}) diff --git a/test/simple_form/cases/test_form_helpers.rb b/test/simple_form/cases/test_form_helpers.rb index b543c77..5aea45d 100644 --- a/test/simple_form/cases/test_form_helpers.rb +++ b/test/simple_form/cases/test_form_helpers.rb @@ -102,6 +102,30 @@ def test_input_override assert_dom_equal expected, output_buffer end + + def test_input_override_with_custom_wrapper_name + simple_form_for(@post, validate: true, wrapper: :default) do |f| + concat f.input(:cost, validate: false, wrapper: :custom_date_wrapper) + end + + csv_data = { + html_settings: { + type: 'SimpleForm::FormBuilder', + error_class: 'error', + error_tag: 'span', + wrapper_error_class: 'field_with_errors', + wrapper_tag: 'div', + wrapper_class: 'input', + wrapper: 'default' + }, + number_format: { separator: '.', delimiter: ',' }, + validators: {} + } + + expected = %(
) + + assert_dom_equal expected, output_buffer + end end end end diff --git a/test/test_loader.rb b/test/test_loader.rb index fb6ba0e..f46501a 100644 --- a/test/test_loader.rb +++ b/test/test_loader.rb @@ -5,7 +5,10 @@ require 'base_helper' require 'client_side_validations/simple_form' -SimpleForm.setup do +SimpleForm.setup do |config| + config.wrappers :custom_date_wrapper, tag: 'div' do |b| + b.use :input, class: 'form-control' + end end TestApp::Application.initialize! diff --git a/vendor/assets/javascripts/rails.validations.simple_form.bootstrap4.js b/vendor/assets/javascripts/rails.validations.simple_form.bootstrap4.js index e5bd428..d934fa1 100644 --- a/vendor/assets/javascripts/rails.validations.simple_form.bootstrap4.js +++ b/vendor/assets/javascripts/rails.validations.simple_form.bootstrap4.js @@ -15,14 +15,17 @@ ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function add(element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, element, settings, message); + this.wrapper(this.wrapperName(element, settings)).add.call(this, element, settings, message); }, remove: function remove(element, settings) { - this.wrapper(settings.wrapper).remove.call(this, element, settings); + this.wrapper(this.wrapperName(element, settings)).remove.call(this, element, settings); }, wrapper: function wrapper(name) { return this.wrappers[name] || this.wrappers["default"]; }, + wrapperName: function wrapperName(element, settings) { + return element.data('clientSideValidationsWrapper') || settings.wrapper; + }, wrappers: { "default": { add: function add(element, settings, message) { diff --git a/vendor/assets/javascripts/rails.validations.simple_form.js b/vendor/assets/javascripts/rails.validations.simple_form.js index 0a24645..62decb9 100644 --- a/vendor/assets/javascripts/rails.validations.simple_form.js +++ b/vendor/assets/javascripts/rails.validations.simple_form.js @@ -15,14 +15,17 @@ ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function add(element, settings, message) { - this.wrapper(settings.wrapper).add.call(this, element, settings, message); + this.wrapper(this.wrapperName(element, settings)).add.call(this, element, settings, message); }, remove: function remove(element, settings) { - this.wrapper(settings.wrapper).remove.call(this, element, settings); + this.wrapper(this.wrapperName(element, settings)).remove.call(this, element, settings); }, wrapper: function wrapper(name) { return this.wrappers[name] || this.wrappers["default"]; }, + wrapperName: function wrapperName(element, settings) { + return element.data('clientSideValidationsWrapper') || settings.wrapper; + }, wrappers: { "default": { add: function add(element, settings, message) { From f953f792f3946c80352e63e20b7e5637bb0a70a1 Mon Sep 17 00:00:00 2001 From: Michal Remis Date: Tue, 28 Apr 2020 21:30:16 +0200 Subject: [PATCH 2/6] Checkboxes association presence validation --- Gemfile | 3 + client_side_validations-simple_form.gemspec | 1 + dist/simple-form.bootstrap4.esm.js | 49 +++++ dist/simple-form.bootstrap4.js | 49 +++++ dist/simple-form.esm.js | 26 ++- dist/simple-form.js | 26 ++- .../simple_form/form_builder.rb | 10 + src/main.bootstrap4.js | 28 +++ src/main.js | 8 +- src/radio_checkbox_validators.js | 21 ++ test/action_view/test_helper.rb | 1 + .../test/form_builders/validateSimpleForm.js | 106 ++++++++-- .../validateSimpleFormBootstrap4.js | 175 +++++++++++++++- test/simple_form/cases/helper.rb | 1 + test/simple_form/cases/test_form_helpers.rb | 186 ++++++++++++++---- test/simple_form/models.rb | 6 + test/simple_form/models/application_record.rb | 13 ++ test/simple_form/models/department.rb | 8 + test/simple_form/models/role.rb | 8 + test/simple_form/models/user.rb | 11 ++ ...ails.validations.simple_form.bootstrap4.js | 49 +++++ .../rails.validations.simple_form.js | 26 ++- 22 files changed, 742 insertions(+), 69 deletions(-) create mode 100644 src/radio_checkbox_validators.js create mode 100644 test/simple_form/models.rb create mode 100644 test/simple_form/models/application_record.rb create mode 100644 test/simple_form/models/department.rb create mode 100644 test/simple_form/models/role.rb create mode 100644 test/simple_form/models/user.rb diff --git a/Gemfile b/Gemfile index 7f4f5e9..6773bbb 100644 --- a/Gemfile +++ b/Gemfile @@ -3,3 +3,6 @@ source 'https://rubygems.org' gemspec + +#gem 'client_side_validations', github: 'DavyJonesLocker/client_side_validations' +gem 'client_side_validations', path: '../client_side_validations' diff --git a/client_side_validations-simple_form.gemspec b/client_side_validations-simple_form.gemspec index 20edf37..221168e 100644 --- a/client_side_validations-simple_form.gemspec +++ b/client_side_validations-simple_form.gemspec @@ -39,6 +39,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rubocop-performance', '~> 1.5' spec.add_development_dependency 'rubocop-rails', '~> 2.5' spec.add_development_dependency 'simplecov', '~> 0.18.5' + spec.add_development_dependency 'sqlite3', '~> 1.4' # For QUnit testing spec.add_development_dependency 'shotgun', '~> 0.9.2' diff --git a/dist/simple-form.bootstrap4.esm.js b/dist/simple-form.bootstrap4.esm.js index 2f2ca70..f661b2f 100644 --- a/dist/simple-form.bootstrap4.esm.js +++ b/dist/simple-form.bootstrap4.esm.js @@ -7,6 +7,25 @@ import $ from 'jquery'; import ClientSideValidations from '@client-side-validations/client-side-validations'; +var originalPresenceValidator = ClientSideValidations.validators.local.presence; + +function checkedCheckboxesCount(element, formSettings) { + var wrapperClass = formSettings.html_settings.wrapper_class; + return element.closest(".".concat(wrapperClass.replace(/ /g, '.'))).find('input[type="checkbox"]:checked').length; +} + +ClientSideValidations.validators.local.presence = function (element, options) { + if (element.attr('type') === 'checkbox') { + var formSettings = element.closest('form[data-client-side-validations]').data('clientSideValidations'); + + if (checkedCheckboxesCount(element, formSettings) === 0) { + return options.message; + } + } else { + return originalPresenceValidator(element, options); + } +}; + ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function add(element, settings, message) { this.wrapper(this.wrapperName(element, settings)).add.call(this, element, settings, message); @@ -45,6 +64,36 @@ ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { element.removeClass('is-invalid'); errorElement.remove(); } + }, + + get horizontal_collection() { + return this.vertical_collection; + }, + + vertical_collection: { + add: function add(element, settings, message) { + var wrapperElement = element.closest('.' + settings.wrapper_class.replace(/ /g, '.')); + var errorElement = wrapperElement.find(settings.error_tag + '.invalid-feedback'); + + if (!errorElement.length) { + errorElement = $('<' + settings.error_tag + '>', { + "class": 'invalid-feedback d-block', + text: message + }); + element.closest('.form-check').siblings('.form-check:last').after(errorElement); + } + + wrapperElement.addClass(settings.wrapper_error_class); + wrapperElement.find('input:visible').addClass('is-invalid'); + errorElement.text(message); + }, + remove: function remove(element, settings) { + var wrapperElement = element.closest('.' + settings.wrapper_class.replace(/ /g, '.')); + var errorElement = wrapperElement.find(settings.error_tag + '.invalid-feedback'); + wrapperElement.removeClass(settings.wrapper_error_class); + errorElement.remove(); + wrapperElement.find('input:visible').removeClass('is-invalid'); + } } } }; diff --git a/dist/simple-form.bootstrap4.js b/dist/simple-form.bootstrap4.js index d934fa1..df732ea 100644 --- a/dist/simple-form.bootstrap4.js +++ b/dist/simple-form.bootstrap4.js @@ -13,6 +13,25 @@ $ = $ && Object.prototype.hasOwnProperty.call($, 'default') ? $['default'] : $; ClientSideValidations = ClientSideValidations && Object.prototype.hasOwnProperty.call(ClientSideValidations, 'default') ? ClientSideValidations['default'] : ClientSideValidations; + var originalPresenceValidator = ClientSideValidations.validators.local.presence; + + function checkedCheckboxesCount(element, formSettings) { + var wrapperClass = formSettings.html_settings.wrapper_class; + return element.closest(".".concat(wrapperClass.replace(/ /g, '.'))).find('input[type="checkbox"]:checked').length; + } + + ClientSideValidations.validators.local.presence = function (element, options) { + if (element.attr('type') === 'checkbox') { + var formSettings = element.closest('form[data-client-side-validations]').data('clientSideValidations'); + + if (checkedCheckboxesCount(element, formSettings) === 0) { + return options.message; + } + } else { + return originalPresenceValidator(element, options); + } + }; + ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function add(element, settings, message) { this.wrapper(this.wrapperName(element, settings)).add.call(this, element, settings, message); @@ -51,6 +70,36 @@ element.removeClass('is-invalid'); errorElement.remove(); } + }, + + get horizontal_collection() { + return this.vertical_collection; + }, + + vertical_collection: { + add: function add(element, settings, message) { + var wrapperElement = element.closest('.' + settings.wrapper_class.replace(/ /g, '.')); + var errorElement = wrapperElement.find(settings.error_tag + '.invalid-feedback'); + + if (!errorElement.length) { + errorElement = $('<' + settings.error_tag + '>', { + "class": 'invalid-feedback d-block', + text: message + }); + element.closest('.form-check').siblings('.form-check:last').after(errorElement); + } + + wrapperElement.addClass(settings.wrapper_error_class); + wrapperElement.find('input:visible').addClass('is-invalid'); + errorElement.text(message); + }, + remove: function remove(element, settings) { + var wrapperElement = element.closest('.' + settings.wrapper_class.replace(/ /g, '.')); + var errorElement = wrapperElement.find(settings.error_tag + '.invalid-feedback'); + wrapperElement.removeClass(settings.wrapper_error_class); + errorElement.remove(); + wrapperElement.find('input:visible').removeClass('is-invalid'); + } } } }; diff --git a/dist/simple-form.esm.js b/dist/simple-form.esm.js index 74ca450..d87cb78 100644 --- a/dist/simple-form.esm.js +++ b/dist/simple-form.esm.js @@ -7,6 +7,25 @@ import $ from 'jquery'; import ClientSideValidations from '@client-side-validations/client-side-validations'; +var originalPresenceValidator = ClientSideValidations.validators.local.presence; + +function checkedCheckboxesCount(element, formSettings) { + var wrapperClass = formSettings.html_settings.wrapper_class; + return element.closest(".".concat(wrapperClass.replace(/ /g, '.'))).find('input[type="checkbox"]:checked').length; +} + +ClientSideValidations.validators.local.presence = function (element, options) { + if (element.attr('type') === 'checkbox') { + var formSettings = element.closest('form[data-client-side-validations]').data('clientSideValidations'); + + if (checkedCheckboxesCount(element, formSettings) === 0) { + return options.message; + } + } else { + return originalPresenceValidator(element, options); + } +}; + ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function add(element, settings, message) { this.wrapper(this.wrapperName(element, settings)).add.call(this, element, settings, message); @@ -31,7 +50,12 @@ ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { "class": settings.error_class, text: message }); - wrapper.append(errorElement); + + if (wrapper.hasClass('check_boxes')) { + element.closest('.checkbox').siblings('.checkbox:last').after(errorElement); + } else { + wrapper.append(errorElement); + } } wrapper.addClass(settings.wrapper_error_class); diff --git a/dist/simple-form.js b/dist/simple-form.js index 62decb9..9ae525b 100644 --- a/dist/simple-form.js +++ b/dist/simple-form.js @@ -13,6 +13,25 @@ $ = $ && Object.prototype.hasOwnProperty.call($, 'default') ? $['default'] : $; ClientSideValidations = ClientSideValidations && Object.prototype.hasOwnProperty.call(ClientSideValidations, 'default') ? ClientSideValidations['default'] : ClientSideValidations; + var originalPresenceValidator = ClientSideValidations.validators.local.presence; + + function checkedCheckboxesCount(element, formSettings) { + var wrapperClass = formSettings.html_settings.wrapper_class; + return element.closest(".".concat(wrapperClass.replace(/ /g, '.'))).find('input[type="checkbox"]:checked').length; + } + + ClientSideValidations.validators.local.presence = function (element, options) { + if (element.attr('type') === 'checkbox') { + var formSettings = element.closest('form[data-client-side-validations]').data('clientSideValidations'); + + if (checkedCheckboxesCount(element, formSettings) === 0) { + return options.message; + } + } else { + return originalPresenceValidator(element, options); + } + }; + ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function add(element, settings, message) { this.wrapper(this.wrapperName(element, settings)).add.call(this, element, settings, message); @@ -37,7 +56,12 @@ "class": settings.error_class, text: message }); - wrapper.append(errorElement); + + if (wrapper.hasClass('check_boxes')) { + element.closest('.checkbox').siblings('.checkbox:last').after(errorElement); + } else { + wrapper.append(errorElement); + } } wrapper.addClass(settings.wrapper_error_class); diff --git a/lib/client_side_validations/simple_form/form_builder.rb b/lib/client_side_validations/simple_form/form_builder.rb index e23bce3..3a9b5db 100644 --- a/lib/client_side_validations/simple_form/form_builder.rb +++ b/lib/client_side_validations/simple_form/form_builder.rb @@ -27,6 +27,16 @@ def input(attribute_name, options = {}, &block) super(attribute_name, options, &block) end + # these methods don't call `super` in SimpleForm and therefore don't use overriden CSV FromBuilder methods + # and therefore aren't included in CSV validations hash.. we add them to the hash here + %i[collection_check_boxes collection_radio_buttons].each do |method_name| + define_method method_name do |method, collection, value_method, text_method, options = {}, html_options = {}, &block| # rubocop:disable Metrics/ParameterLists + build_validation_options method, html_options.merge(name: options[:name]) + add_field_specific_wrapper_name_to_field_options(method_name, options, &block) + super(method, collection, value_method, text_method, options, html_options, &block) + end + end + private def wrapper_error_component diff --git a/src/main.bootstrap4.js b/src/main.bootstrap4.js index 6044c0e..05fce0f 100644 --- a/src/main.bootstrap4.js +++ b/src/main.bootstrap4.js @@ -1,5 +1,6 @@ import $ from 'jquery' import ClientSideValidations from '@client-side-validations/client-side-validations' +import './radio_checkbox_validators' ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function (element, settings, message) { @@ -39,6 +40,33 @@ ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { element.removeClass('is-invalid') errorElement.remove() } + }, + get horizontal_collection () { + return this.vertical_collection + }, + vertical_collection: { + add (element, settings, message) { + const wrapperElement = element.closest('.' + settings.wrapper_class.replace(/ /g, '.')) + var errorElement = wrapperElement.find(settings.error_tag + '.invalid-feedback') + + if (!errorElement.length) { + errorElement = $('<' + settings.error_tag + '>', { class: 'invalid-feedback d-block', text: message }) + element.closest('.form-check').siblings('.form-check:last').after(errorElement) + } + + wrapperElement.addClass(settings.wrapper_error_class) + wrapperElement.find('input:visible').addClass('is-invalid') + errorElement.text(message) + }, + remove (element, settings) { + const wrapperElement = element.closest('.' + settings.wrapper_class.replace(/ /g, '.')) + const errorElement = wrapperElement.find(settings.error_tag + '.invalid-feedback') + + wrapperElement.removeClass(settings.wrapper_error_class) + errorElement.remove() + wrapperElement.find('input:visible').removeClass('is-invalid') + } + } } } diff --git a/src/main.js b/src/main.js index b3e9bf7..2496cb7 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,6 @@ import $ from 'jquery' import ClientSideValidations from '@client-side-validations/client-side-validations' +import './radio_checkbox_validators' ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { add: function (element, settings, message) { @@ -23,7 +24,12 @@ ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { if (!errorElement.length) { errorElement = $('<' + settings.error_tag + '>', { class: settings.error_class, text: message }) - wrapper.append(errorElement) + + if (wrapper.hasClass('check_boxes')) { + element.closest('.checkbox').siblings('.checkbox:last').after(errorElement) + } else { + wrapper.append(errorElement) + } } wrapper.addClass(settings.wrapper_error_class) diff --git a/src/radio_checkbox_validators.js b/src/radio_checkbox_validators.js new file mode 100644 index 0000000..208505f --- /dev/null +++ b/src/radio_checkbox_validators.js @@ -0,0 +1,21 @@ +import ClientSideValidations from '@client-side-validations/client-side-validations' + +const originalPresenceValidator = ClientSideValidations.validators.local.presence + +function checkedCheckboxesCount (element, formSettings) { + const wrapperClass = formSettings.html_settings.wrapper_class + + return element.closest(`.${wrapperClass.replace(/ /g, '.')}`).find('input[type="checkbox"]:checked').length +} + +ClientSideValidations.validators.local.presence = function (element, options) { + if (element.attr('type') === 'checkbox') { + const formSettings = element.closest('form[data-client-side-validations]').data('clientSideValidations') + + if (checkedCheckboxesCount(element, formSettings) === 0) { + return options.message + } + } else { + return originalPresenceValidator(element, options) + } +} diff --git a/test/action_view/test_helper.rb b/test/action_view/test_helper.rb index 8d5457d..1048a0f 100644 --- a/test/action_view/test_helper.rb +++ b/test/action_view/test_helper.rb @@ -26,6 +26,7 @@ def _routes Routes.draw do resources :posts + resources :users end def default_url_options diff --git a/test/javascript/public/test/form_builders/validateSimpleForm.js b/test/javascript/public/test/form_builders/validateSimpleForm.js index 3643175..7883471 100644 --- a/test/javascript/public/test/form_builders/validateSimpleForm.js +++ b/test/javascript/public/test/form_builders/validateSimpleForm.js @@ -21,7 +21,8 @@ QUnit.module('Validate SimpleForm', { }, validators: { 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] }, - 'user[date_of_birth]': { presence: [{ message: 'must be present' }] } + 'user[date_of_birth]': { presence: [{ message: 'must be present' }] }, + 'user[role_ids]': { presence: [{ message: 'must be present' }] } } } @@ -31,22 +32,66 @@ QUnit.module('Validate SimpleForm', { 'data-client-side-validations': JSON.stringify(dataCsv), method: 'post', id: 'new_user' - })) - .find('form') - .append($('
')) - .find('div') - .append($('', { - name: 'user[name]', - id: 'user_name', - type: 'text' - })) - .append($('')) - .append($('', { - name: 'user[date_of_birth]', - id: 'date_of_birth', - type: 'text', - 'data-client-side-validations-wrapper': 'custom_date_wrapper' - })) + }) + .append($('
') + .append($('', { + name: 'user[name]', + id: 'user_name', + type: 'text' + })) + .append($('')) + ) + .append($('
') + .append($('', { + name: 'user[date_of_birth]', + id: 'date_of_birth', + type: 'text', + 'data-client-side-validations-wrapper': 'custom_date_wrapper' + })) + ) + .append($('
', { class: "input check_boxes required user_roles" }) + .append('') + .append('') + .append($('') + .append($('