├── .babelrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── eslint.yml │ ├── javascript.yml │ ├── rubocop.yml │ └── ruby.yml ├── .gitignore ├── .rubocop.yml ├── Appraisals ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── client_side_validations-simple_form.gemspec ├── dist ├── simple-form.bootstrap4.esm.js ├── simple-form.bootstrap4.js ├── simple-form.esm.js └── simple-form.js ├── eslint.config.mjs ├── gemfiles ├── csv_22.0.gemfile └── csv_edge.gemfile ├── lib └── client_side_validations │ ├── generators │ └── simple_form.rb │ ├── simple_form.rb │ └── simple_form │ ├── engine.rb │ ├── form_builder.rb │ └── version.rb ├── package.json ├── rollup.config.mjs ├── src ├── index.bootstrap4.js ├── index.js └── utils.js ├── test ├── action_view │ ├── models.rb │ ├── models │ │ ├── category.rb │ │ ├── comment.rb │ │ └── post.rb │ └── test_helper.rb ├── base_helper.rb ├── generators │ └── cases │ │ └── test_generators.rb ├── javascript │ ├── config.ru │ ├── public │ │ └── test │ │ │ ├── form_builders │ │ │ ├── validateSimpleForm.js │ │ │ ├── validateSimpleFormBootstrap.js │ │ │ ├── validateSimpleFormBootstrap4.js │ │ │ └── validateSimpleFormBootstrap5.js │ │ │ └── settings.js │ ├── run-qunit.mjs │ ├── server.rb │ └── views │ │ ├── index.erb │ │ └── layout.erb ├── simple_form │ └── cases │ │ ├── helper.rb │ │ ├── test_engine.rb │ │ ├── test_form_builder.rb │ │ └── test_form_helpers.rb └── test_loader.rb └── vendor └── assets └── javascripts ├── rails.validations.simple_form.bootstrap4.js └── rails.validations.simple_form.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/** linguist-generated 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Steps to reproduce* 11 | 12 | ### Expected behavior* 13 | _Tell us what should happen_ 14 | 15 | ### Actual behavior* 16 | _Tell us what happens instead_ 17 | 18 | ### System configuration* 19 | **Rails version**: 20 | 21 | **Ruby version**: 22 | 23 | **Client Side Validations version**: 24 | 25 | **Client Side Validations Simple Form version**: 26 | 27 | **Bootstrap version (if used)**: 28 | 29 | ### Code snippet from your model of the validations* 30 | 31 | ### Relevant part of `application.js`* 32 | 33 | ### The whole form code from your template* 34 | 35 | ### The resulting HTML* 36 | 37 | ### Browser's development console output* 38 | - [ ] I confirm that my browser's development console output does not contain errors 39 | 40 | ### Additional JavaScript Libraries* 41 | _If your issue depends on other JavaScript libraries, please list them here. E.g: *Bootstrap Modal v3.3.7, jQuery UI Datepicker 1.12.4*._ 42 | 43 | ### Repository demostrating the issue 44 | Debugging CSV issues is a time consuming task. If you want to speed up things, please 45 | provide a link to a repository showing the issue. 46 | 47 | --- 48 | 49 | **\*** Failure to include this requirement may result in the issue being closed. 50 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: bundler 8 | directory: "/" 9 | schedule: 10 | interval: daily 11 | open-pull-requests-limit: 10 12 | - package-ecosystem: npm 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | open-pull-requests-limit: 10 17 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | eslint: 14 | name: ESLint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: pnpm/action-setup@v4 19 | - name: Set up Node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: '22' 23 | - name: Install node dependencies 24 | run: pnpm install 25 | - name: Run JavaScript linter 26 | run: pnpm eslint 27 | -------------------------------------------------------------------------------- /.github/workflows/javascript.yml: -------------------------------------------------------------------------------- 1 | name: JavaScript tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | name: JavaScript Tests 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Ruby 19 | uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: '3.4' 22 | bundler-cache: true 23 | - uses: pnpm/action-setup@v4 24 | - name: Set up Node 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: '22' 28 | - name: Install node dependencies 29 | run: pnpm install 30 | - name: Run tests 31 | run: bundle exec rake test:js 32 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | name: Rubocop 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | rubocop: 14 | name: Rubocop 15 | runs-on: ${{ matrix.os }} 16 | env: 17 | BUNDLE_JOBS: 4 18 | BUNDLE_RETRY: 3 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest] 22 | ruby-version: ['3.4'] 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{ matrix.ruby-version }} 30 | bundler-cache: true 31 | - name: Ruby linter 32 | run: bundle exec rubocop -f github 33 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | name: Ruby Tests 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | ruby-version: ['3.1', '3.2', '3.3', '3.4'] 19 | gemfile: [ csv_22.0 ] 20 | channel: ['stable'] 21 | 22 | include: 23 | - ruby-version: 'head' 24 | gemfile: csv_22.0 25 | channel: 'experimental' 26 | - ruby-version: '3.1' 27 | gemfile: csv_edge 28 | channel: 'experimental' 29 | - ruby-version: '3.2' 30 | gemfile: csv_edge 31 | channel: 'experimental' 32 | - ruby-version: '3.3' 33 | gemfile: csv_edge 34 | channel: 'experimental' 35 | - ruby-version: '3.4' 36 | gemfile: csv_edge 37 | channel: 'experimental' 38 | - ruby-version: 'head' 39 | gemfile: csv_edge 40 | channel: 'experimental' 41 | 42 | env: 43 | BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile 44 | 45 | continue-on-error: ${{ matrix.channel != 'stable' }} 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | - name: Set up Ruby 50 | uses: ruby/setup-ruby@v1 51 | with: 52 | ruby-version: ${{ matrix.ruby-version }} 53 | bundler-cache: true 54 | - name: Run tests 55 | run: bundle exec rake test:ruby 56 | - name: Coveralls Parallel 57 | uses: coverallsapp/github-action@v2 58 | with: 59 | github-token: ${{ secrets.github_token }} 60 | flag-name: run-${{ matrix.ruby-version }} 61 | parallel: true 62 | 63 | coverage: 64 | name: Coverage 65 | needs: test 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Coveralls Finished 69 | uses: coverallsapp/github-action@v2 70 | with: 71 | github-token: ${{ secrets.github_token }} 72 | parallel-finished: true 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | *.lock 4 | pkg/* 5 | tags 6 | test/generators/tmp/* 7 | *.swp 8 | .*rc 9 | bundler_stubs/* 10 | binstubs/* 11 | bin/* 12 | coverage 13 | test/log 14 | .byebug_history 15 | 16 | node_modules 17 | 18 | !.babelrc 19 | 20 | pnpm-lock.yaml 21 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - rubocop-minitest 3 | - rubocop-packaging 4 | - rubocop-performance 5 | - rubocop-rails 6 | - rubocop-rake 7 | 8 | AllCops: 9 | TargetRailsVersion: 6.1 10 | TargetRubyVersion: 3.1 11 | NewCops: enable 12 | DisplayStyleGuide: true 13 | ExtraDetails: true 14 | Exclude: 15 | - .git/**/* 16 | - gemfiles/**/* 17 | - node_modules/**/* 18 | - test/generators/tmp/**/* 19 | - tmp/**/* 20 | - vendor/**/* 21 | 22 | Layout/HashAlignment: 23 | EnforcedColonStyle: table 24 | EnforcedHashRocketStyle: table 25 | 26 | Layout/LineLength: 27 | Enabled: false 28 | 29 | Metrics/AbcSize: 30 | Max: 23.02 31 | 32 | Metrics/BlockLength: 33 | Exclude: 34 | - '*.gemspec' 35 | - 'Rakefile' 36 | - 'test/**/*' 37 | 38 | Metrics/ClassLength: 39 | Exclude: 40 | - 'test/**/*' 41 | 42 | Metrics/CyclomaticComplexity: 43 | Max: 7 # TODO: Lower to 6 44 | 45 | Metrics/MethodLength: 46 | Exclude: 47 | - 'test/**/*' 48 | 49 | Metrics/ModuleLength: 50 | Exclude: 51 | - 'test/**/*' 52 | 53 | Minitest/MultipleAssertions: 54 | Enabled: false 55 | 56 | Rails/RakeEnvironment: 57 | Enabled: false 58 | 59 | Style/Documentation: 60 | Enabled: false 61 | 62 | Style/IfUnlessModifier: 63 | Enabled: false 64 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise 'csv-22.0' do 4 | gem 'client_side_validations', '~> 22.0' 5 | end 6 | 7 | appraise 'csv-edge' do 8 | gem 'client_side_validations', git: 'https://github.com/DavyJonesLocker/client_side_validations.git', branch: 'main' 9 | end 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 17.0.0 / unreleased 4 | 5 | * [FEATURE] Drop Internet Explorer and other older browsers support 6 | * [ENHANCEMENT] Test against Ruby 3.3 and 3.4 7 | * [ENHANCEMENT] Update QUnit to 2.23.0 8 | 9 | ## 16.0.0 / 2023-09-02 10 | 11 | * [FEATURE] Do not require jQuery 12 | * [ENHANCEMENT] Test against Ruby 3.2 13 | * [ENHANCEMENT] Update QUnit to 2.19.4 14 | * [FEATURE] Drop support to Ruby 2.6 15 | * [FEATURE] Drop support to CSV < 22.0 16 | 17 | ## 15.0.0 / 2022-09-18 18 | 19 | * [FEATURE] Drop Ruby 2.5 support 20 | * [FEATURE] Drop Client Side Validations < 21.0 compatibility 21 | * [ENHANCEMENT] Test against jQuery 3.6.1 by default 22 | * [ENHANCEMENT] Update QUnit to 2.19.1 23 | * [ENHANCEMENT] Update development dependencies 24 | 25 | ## 14.1.0 / 2021-12-16 26 | 27 | * [FEATURE] Add Client Side Validations 20.0 compatibility 28 | * [ENHANCEMENT] Update development dependencies 29 | 30 | ## 14.0.1 / 2021-11-15 31 | 32 | * [ENHANCEMENT] Require MFA to publish gems 33 | * [ENHANCEMENT] Update development dependencies 34 | 35 | ## 14.0.0 / 2021-10-01 36 | 37 | * [FEATURE] Drop Ruby 2.4 support 38 | * [FEATURE] Drop Rails 5.0 and 5.1 support 39 | * [FEATURE] Drop legacy browsers support (including IE8 and IE9) 40 | * [FEATURE] Drop Yarn < 1.19 and Node < 12.0 support 41 | * [FEATURE] Add JavaScript sources to node package 42 | * [ENHANCEMENT] Minor JS Refactor 43 | * [ENHANCEMENT] Update development dependencies 44 | * [ENHANCEMENT] Update QUnit to 2.17.2 45 | 46 | ## 13.0.0 / 2021-03-26 47 | 48 | * [FEATURE] Insert validation feedback before help text [#116](https://github.com/DavyJonesLocker/client_side_validations-simple_form/pull/116) **POSSIBLE BREAKING CHANGE!** 49 | * [ENHANCEMENT] Test against jQuery 3.6.0 by default 50 | * [ENHANCEMENT] Update development dependencies 51 | 52 | ## 12.1.0 / 2020-02-13 53 | 54 | * Add CSV 18.0 compatibility 55 | * [ENHANCEMENT] Update development dependencies 56 | 57 | ## 12.0.0 / 2020-01-23 58 | 59 | * [FEATURE] Allow nested `:error` component [#111](https://github.com/DavyJonesLocker/client_side_validations-simple_form/pull/111) **POSSIBLE BREAKING CHANGE!** 60 | * [ENHANCEMENT] Default branch is now `main` **POSSIBLE BREAKING CHANGE!** 61 | * [ENHANCEMENT] Update QUnit to 2.14.0 62 | * [ENHANCEMENT] Update development dependencies 63 | 64 | ## 11.2.0 / 2020-12-21 65 | 66 | * [FEATURE] Allow Ruby 3.0.0 (really) 67 | * [ENHANCEMENT] Replace Thin with Webrick 68 | * [ENHANCEMENT] Update development dependencies 69 | 70 | ## 11.1.0 / 2020-10-10 71 | 72 | * [FEATURE] Allow Ruby 3.0.0 73 | * [ENHANCEMENT] Test against latest Ruby 2.7.2 74 | * [ENHANCEMENT] Update QUnit to 2.11.3 75 | * [ENHANCEMENT] Update development dependencies 76 | 77 | ## 11.0.0 / 2020-05-16 78 | 79 | * [FEATURE] Drop Ruby 2.3 support 80 | * [FEATURE] Add Client Side Validations 17.0 compatibility 81 | * [ENHANCEMENT] Test against jQuery 3.5.1 by default 82 | 83 | ## 10.1.0 / 2020-04-10 84 | 85 | * [FEATURE] Add jQuery 3.5.0 compatibility ([#77](https://github.com/DavyJonesLocker/client_side_validations-simple_form/pull/77)) 86 | * [ENHANCEMENT] Test against latest Ruby versions 87 | * [ENHANCEMENT] Update development dependencies 88 | 89 | ## 10.0.0 / 2020-03-18 90 | 91 | * [FEATURE] Fallback on `full_error` if `error` component is not found ([#75](https://github.com/DavyJonesLocker/client_side_validations-simple_form/issues/75)) 92 | * [FEATURE] Support multiple css classes in error element and input wrappers 93 | * [ENHANCEMENT] Update development dependencies 94 | 95 | ## 9.2.0 / 2019-12-25 96 | 97 | * [FEATURE] Ruby 2.7 support 98 | * [ENHANCEMENT] Update development dependencies 99 | 100 | ## 9.1.0 / 2019-10-06 101 | 102 | * [FEATURE] Add ClientSideValidations JS 0.1.0 compatibility 103 | * [ENHANCEMENT] Test against latest Ruby versions 104 | * [ENHANCEMENT] Update development dependencies 105 | 106 | ## 9.0.0 / 2019-09-30 107 | 108 | * [FEATURE] Drop Simple Form 4.x compatibility 109 | * [ENHANCEMENT] Test against latest Ruby versions 110 | * [ENHANCEMENT] Update development dependencies 111 | 112 | ## 8.0.0 / 2019-08-25 113 | 114 | * [FEATURE] Move to ES6 115 | * [FEATURE] Add ClientSideValidations 16.0 compatibility 116 | * [FEATURE] Add Webpacker compatibility 117 | * [FEATURE] Drop Simple Form 3.5 compatibility 118 | * [ENHANCEMENT] Update development dependencies 119 | 120 | ## 7.0.0 / 2019-05-14 121 | 122 | * [FEATURE] Add ClientSideValidations 15.0 compatibility 123 | * [FEATURE] Drop Ruby 2.2 support 124 | * [FEATURE] Drop ClientSideValidations < 15.0 compatibility 125 | * [ENHANCEMENT] Test against jQuery 3.4.1 by default 126 | 127 | ## 6.10.0 / 2019-04-23 128 | 129 | * [FEATURE] Add ClientSideValidations 14.0 compatibility 130 | * [ENHANCEMENT] Test against Ruby 2.6.3 131 | * [ENHANCEMENT] Test against jQuery 3.4.0 by default 132 | * [ENHANCEMENT] Update QUnit to 2.9.2 133 | * [ENHANCEMENT] Update development dependencies 134 | 135 | ## 6.9.0 / 2019-03-02 136 | 137 | * [FEATURE] Add ClientSideValidations 13.0 compatibility 138 | * [ENHANCEMENT] Test against Ruby 2.6.1 139 | * [ENHANCEMENT] Update QUnit to 2.9.2 140 | * [ENHANCEMENT] Update development dependencies 141 | 142 | ## 6.8.0 / 2018-12-12 143 | 144 | * [FEATURE] Add ClientSideValidations 12.0 compatibility 145 | * [ENHANCEMENT] Update QUnit to 2.8.0 146 | * [ENHANCEMENT] Update development dependencies 147 | 148 | ## 6.7.0 / 2018-09-09 149 | 150 | * [FEATURE] Add Bootstrap 4 support 151 | * [ENHANCEMENT] Update QUnit to 2.6.2 152 | * [ENHANCEMENT] Update development dependencies 153 | 154 | ## 6.6.0 / 2018-04-13 155 | 156 | * [FEATURE] Add Simple Form 4.0 compatibility 157 | * [ENHANCEMENT] Test against Ruby 2.2.10, 2.3.7, 2.4.4, and 2.5.1 158 | * [ENHANCEMENT] Test against jQuery 3.3.1 159 | * [ENHANCEMENT] Update development dependencies 160 | 161 | ## 6.5.1 / 2018-02-03 162 | 163 | * [ENHANCEMENT] Test against Ruby 2.2.9, 2.3.6, 2.4.3, and 2.5.0 164 | * [ENHANCEMENT] Update development dependencies 165 | 166 | ## 6.5.0 / 2017-11-29 167 | 168 | * [FEATURE] Add ClientSideValidations 11.0 compatibility 169 | 170 | ## 6.4.0 / 2017-10-09 171 | 172 | * [FEATURE] Add ClientSideValidations 10.0 compatibility 173 | * [ENHANCEMENT] Test against Ruby 2.2.8, 2.3.5, and 2.4.2 174 | * [ENHANCEMENT] Update development dependencies 175 | 176 | ## 6.3.0 / 2017-05-27 177 | 178 | * [ENHANCEMENT] Update runtime dependencies 179 | * [ENHANCEMENT] Update development dependencies 180 | * [ENHANCEMENT] Add spec for simple_fields_for to avoid regressions 181 | 182 | ## 6.2.0 / 2017-04-24 183 | 184 | * [ENHANCEMENT] Code cleanup 185 | * [ENHANCEMENT] Test against Ruby 2.2.7 and 2.3.4 186 | * [ENHANCEMENT] Update development dependencies 187 | 188 | ## 6.1.0 / 2017-03-23 189 | 190 | * [ENHANCEMENT] Use Ruby 2.3's Frozen String Literal Pragma 191 | * [ENHANCEMENT] Test against Ruby 2.4.1 192 | * [ENHANCEMENT] Test against jQuery 3.2.0 and 3.2.1 193 | * [ENHANCEMENT] Follow Vandamme's changelog conventions 194 | 195 | ## 6.0.0 / 2017-01-31 196 | 197 | * [FEATURE] ClientSideValidations 9.0 compatibility 198 | 199 | ## 5.2.0 / 2017-01-22 200 | 201 | * [FEATURE] ClientSideValidations 8.0 compatibility 202 | 203 | ## 5.1.0 / 2017-01-22 204 | 205 | * [FEATURE] ClientSideValidations 7.0 compatibility 206 | * [ENHANCEMENT] Update development dependencies 207 | 208 | ## 5.0.0 / 2017-01-20 209 | 210 | * [FEATURE] ClientSideValidations 6.0 compatibility 211 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines # 2 | 3 | ## Submitting a new issue ## 4 | 5 | If you need to open a new issue you *must* provide the following: 6 | 7 | 1. Version of ClientSideValidations 8 | 2. Version of ClientSideValidations-SimpleForm 9 | 3. Version of Rails 10 | 4. Code snippet from your model of the validations 11 | 5. The form code from your template 12 | 6. The resulting HTML 13 | 14 | Failure to include the above mentioned requirements will result in the 15 | issue being closed. 16 | 17 | If you want to ensure that your issue gets fixed *fast* you should 18 | attempt to reproduce the issue in an isolated example application that 19 | you can share. 20 | 21 | ## Making a pull request ## 22 | 23 | If you'd like to submit a pull request please adhere to the following: 24 | 25 | 1. Your code *must* be tested. 26 | 2. Make sure that `bundle exec rake` pass 27 | 3. Make sure that `bundle exec rake test:js` pass 28 | 29 | Please note that you must adhere to each of the above mentioned rules. 30 | Failure to do so will result in an immediate closing of the pull 31 | request. If you update and rebase the pull request to follow the 32 | guidelines your pull request will be re-opened and considered for 33 | inclusion. 34 | 35 | ## Coding standards 36 | 37 | ### Ruby 38 | 39 | - [Ruby Styleguide](https://github.com/rubocop/ruby-style-guide) 40 | 41 | ### Commits 42 | 43 | - [How to Write a Git Commit Message](https://cbea.ms/git-commit/#seven-rules) 44 | 45 | ## License 46 | 47 | By contributing your code, you agree to license your contribution under the terms of the [MIT License](LICENSE) 48 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | gem 'appraisal' 8 | gem 'byebug' 9 | gem 'm' 10 | gem 'minitest' 11 | gem 'mocha' 12 | gem 'rake' 13 | gem 'rubocop' 14 | gem 'rubocop-minitest' 15 | gem 'rubocop-packaging' 16 | gem 'rubocop-performance' 17 | gem 'rubocop-rails' 18 | gem 'rubocop-rake' 19 | gem 'shotgun' 20 | gem 'simplecov' 21 | gem 'simplecov-lcov' 22 | gem 'sinatra' 23 | gem 'webrick' 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Geremia Taglialatela, Brian Cardarella 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ClientSideValidations-SimpleForm # 2 | 3 | [![Gem Version](https://badge.fury.io/rb/client_side_validations-simple_form.svg)](https://badge.fury.io/rb/client_side_validations-simple_form) 4 | [![npm version](https://badge.fury.io/js/%40client-side-validations%2Fsimple-form.svg)](https://badge.fury.io/js/%40client-side-validations%2Fsimple-form) 5 | [![Ruby Build Status](https://github.com/DavyJonesLocker/client_side_validations-simple_form/actions/workflows/ruby.yml/badge.svg)](https://github.com/DavyJonesLocker/client_side_validations-simple_form/actions) 6 | [![JavaScript Build Status](https://github.com/DavyJonesLocker/client_side_validations-simple_form/actions/workflows/javascript.yml/badge.svg)](https://github.com/DavyJonesLocker/client_side_validations-simple_form/actions) 7 | [![Maintainability](https://api.codeclimate.com/v1/badges/b9e9cbbd0d9f454adba7/maintainability)](https://codeclimate.com/github/DavyJonesLocker/client_side_validations-simple_form/maintainability) 8 | [![Coverage Status](https://coveralls.io/repos/github/DavyJonesLocker/client_side_validations-simple_form/badge.svg?branch=main)](https://coveralls.io/github/DavyJonesLocker/client_side_validations-simple_form?branch=main) 9 | 10 | [Simple Form](https://github.com/heartcombo/simple_form) plugin for [ClientSideValidations](https://github.com/DavyJonesLocker/client_side_validations) 11 | 12 | ## Installation ## 13 | 14 | In your Gemfile add the following: 15 | 16 | ```ruby 17 | gem 'simple_form' 18 | gem 'client_side_validations' 19 | gem 'client_side_validations-simple_form' 20 | ``` 21 | 22 | Order matters here. `simple_form` and `client_side_validations` need to be 23 | required **before** `client_side_validations-simple_form`. 24 | 25 | [Follow the remaining installation instructions for ClientSideValidations](https://github.com/DavyJonesLocker/client_side_validations/blob/main/README.md) 26 | 27 | ### JavaScript file ### 28 | 29 | Instructions depend on your technology stack. 30 | 31 | #### When using Webpacker #### 32 | 33 | Make sure that you are requiring jQuery and Client Side Validations. 34 | 35 | Add the following package: 36 | 37 | ```sh 38 | yarn add @client-side-validations/simple-form 39 | ``` 40 | 41 | Then, according to the CSS framework and module system you are using, add 42 | **one** of the following lines to your `app/javascript/packs/application.js` 43 | pack: 44 | 45 | ```js 46 | // No framework / Generic frameworks / Bootstrap 3 with `import` syntax 47 | import '@client-side-validations/simple-form/src' 48 | 49 | // Bootstrap 4+ with `import` syntax 50 | import '@client-side-validations/simple-form/src/index.bootstrap4' 51 | 52 | // No framework / Generic frameworks / Bootstrap 3 with `require` syntax 53 | require('@client-side-validations/simple-form') 54 | 55 | // Bootstrap 4+ with `require` syntax 56 | require('@client-side-validations/simple-form/dist/simple-form.bootstrap4.esm') 57 | ``` 58 | 59 | #### When using Sprockets #### 60 | 61 | Make sure that you are requiring jQuery and Client Side Validations. 62 | 63 | According to the web framework you are using, add **one** of the following 64 | lines to your `app/assets/javascripts/application.js`, **after** 65 | `//= require rails.validations` 66 | 67 | ```js 68 | // No framework / Generic frameworks / Bootstrap 3 69 | //= require rails.validations.simple_form 70 | 71 | // Bootstrap 4+ 72 | //= require rails.validations.simple_form.bootstrap4 73 | ``` 74 | 75 | If you need to copy the asset files from the gem into your project, run: 76 | 77 | ``` 78 | rails g client_side_validations:copy_assets 79 | ``` 80 | 81 | Note: If you run `copy_assets`, you will need to run it again each time you update this project. 82 | 83 | ## Usage ## 84 | 85 | The usage is the same as `ClientSideValidations`, just pass `validate: true` to the form builder 86 | 87 | ```ruby 88 | <%= simple_form_for @book, validate: true do |book| %> 89 | <%= book.input :name %> 90 | <% end %> 91 | ``` 92 | 93 | Per-input options are done with `:validate` 94 | 95 | ```ruby 96 | <%= book.input :name, validate: { presence: true, uniqueness: false } %> 97 | ``` 98 | 99 | ## Authors ## 100 | 101 | [Brian Cardarella](https://twitter.com/bcardarella) 102 | 103 | [Geremia Taglialatela](https://twitter.com/gtagliala) 104 | 105 | [We are very thankful for the many contributors](https://github.com/DavyJonesLocker/client_side_validations-simple_form/graphs/contributors) 106 | 107 | ## Versioning ## 108 | 109 | This gem follows [Semantic Versioning](https://semver.org) 110 | 111 | ## Want to help? ## 112 | 113 | Please do! We are always looking to improve this gem. Please see our 114 | [Contribution Guidelines](https://github.com/DavyJonesLocker/client_side_validations-simple_form/blob/main/CONTRIBUTING.md) 115 | on how to properly submit issues and pull requests. 116 | 117 | ## Legal ## 118 | 119 | [DockYard](https://dockyard.com/), LLC © 2012-2023 120 | 121 | [@dockyard](https://twitter.com/dockyard) 122 | 123 | [Licensed under the MIT license](https://opensource.org/licenses/mit) 124 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler' 4 | Bundler::GemHelper.install_tasks 5 | require 'rubocop/rake_task' 6 | 7 | RuboCop::RakeTask.new 8 | 9 | task default: [:rubocop, 'test:ruby'] 10 | 11 | require 'rake/testtask' 12 | namespace :test do 13 | desc %(Run all tests) 14 | task all: [:rubocop, :lint_javascript, 'test:ruby', 'test:js'] 15 | 16 | desc %(Test Ruby code) 17 | Rake::TestTask.new(:ruby) do |test| 18 | test.libs << 'lib' << 'test' 19 | test.test_files = Dir.glob("#{File.dirname(__FILE__)}/test/**/test_*.rb") 20 | test.warning = false 21 | end 22 | 23 | desc %(Test JavaScript code) 24 | task js: ['regenerate_javascript', 'test:server', 'test:qunit'] 25 | 26 | desc %(Starts the test server) 27 | task :server do 28 | puts "Opening test app at #{test_url} ..." 29 | server_command = "bundle exec rackup test/javascript/config.ru -q -p #{test_port}" 30 | 31 | if ENV['UI'] 32 | system server_command 33 | else 34 | @server = fork { exec server_command } 35 | end 36 | 37 | # Give Sinatra some time to start 38 | sleep 3 39 | end 40 | 41 | desc %(Starts the test server which reloads everything on each refresh) 42 | task :reloadable do 43 | exec "bundle exec shotgun test/javascript/config.ru -p #{test_port} --server webrick" 44 | end 45 | 46 | desc %(Starts qunit tests) 47 | task :qunit do 48 | if ENV['UI'] 49 | system(*browse_cmd(url)) 50 | else 51 | run_headless_tests 52 | end 53 | end 54 | end 55 | 56 | desc %(Lint JavaScript files) 57 | task :lint_javascript do 58 | run_pnpm_script 'eslint' 59 | end 60 | 61 | desc %(Regenerate JavaScript files) 62 | task :regenerate_javascript do 63 | run_pnpm_script 'build' 64 | end 65 | 66 | desc %(Commit JavaScript files) 67 | task :commit_javascript do 68 | perform_git_commit 69 | end 70 | 71 | def perform_git_commit 72 | system('git add dist vendor', out: File::NULL, err: File::NULL) 73 | 74 | if system('git commit -m "Regenerated JavaScript files"', out: File::NULL, err: File::NULL) 75 | puts 'Committed changes' 76 | else 77 | puts 'Nothing to commit' 78 | end 79 | end 80 | 81 | # Returns an array e.g.: ['open', 'http://example.com'] 82 | # rubocop:disable Metrics/CyclomaticComplexity 83 | def browse_cmd(url) 84 | require 'rbconfig' 85 | browser = ENV.fetch('BROWSER') { RbConfig::CONFIG['host_os'].include?('darwin') && 'open' } || 86 | (RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw|windows/ && 'start') || 87 | %w[xdg-open x-www-browser firefox opera mozilla netscape].find { |comm| which comm } 88 | 89 | abort('ERROR: no web browser detected') unless browser 90 | Array(browser) << url 91 | end 92 | # rubocop:enable Metrics/CyclomaticComplexity 93 | 94 | # which('ruby') #=> /usr/bin/ruby 95 | def which(cmd) 96 | exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] 97 | ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| 98 | exts.each do |ext| 99 | exe = "#{path}/#{cmd}#{ext}" 100 | return exe if File.executable? exe 101 | end 102 | end 103 | nil 104 | end 105 | 106 | def run_headless_tests 107 | run_pnpm_script 'test', "#{test_url}?autostart=false" do 108 | Process.kill 'INT', @server 109 | end 110 | end 111 | 112 | def run_pnpm_script(script, options = '') 113 | require 'English' 114 | 115 | system "pnpm #{script} #{options}" 116 | exit_code = $CHILD_STATUS.exitstatus 117 | 118 | yield if block_given? 119 | 120 | exit exit_code unless exit_code.zero? 121 | end 122 | 123 | def test_port 124 | @test_port ||= 4567 125 | end 126 | 127 | def test_url 128 | @test_url ||= "http://localhost:#{test_port}" 129 | end 130 | 131 | task build: %i[regenerate_javascript commit_javascript] 132 | -------------------------------------------------------------------------------- /client_side_validations-simple_form.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'client_side_validations/simple_form/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'client_side_validations-simple_form' 9 | spec.version = ClientSideValidations::SimpleForm::VERSION 10 | spec.authors = ['Geremia Taglialatela', 'Brian Cardarella'] 11 | spec.email = ['tagliala.dev@gmail.com', 'bcardarella@gmail.com'] 12 | 13 | spec.summary = 'ClientSideValidations SimpleForm' 14 | spec.description = 'SimpleForm Plugin for ClientSideValidations' 15 | spec.homepage = 'https://github.com/DavyJonesLocker/client_side_validations-simple_form' 16 | spec.license = 'MIT' 17 | 18 | spec.metadata['rubygems_mfa_required'] = 'true' 19 | 20 | spec.metadata['bug_tracker_uri'] = 'https://github.com/DavyJonesLocker/client_side_validations-simple_form/issues' 21 | spec.metadata['changelog_uri'] = 'https://github.com/DavyJonesLocker/client_side_validations-simple_form/blob/main/CHANGELOG.md' 22 | spec.metadata['source_code_uri'] = 'https://github.com/DavyJonesLocker/client_side_validations-simple_form' 23 | 24 | spec.files = Dir.glob('{CHANGELOG.md,LICENSE.md,README.md,lib/**/*.rb,vendor/**/*.js}', File::FNM_DOTMATCH) 25 | spec.require_paths = ['lib'] 26 | 27 | spec.platform = Gem::Platform::RUBY 28 | spec.required_ruby_version = '>= 3.1' 29 | 30 | spec.add_dependency 'client_side_validations', '>= 22.0', '< 24' 31 | spec.add_dependency 'simple_form', '~> 5.2' 32 | end 33 | -------------------------------------------------------------------------------- /dist/simple-form.bootstrap4.esm.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Client Side Validations Simple Form JS (Default) - v0.5.0 (https://github.com/DavyJonesLocker/client_side_validations-simple_form) 3 | * Copyright (c) 2025 Geremia Taglialatela, Brian Cardarella 4 | * Licensed under MIT (https://opensource.org/licenses/mit-license.php) 5 | */ 6 | 7 | import ClientSideValidations from '@client-side-validations/client-side-validations'; 8 | 9 | const addClass = (element, customClass) => { 10 | if (customClass) { 11 | element.classList.add(...customClass.split(' ')); 12 | } 13 | }; 14 | const removeClass = (element, customClass) => { 15 | if (customClass) { 16 | element.classList.remove(...customClass.split(' ')); 17 | } 18 | }; 19 | 20 | ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { 21 | add: function ($element, settings, message) { 22 | this.wrapper(settings.wrapper).add.call(this, $element[0], settings, message); 23 | }, 24 | remove: function ($element, settings) { 25 | this.wrapper(settings.wrapper).remove.call(this, $element[0], settings); 26 | }, 27 | wrapper: function (name) { 28 | return this.wrappers[name] || this.wrappers.default; 29 | }, 30 | wrappers: { 31 | default: { 32 | add(element, settings, message) { 33 | const wrapperElement = element.parentElement; 34 | let errorElement = wrapperElement.querySelector("".concat(settings.error_tag, ".invalid-feedback")); 35 | if (!errorElement) { 36 | const formTextElement = wrapperElement.querySelector('.form-text'); 37 | errorElement = document.createElement(settings.error_tag); 38 | addClass(errorElement, 'invalid-feedback'); 39 | errorElement.textContent = message; 40 | if (formTextElement) { 41 | formTextElement.before(errorElement); 42 | } else { 43 | wrapperElement.appendChild(errorElement); 44 | } 45 | } 46 | addClass(wrapperElement, settings.wrapper_error_class); 47 | addClass(element, 'is-invalid'); 48 | errorElement.textContent = message; 49 | }, 50 | remove(element, settings) { 51 | const wrapperElement = element.parentElement; 52 | const errorElement = wrapperElement.querySelector("".concat(settings.error_tag, ".invalid-feedback")); 53 | removeClass(wrapperElement, settings.wrapper_error_class); 54 | removeClass(element, 'is-invalid'); 55 | if (errorElement) { 56 | errorElement.remove(); 57 | } 58 | } 59 | } 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /dist/simple-form.bootstrap4.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Client Side Validations Simple Form JS (Bootstrap 4+) - v0.5.0 (https://github.com/DavyJonesLocker/client_side_validations-simple_form) 3 | * Copyright (c) 2025 Geremia Taglialatela, Brian Cardarella 4 | * Licensed under MIT (https://opensource.org/licenses/mit-license.php) 5 | */ 6 | 7 | (function (global, factory) { 8 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('@client-side-validations/client-side-validations')) : 9 | typeof define === 'function' && define.amd ? define(['@client-side-validations/client-side-validations'], factory) : 10 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ClientSideValidations)); 11 | })(this, (function (ClientSideValidations) { 'use strict'; 12 | 13 | const addClass = (element, customClass) => { 14 | if (customClass) { 15 | element.classList.add(...customClass.split(' ')); 16 | } 17 | }; 18 | const removeClass = (element, customClass) => { 19 | if (customClass) { 20 | element.classList.remove(...customClass.split(' ')); 21 | } 22 | }; 23 | 24 | ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { 25 | add: function ($element, settings, message) { 26 | this.wrapper(settings.wrapper).add.call(this, $element[0], settings, message); 27 | }, 28 | remove: function ($element, settings) { 29 | this.wrapper(settings.wrapper).remove.call(this, $element[0], settings); 30 | }, 31 | wrapper: function (name) { 32 | return this.wrappers[name] || this.wrappers.default; 33 | }, 34 | wrappers: { 35 | default: { 36 | add(element, settings, message) { 37 | const wrapperElement = element.parentElement; 38 | let errorElement = wrapperElement.querySelector("".concat(settings.error_tag, ".invalid-feedback")); 39 | if (!errorElement) { 40 | const formTextElement = wrapperElement.querySelector('.form-text'); 41 | errorElement = document.createElement(settings.error_tag); 42 | addClass(errorElement, 'invalid-feedback'); 43 | errorElement.textContent = message; 44 | if (formTextElement) { 45 | formTextElement.before(errorElement); 46 | } else { 47 | wrapperElement.appendChild(errorElement); 48 | } 49 | } 50 | addClass(wrapperElement, settings.wrapper_error_class); 51 | addClass(element, 'is-invalid'); 52 | errorElement.textContent = message; 53 | }, 54 | remove(element, settings) { 55 | const wrapperElement = element.parentElement; 56 | const errorElement = wrapperElement.querySelector("".concat(settings.error_tag, ".invalid-feedback")); 57 | removeClass(wrapperElement, settings.wrapper_error_class); 58 | removeClass(element, 'is-invalid'); 59 | if (errorElement) { 60 | errorElement.remove(); 61 | } 62 | } 63 | } 64 | } 65 | }; 66 | 67 | })); 68 | -------------------------------------------------------------------------------- /dist/simple-form.esm.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Client Side Validations Simple Form JS (Default) - v0.5.0 (https://github.com/DavyJonesLocker/client_side_validations-simple_form) 3 | * Copyright (c) 2025 Geremia Taglialatela, Brian Cardarella 4 | * Licensed under MIT (https://opensource.org/licenses/mit-license.php) 5 | */ 6 | 7 | import ClientSideValidations from '@client-side-validations/client-side-validations'; 8 | 9 | const addClass = (element, customClass) => { 10 | if (customClass) { 11 | element.classList.add(...customClass.split(' ')); 12 | } 13 | }; 14 | const removeClass = (element, customClass) => { 15 | if (customClass) { 16 | element.classList.remove(...customClass.split(' ')); 17 | } 18 | }; 19 | 20 | ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { 21 | add: function ($element, settings, message) { 22 | this.wrapper(settings.wrapper).add.call(this, $element[0], settings, message); 23 | }, 24 | remove: function ($element, settings) { 25 | this.wrapper(settings.wrapper).remove.call(this, $element[0], settings); 26 | }, 27 | wrapper: function (name) { 28 | return this.wrappers[name] || this.wrappers.default; 29 | }, 30 | wrappers: { 31 | default: { 32 | add(element, settings, message) { 33 | const wrapperElement = element.closest("".concat(settings.wrapper_tag, ".").concat(settings.wrapper_class.replace(/ /g, '.'))); 34 | let errorElement = wrapperElement.querySelector("".concat(settings.error_tag, ".").concat(settings.error_class.replace(/ /g, '.'))); 35 | if (!errorElement) { 36 | errorElement = document.createElement(settings.error_tag); 37 | addClass(errorElement, settings.error_class); 38 | errorElement.textContent = message; 39 | wrapperElement.appendChild(errorElement); 40 | } 41 | addClass(wrapperElement, settings.wrapper_error_class); 42 | errorElement.textContent = message; 43 | }, 44 | remove(element, settings) { 45 | const wrapperElement = element.closest("".concat(settings.wrapper_tag, ".").concat(settings.wrapper_class.replace(/ /g, '.'))); 46 | const errorElement = wrapperElement.querySelector("".concat(settings.error_tag, ".").concat(settings.error_class.replace(/ /g, '.'))); 47 | removeClass(wrapperElement, settings.wrapper_error_class); 48 | if (errorElement) { 49 | errorElement.remove(); 50 | } 51 | } 52 | } 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /dist/simple-form.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Client Side Validations Simple Form JS (Default) - v0.5.0 (https://github.com/DavyJonesLocker/client_side_validations-simple_form) 3 | * Copyright (c) 2025 Geremia Taglialatela, Brian Cardarella 4 | * Licensed under MIT (https://opensource.org/licenses/mit-license.php) 5 | */ 6 | 7 | (function (global, factory) { 8 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('@client-side-validations/client-side-validations')) : 9 | typeof define === 'function' && define.amd ? define(['@client-side-validations/client-side-validations'], factory) : 10 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ClientSideValidations)); 11 | })(this, (function (ClientSideValidations) { 'use strict'; 12 | 13 | const addClass = (element, customClass) => { 14 | if (customClass) { 15 | element.classList.add(...customClass.split(' ')); 16 | } 17 | }; 18 | const removeClass = (element, customClass) => { 19 | if (customClass) { 20 | element.classList.remove(...customClass.split(' ')); 21 | } 22 | }; 23 | 24 | ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { 25 | add: function ($element, settings, message) { 26 | this.wrapper(settings.wrapper).add.call(this, $element[0], settings, message); 27 | }, 28 | remove: function ($element, settings) { 29 | this.wrapper(settings.wrapper).remove.call(this, $element[0], settings); 30 | }, 31 | wrapper: function (name) { 32 | return this.wrappers[name] || this.wrappers.default; 33 | }, 34 | wrappers: { 35 | default: { 36 | add(element, settings, message) { 37 | const wrapperElement = element.closest("".concat(settings.wrapper_tag, ".").concat(settings.wrapper_class.replace(/ /g, '.'))); 38 | let errorElement = wrapperElement.querySelector("".concat(settings.error_tag, ".").concat(settings.error_class.replace(/ /g, '.'))); 39 | if (!errorElement) { 40 | errorElement = document.createElement(settings.error_tag); 41 | addClass(errorElement, settings.error_class); 42 | errorElement.textContent = message; 43 | wrapperElement.appendChild(errorElement); 44 | } 45 | addClass(wrapperElement, settings.wrapper_error_class); 46 | errorElement.textContent = message; 47 | }, 48 | remove(element, settings) { 49 | const wrapperElement = element.closest("".concat(settings.wrapper_tag, ".").concat(settings.wrapper_class.replace(/ /g, '.'))); 50 | const errorElement = wrapperElement.querySelector("".concat(settings.error_tag, ".").concat(settings.error_class.replace(/ /g, '.'))); 51 | removeClass(wrapperElement, settings.wrapper_error_class); 52 | if (errorElement) { 53 | errorElement.remove(); 54 | } 55 | } 56 | } 57 | } 58 | }; 59 | 60 | })); 61 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import neostandard from 'neostandard' 2 | import compat from 'eslint-plugin-compat' 3 | 4 | export default [ 5 | { 6 | ignores: [ 7 | 'coverage/*', 8 | 'dist/*', 9 | 'test/*', 10 | 'vendor/*', 11 | ] 12 | }, 13 | compat.configs['flat/recommended'], 14 | ...neostandard() 15 | ] 16 | -------------------------------------------------------------------------------- /gemfiles/csv_22.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal", "~> 2.5" 6 | gem "byebug", "~> 11.1" 7 | gem "m", "~> 1.6" 8 | gem "minitest", "~> 5.19" 9 | gem "mocha", "~> 2.1" 10 | gem "rake", "~> 13.0" 11 | gem "rubocop", "~> 1.56" 12 | gem "rubocop-minitest", "~> 0.31.0" 13 | gem "rubocop-packaging", "~> 0.5.2" 14 | gem "rubocop-performance", "~> 1.19" 15 | gem "rubocop-rails", "~> 2.20" 16 | gem "rubocop-rake", "~> 0.6.0" 17 | gem "shotgun", "~> 0.9.2" 18 | gem "simplecov", "~> 0.22.0" 19 | gem "simplecov-lcov", "~> 0.8.0" 20 | gem "sinatra", "~> 3.1" 21 | gem "webrick", "~> 1.7" 22 | gem "client_side_validations", "~> 22.0" 23 | 24 | gemspec path: "../" 25 | -------------------------------------------------------------------------------- /gemfiles/csv_edge.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal", "~> 2.5" 6 | gem "byebug", "~> 11.1" 7 | gem "m", "~> 1.6" 8 | gem "minitest", "~> 5.19" 9 | gem "mocha", "~> 2.1" 10 | gem "rake", "~> 13.0" 11 | gem "rubocop", "~> 1.56" 12 | gem "rubocop-minitest", "~> 0.31.0" 13 | gem "rubocop-packaging", "~> 0.5.2" 14 | gem "rubocop-performance", "~> 1.19" 15 | gem "rubocop-rails", "~> 2.20" 16 | gem "rubocop-rake", "~> 0.6.0" 17 | gem "shotgun", "~> 0.9.2" 18 | gem "simplecov", "~> 0.22.0" 19 | gem "simplecov-lcov", "~> 0.8.0" 20 | gem "sinatra", "~> 3.1" 21 | gem "webrick", "~> 1.7" 22 | gem "client_side_validations", git: "https://github.com/DavyJonesLocker/client_side_validations.git", branch: "main" 23 | 24 | gemspec path: "../" 25 | -------------------------------------------------------------------------------- /lib/client_side_validations/generators/simple_form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ClientSideValidations 4 | module Generators 5 | class SimpleForm 6 | def self.assets 7 | [{ 8 | path: File.expand_path('../../../vendor/assets/javascripts', __dir__), 9 | file: 'rails.validations.simple_form.js' 10 | }, 11 | { 12 | path: File.expand_path('../../../vendor/assets/javascripts', __dir__), 13 | file: 'rails.validations.simple_form.bootstrap4.js' 14 | }] 15 | end 16 | 17 | Generators.register_assets(self) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/client_side_validations/simple_form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simple_form' 4 | require 'client_side_validations' 5 | 6 | require_relative 'simple_form/form_builder' 7 | 8 | if defined?(Rails) 9 | require_relative 'simple_form/engine' 10 | require_relative 'generators/simple_form' 11 | end 12 | -------------------------------------------------------------------------------- /lib/client_side_validations/simple_form/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ClientSideValidations 4 | module SimpleForm 5 | class Engine < ::Rails::Engine 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/client_side_validations/simple_form/form_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ClientSideValidations 4 | module SimpleForm 5 | module FormBuilder 6 | def client_side_form_settings(options, _form_helper) 7 | { 8 | type: self.class.name, 9 | error_class: wrapper_error_component.defaults[:class].join(' '), 10 | error_tag: wrapper_error_component.defaults[:tag], 11 | wrapper_error_class: wrapper.defaults[:error_class], 12 | wrapper_tag: wrapper.defaults[:tag], 13 | wrapper_class: wrapper.defaults[:class].join(' '), 14 | wrapper: options[:wrapper] || ::SimpleForm.default_wrapper 15 | } 16 | end 17 | 18 | def input(attribute_name, options = {}, &) 19 | if options.key?(:validate) 20 | options[:input_html] ||= {} 21 | options[:input_html][:validate] = options[:validate] 22 | options.delete(:validate) 23 | end 24 | 25 | super 26 | end 27 | 28 | private 29 | 30 | def wrapper_error_component 31 | @wrapper_error_component ||= 32 | if namespace_present?(wrapper, :error) 33 | wrapper.find(:error) 34 | else 35 | wrapper.find(:full_error) 36 | end 37 | end 38 | 39 | def namespace_present?(component, namespace) 40 | return true if component.namespace == namespace 41 | 42 | component.try(:components)&.each do |child_component| 43 | return true if namespace_present?(child_component, namespace) 44 | end 45 | 46 | false 47 | end 48 | end 49 | end 50 | end 51 | 52 | SimpleForm::FormBuilder.prepend ClientSideValidations::SimpleForm::FormBuilder 53 | -------------------------------------------------------------------------------- /lib/client_side_validations/simple_form/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ClientSideValidations 4 | module SimpleForm 5 | VERSION = '17.0.0.alpha1' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@client-side-validations/simple-form", 3 | "description": "Client Side Validations Simple Form plugin", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/DavyJonesLocker/client_side_validations-simple_form.git" 7 | }, 8 | "keywords": [ 9 | "rails", 10 | "browser", 11 | "client side validations", 12 | "simple form" 13 | ], 14 | "author": "Geremia Taglialatela ", 15 | "contributors": [], 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/DavyJonesLocker/client_side_validations-simple_form/issues" 19 | }, 20 | "homepage": "https://github.com/DavyJonesLocker/client_side_validations-simple_form", 21 | "scripts": { 22 | "build": "rollup -c", 23 | "test": "test/javascript/run-qunit.mjs" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.26.10", 27 | "@babel/preset-env": "^7.26.9", 28 | "@rollup/plugin-babel": "^6.0.4", 29 | "@rollup/plugin-node-resolve": "^16.0.1", 30 | "chrome-launcher": "^1.1.2", 31 | "eslint": "^9.25.1", 32 | "eslint-plugin-compat": "^6.0.2", 33 | "neostandard": "^0.12.1", 34 | "puppeteer-core": "^24.7.2", 35 | "rollup": "^4.40.0", 36 | "rollup-plugin-copy": "^3.5.0" 37 | }, 38 | "peerDependencies": { 39 | "@client-side-validations/client-side-validations": "^0.4.0" 40 | }, 41 | "main": "dist/simple-form.js", 42 | "module": "dist/simple-form.esm.js", 43 | "engines": { 44 | "node": ">= 18.12" 45 | }, 46 | "packageManager": "pnpm@^9.12.1", 47 | "browserslist": [ 48 | ">= 0.5%", 49 | "last 2 major versions", 50 | "not dead", 51 | "Chrome >= 60", 52 | "Firefox >= 60", 53 | "Firefox ESR", 54 | "iOS >= 12", 55 | "Safari >= 12" 56 | ], 57 | "files": [ 58 | "dist", 59 | "src/**/*.js" 60 | ], 61 | "standard": { 62 | "ignore": [ 63 | "/dist/", 64 | "/gemfiles/", 65 | "/test/", 66 | "/vendor/" 67 | ] 68 | }, 69 | "version": "0.5.0", 70 | "directories": { 71 | "lib": "lib", 72 | "test": "test" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel' 2 | import copy from 'rollup-plugin-copy' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | 5 | import { createRequire } from 'node:module' 6 | 7 | const require = createRequire(import.meta.url) 8 | const pkg = require('./package.json') 9 | 10 | const year = new Date().getFullYear() 11 | 12 | const banner = (formBuilderName) => `/*! 13 | * Client Side Validations Simple Form JS (${formBuilderName}) - v${pkg.version} (https://github.com/DavyJonesLocker/client_side_validations-simple_form) 14 | * Copyright (c) ${year} Geremia Taglialatela, Brian Cardarella 15 | * Licensed under MIT (https://opensource.org/licenses/mit-license.php) 16 | */ 17 | ` 18 | 19 | const umdModuleConfig = (inputFileName, outputModuleFileName, outputVendorFileName, formBuilderName) => ({ 20 | input: `src/${inputFileName}`, 21 | external: ['@client-side-validations/client-side-validations'], 22 | output: [ 23 | { 24 | file: outputModuleFileName, 25 | banner: banner(formBuilderName), 26 | format: 'umd', 27 | globals: { 28 | '@client-side-validations/client-side-validations': 'ClientSideValidations' 29 | } 30 | } 31 | ], 32 | plugins: [ 33 | resolve(), 34 | babel({ babelHelpers: 'bundled' }), 35 | copy({ 36 | targets: [ 37 | { src: outputModuleFileName, dest: 'vendor/assets/javascripts/', rename: outputVendorFileName } 38 | ], 39 | hook: 'writeBundle', 40 | verbose: true 41 | }) 42 | ] 43 | }) 44 | 45 | const esModuleConfig = (inputFileName, outputModuleFileName, formBuilderName) => ({ 46 | input: `src/${inputFileName}`, 47 | external: ['@client-side-validations/client-side-validations'], 48 | output: [ 49 | { 50 | file: outputModuleFileName, 51 | banner: banner(formBuilderName), 52 | format: 'es' 53 | } 54 | ], 55 | plugins: [ 56 | babel({ babelHelpers: 'bundled' }) 57 | ] 58 | }) 59 | 60 | export default [ 61 | umdModuleConfig('index.js', pkg.main, 'rails.validations.simple_form.js', 'Default'), 62 | umdModuleConfig('index.bootstrap4.js', 'dist/simple-form.bootstrap4.js', 'rails.validations.simple_form.bootstrap4.js', 'Bootstrap 4+'), 63 | esModuleConfig('index.js', pkg.module, 'Default'), 64 | esModuleConfig('index.bootstrap4.js', 'dist/simple-form.bootstrap4.esm.js', 'Default') 65 | ] 66 | -------------------------------------------------------------------------------- /src/index.bootstrap4.js: -------------------------------------------------------------------------------- 1 | import ClientSideValidations from '@client-side-validations/client-side-validations' 2 | import { addClass, removeClass } from './utils' 3 | 4 | ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { 5 | add: function ($element, settings, message) { 6 | this.wrapper(settings.wrapper).add.call(this, $element[0], settings, message) 7 | }, 8 | remove: function ($element, settings) { 9 | this.wrapper(settings.wrapper).remove.call(this, $element[0], settings) 10 | }, 11 | wrapper: function (name) { 12 | return this.wrappers[name] || this.wrappers.default 13 | }, 14 | 15 | wrappers: { 16 | default: { 17 | add (element, settings, message) { 18 | const wrapperElement = element.parentElement 19 | let errorElement = wrapperElement.querySelector(`${settings.error_tag}.invalid-feedback`) 20 | 21 | if (!errorElement) { 22 | const formTextElement = wrapperElement.querySelector('.form-text') 23 | 24 | errorElement = document.createElement(settings.error_tag) 25 | addClass(errorElement, 'invalid-feedback') 26 | errorElement.textContent = message 27 | 28 | if (formTextElement) { 29 | formTextElement.before(errorElement) 30 | } else { 31 | wrapperElement.appendChild(errorElement) 32 | } 33 | } 34 | 35 | addClass(wrapperElement, settings.wrapper_error_class) 36 | addClass(element, 'is-invalid') 37 | 38 | errorElement.textContent = message 39 | }, 40 | 41 | remove (element, settings) { 42 | const wrapperElement = element.parentElement 43 | const errorElement = wrapperElement.querySelector(`${settings.error_tag}.invalid-feedback`) 44 | 45 | removeClass(wrapperElement, settings.wrapper_error_class) 46 | removeClass(element, 'is-invalid') 47 | 48 | if (errorElement) { 49 | errorElement.remove() 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ClientSideValidations from '@client-side-validations/client-side-validations' 2 | import { addClass, removeClass } from './utils' 3 | 4 | ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = { 5 | add: function ($element, settings, message) { 6 | this.wrapper(settings.wrapper).add.call(this, $element[0], settings, message) 7 | }, 8 | remove: function ($element, settings) { 9 | this.wrapper(settings.wrapper).remove.call(this, $element[0], settings) 10 | }, 11 | wrapper: function (name) { 12 | return this.wrappers[name] || this.wrappers.default 13 | }, 14 | 15 | wrappers: { 16 | default: { 17 | add (element, settings, message) { 18 | const wrapperElement = element.closest(`${settings.wrapper_tag}.${settings.wrapper_class.replace(/ /g, '.')}`) 19 | let errorElement = wrapperElement.querySelector(`${settings.error_tag}.${settings.error_class.replace(/ /g, '.')}`) 20 | 21 | if (!errorElement) { 22 | errorElement = document.createElement(settings.error_tag) 23 | addClass(errorElement, settings.error_class) 24 | errorElement.textContent = message 25 | 26 | wrapperElement.appendChild(errorElement) 27 | } 28 | 29 | addClass(wrapperElement, settings.wrapper_error_class) 30 | 31 | errorElement.textContent = message 32 | }, 33 | 34 | remove (element, settings) { 35 | const wrapperElement = element.closest(`${settings.wrapper_tag}.${settings.wrapper_class.replace(/ /g, '.')}`) 36 | const errorElement = wrapperElement.querySelector(`${settings.error_tag}.${settings.error_class.replace(/ /g, '.')}`) 37 | 38 | removeClass(wrapperElement, settings.wrapper_error_class) 39 | 40 | if (errorElement) { 41 | errorElement.remove() 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const addClass = (element, customClass) => { 2 | if (customClass) { 3 | element.classList.add(...customClass.split(' ')) 4 | } 5 | } 6 | 7 | export const removeClass = (element, customClass) => { 8 | if (customClass) { 9 | element.classList.remove(...customClass.split(' ')) 10 | } 11 | } 12 | 13 | export default { 14 | addClass, 15 | removeClass 16 | } 17 | -------------------------------------------------------------------------------- /test/action_view/models.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_model' 4 | require 'client_side_validations/active_model' 5 | require 'action_view/models/post' 6 | require 'action_view/models/category' 7 | -------------------------------------------------------------------------------- /test/action_view/models/category.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Category 4 | extend ActiveModel::Naming 5 | extend ActiveModel::Translation 6 | include ActiveModel::Validations 7 | include ActiveModel::Conversion 8 | 9 | attr_reader :id, :title 10 | 11 | validates :title, presence: true 12 | 13 | def initialize(params = {}) 14 | params.each do |attr, value| 15 | public_send(:"#{attr}=", value) 16 | end 17 | end 18 | 19 | def persisted? 20 | false 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/action_view/models/comment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Comment 4 | extend ActiveModel::Naming 5 | extend ActiveModel::Translation 6 | include ActiveModel::Validations 7 | include ActiveModel::Conversion 8 | 9 | attr_reader :id, :post_id, :title, :body 10 | 11 | validates :title, :body, presence: true 12 | 13 | def initialize(params = {}) 14 | params.each do |attr, value| 15 | public_send(:"#{attr}=", value) 16 | end 17 | end 18 | 19 | def persisted? 20 | false 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/action_view/models/post.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Post 4 | extend ActiveModel::Naming 5 | extend ActiveModel::Translation 6 | include ActiveModel::Validations 7 | include ActiveModel::Conversion 8 | 9 | attr_accessor :title, :author_name, :body, :secret, :written_on, :cost, :comments, :comment_ids, :category 10 | 11 | validates :cost, presence: true 12 | 13 | def initialize(params = {}) 14 | params.each do |attr, value| 15 | public_send(:"#{attr}=", value) 16 | end 17 | end 18 | 19 | def persisted? 20 | false 21 | end 22 | 23 | def comments_attributes=(attributes); end 24 | 25 | def category_attributes=(attributes); end 26 | end 27 | -------------------------------------------------------------------------------- /test/action_view/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'base_helper' 4 | require 'action_view' 5 | require 'action_view/models' 6 | require 'client_side_validations/action_view' 7 | 8 | module ActionController 9 | class Base 10 | include ActionDispatch::Routing::RouteSet.new.url_helpers 11 | end 12 | end 13 | 14 | module ActionViewTestSetup 15 | include ::ClientSideValidations::ActionView::Helpers::FormHelper 16 | 17 | def form_for(*) 18 | @output_buffer = super 19 | end 20 | 21 | Routes = ActionDispatch::Routing::RouteSet.new 22 | include Routes.url_helpers 23 | def _routes 24 | Routes 25 | end 26 | 27 | Routes.draw do 28 | resources :posts 29 | end 30 | 31 | def default_url_options 32 | { only_path: true } 33 | end 34 | 35 | def url_for(object) 36 | @url_for_options = object 37 | if object.is_a?(Hash) && object[:use_route].blank? && object[:controller].blank? 38 | object[:controller] = 'main' 39 | object[:action] = 'index' 40 | end 41 | super 42 | end 43 | 44 | def setup 45 | super 46 | @post = Post.new 47 | 48 | if defined?(ActionView::OutputFlow) 49 | @view_flow = ActionView::OutputFlow.new 50 | else 51 | @_content_for = Hash.new { |h, k| h[k] = ActiveSupport::SafeBuffer.new } 52 | end 53 | end 54 | 55 | def posts_path(_options = {}) 56 | '/posts' 57 | end 58 | 59 | def post_path(post, options = {}) 60 | if options[:format] 61 | "/posts/#{post.id}.#{options[:format]}" 62 | else 63 | "/posts/#{post.id}" 64 | end 65 | end 66 | 67 | def protect_against_forgery? 68 | false 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/base_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Configure Rails Environment 4 | ENV['RAILS_ENV'] = 'test' 5 | 6 | require 'simplecov' 7 | 8 | SimpleCov.start 'rails' do 9 | if ENV['CI'] 10 | require 'simplecov-lcov' 11 | 12 | SimpleCov::Formatter::LcovFormatter.config do |c| 13 | c.report_with_single_file = true 14 | c.single_report_path = 'coverage/lcov.info' 15 | end 16 | 17 | formatter SimpleCov::Formatter::LcovFormatter 18 | end 19 | 20 | add_filter %w[version.rb] 21 | end 22 | 23 | require 'rubygems' 24 | require 'minitest/autorun' 25 | require 'byebug' 26 | require 'mocha/minitest' 27 | require 'rails' 28 | 29 | module TestApp 30 | class Application < Rails::Application 31 | config.try :load_defaults, "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" 32 | 33 | config.root = __dir__ 34 | config.active_support.deprecation = :log 35 | config.active_support.test_order = :random 36 | config.eager_load = false 37 | config.secret_key_base = '42' 38 | I18n.enforce_available_locales = true 39 | end 40 | end 41 | 42 | module ClientSideValidations; end 43 | -------------------------------------------------------------------------------- /test/generators/cases/test_generators.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'base_helper' 4 | 5 | require 'rails/generators/test_case' 6 | require 'client_side_validations/simple_form' 7 | require 'generators/client_side_validations/copy_assets_generator' 8 | require 'generators/client_side_validations/install_generator' 9 | 10 | class CopyAssetsGeneratorTest < Rails::Generators::TestCase 11 | tests ClientSideValidations::Generators::CopyAssetsGenerator 12 | destination File.expand_path('../tmp', __dir__) 13 | setup :prepare_destination 14 | 15 | test 'Assert file is properly created when no asset pipeline present' do 16 | stub_configuration 17 | run_generator 18 | 19 | assert_file 'public/javascripts/rails.validations.simple_form.js' 20 | assert_file 'public/javascripts/rails.validations.simple_form.bootstrap4.js' 21 | end 22 | 23 | private 24 | 25 | def stub_configuration 26 | Rails.stubs(:configuration).returns(mock('Configuration')) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/javascript/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path(__dir__) 4 | require 'server' 5 | run Sinatra::Application 6 | -------------------------------------------------------------------------------- /test/javascript/public/test/form_builders/validateSimpleForm.js: -------------------------------------------------------------------------------- 1 | QUnit.module('Validate SimpleForm', { 2 | before: function () { 3 | currentFormBuilder = window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] 4 | window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = DEFAULT_FORM_BUILDER 5 | }, 6 | 7 | after: function () { 8 | window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = currentFormBuilder 9 | }, 10 | 11 | beforeEach: function () { 12 | dataCsv = { 13 | html_settings: { 14 | type: 'SimpleForm::FormBuilder', 15 | error_class: 'error', 16 | error_tag: 'span', 17 | wrapper_error_class: 'field_with_errors', 18 | wrapper_tag: 'div', 19 | wrapper_class: 'input', 20 | wrapper: 'default' 21 | }, 22 | validators: { 23 | 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] } 24 | } 25 | } 26 | 27 | $('#qunit-fixture') 28 | .append($('
', { 29 | action: '/users', 30 | 'data-client-side-validations': JSON.stringify(dataCsv), 31 | method: 'post', 32 | id: 'new_user' 33 | })) 34 | .find('form') 35 | .append($('
')) 36 | .find('div') 37 | .append($('', { 38 | name: 'user[name]', 39 | id: 'user_name', 40 | type: 'text' 41 | })) 42 | .append($('')) 43 | $('form#new_user').validate() 44 | } 45 | }) 46 | 47 | QUnit.test('Validate error attaching and detaching', function (assert) { 48 | var form = $('form#new_user'); var input = form.find('input#user_name') 49 | var label = $('label[for="user_name"]') 50 | 51 | input.trigger('focusout') 52 | assert.ok(input.parent().hasClass('field_with_errors')) 53 | assert.ok(label.parent().hasClass('field_with_errors')) 54 | assert.ok(input.parent().find('span.error:contains("must be present")')[0]) 55 | 56 | input.val('abc') 57 | input.trigger('change') 58 | input.trigger('focusout') 59 | assert.ok(input.parent().hasClass('field_with_errors')) 60 | assert.ok(label.parent().hasClass('field_with_errors')) 61 | assert.ok(input.parent().find('span.error:contains("is invalid")')[0]) 62 | 63 | input.val('123') 64 | input.trigger('change') 65 | input.trigger('focusout') 66 | assert.notOk(input.parent().hasClass('field_with_errors')) 67 | assert.notOk(label.parent().hasClass('field_with_errors')) 68 | assert.notOk(form.find('span.error')[0]) 69 | }) 70 | 71 | QUnit.test('Validate pre-existing error blocks are re-used', function (assert) { 72 | var form = $('form#new_user'); var input = form.find('input#user_name') 73 | var label = $('label[for="user_name"]') 74 | 75 | input.parent().append($('Error from Server')) 76 | assert.ok(input.parent().find('span.error:contains("Error from Server")')[0]) 77 | input.val('abc') 78 | input.trigger('change') 79 | input.trigger('focusout') 80 | assert.ok(input.parent().hasClass('field_with_errors')) 81 | assert.ok(label.parent().hasClass('field_with_errors')) 82 | assert.ok(input.parent().find('span.error:contains("is invalid")').length === 1) 83 | assert.ok(form.find('span.error').length === 1) 84 | }) 85 | 86 | QUnit.test("Display error messages when wrapper and error tags have more than two css classes", function (assert) { 87 | dataCsv = { 88 | html_settings: { 89 | type: 'SimpleForm::FormBuilder', 90 | error_class: 'error error_class_one error_class_two', 91 | error_tag: 'span', 92 | wrapper_error_class: 'field_with_errors', 93 | wrapper_tag: 'div', 94 | wrapper_class: 'input wrapper_class_one wrapper_class_two', 95 | wrapper: 'default' 96 | }, 97 | validators: { 98 | 'user_2[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] } 99 | } 100 | } 101 | 102 | $('#qunit-fixture') 103 | .html('') 104 | .append($('', { 105 | action: '/users', 106 | 'data-client-side-validations': JSON.stringify(dataCsv), 107 | method: 'post', 108 | id: 'new_user_2' 109 | })) 110 | .find('form') 111 | .append($('
')) 112 | .find('div') 113 | .append($('', { 114 | name: 'user_2[name]', 115 | id: 'user_2_name', 116 | type: 'text' 117 | })) 118 | .append($('')) 119 | $('form#new_user_2').validate() 120 | 121 | var form = $('form#new_user_2') 122 | var input = form.find('input#user_2_name') 123 | 124 | input.val('') 125 | input.trigger('focusout') 126 | 127 | assert.ok(input.parent().hasClass('field_with_errors')) 128 | 129 | input.val('123') 130 | input.trigger('change') 131 | input.trigger('focusout') 132 | 133 | assert.notOk(input.parent().hasClass('field_with_errors')) 134 | assert.notOk(form.find('span.error').length) 135 | }) 136 | -------------------------------------------------------------------------------- /test/javascript/public/test/form_builders/validateSimpleFormBootstrap.js: -------------------------------------------------------------------------------- 1 | QUnit.module('Validate SimpleForm Bootstrap', { 2 | before: function () { 3 | currentFormBuilder = window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] 4 | window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = DEFAULT_FORM_BUILDER 5 | }, 6 | 7 | after: function () { 8 | window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = currentFormBuilder 9 | }, 10 | 11 | beforeEach: function () { 12 | dataCsv = { 13 | html_settings: { 14 | type: 'SimpleForm::FormBuilder', 15 | error_class: 'help-inline', 16 | error_tag: 'span', 17 | wrapper_error_class: 'error', 18 | wrapper_tag: 'div', 19 | wrapper_class: 'control-group control-group-2 control-group-3', 20 | wrapper: 'bootstrap' 21 | }, 22 | validators: { 23 | 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] }, 24 | 'user[username]': { presence: [{ message: 'must be present' }] } 25 | } 26 | } 27 | 28 | $('#qunit-fixture') 29 | .append($('', { 30 | action: '/users', 31 | 'data-client-side-validations': JSON.stringify(dataCsv), 32 | method: 'post', 33 | id: 'new_user' 34 | })) 35 | .find('form') 36 | .append($('
', { 37 | 'class': 'form-inputs' 38 | })) 39 | .find('div') 40 | .append($('
', { 41 | 'class': 'control-group control-group-2 control-group-3 control-group-user-name' 42 | })) 43 | .find('div.control-group-user-name') 44 | .append($('')) 45 | .append($('
', { 46 | 'class': 'controls' 47 | })) 48 | .find('div') 49 | .append($('', { 50 | name: 'user[name]', 51 | id: 'user_name', 52 | type: 'text' 53 | })) 54 | .append($('
', { 55 | 'class': 'control-group control-group-2 control-group-3 control-group-user-username' 56 | })) 57 | .find('div.control-group-user-username') 58 | .append($('')) 59 | .append($('
', { 60 | 'class': 'input-group' 61 | })) 62 | .find('div') 63 | .append($('', { 64 | 'class': 'input-group-addon', 65 | text: '@' 66 | })) 67 | .append($('', { 68 | name: 'user[username]', 69 | id: 'user_username', 70 | type: 'text' 71 | })) 72 | 73 | $('form#new_user').validate() 74 | } 75 | }) 76 | 77 | var wrappers = ['horizontal_form', 'vertical_form', 'inline_form'] 78 | 79 | for (var i = 0; i < wrappers.length; i++) { 80 | var wrapper = wrappers[i] 81 | 82 | QUnit.test(wrapper + ' - Validate error attaching and detaching', function (assert) { 83 | var form = $('form#new_user'); var input = form.find('input#user_name') 84 | var label = $('label[for="user_name"]') 85 | form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper 86 | 87 | input.trigger('focusout') 88 | assert.ok(input.parent().parent().hasClass('error')) 89 | assert.ok(label.parent().hasClass('error')) 90 | assert.ok(input.parent().parent().find('span.help-inline:contains("must be present")')[0]) 91 | 92 | input.val('abc') 93 | input.trigger('change') 94 | input.trigger('focusout') 95 | assert.ok(input.parent().parent().hasClass('error')) 96 | assert.ok(input.parent().parent().find('span.help-inline:contains("is invalid")')[0]) 97 | assert.ok(label.parent().hasClass('error')) 98 | 99 | input.val('123') 100 | input.trigger('change') 101 | input.trigger('focusout') 102 | assert.notOk(input.parent().parent().hasClass('error')) 103 | assert.notOk(input.parent().parent().find('span.help-inline')[0]) 104 | assert.notOk(label.parent().hasClass('error')) 105 | }) 106 | 107 | QUnit.test(wrapper + ' - Validate pre-existing error blocks are re-used', function (assert) { 108 | var form = $('form#new_user'); var input = form.find('input#user_name') 109 | var label = $('label[for="user_name"]') 110 | form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper 111 | 112 | input.parent().append($('Error from Server')) 113 | assert.ok(input.parent().find('span.help-inline:contains("Error from Server")')[0]) 114 | input.val('abc') 115 | input.trigger('change') 116 | input.trigger('focusout') 117 | assert.ok(input.parent().parent().hasClass('error')) 118 | assert.ok(label.parent().hasClass('error')) 119 | assert.ok(input.parent().find('span.help-inline:contains("is invalid")').length === 1) 120 | assert.ok(form.find('span.help-inline').length === 1) 121 | }) 122 | 123 | QUnit.test(wrapper + ' - Validate input-group', function (assert) { 124 | var form = $('form#new_user'); var input = form.find('input#user_username') 125 | form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper 126 | 127 | input.trigger('change') 128 | input.trigger('focusout') 129 | assert.ok(input.closest('.input-group').find('span.help-inline').length === 0) 130 | assert.ok(input.closest('.control-group').find('span.help-inline').length === 1) 131 | 132 | input.val('abc') 133 | input.trigger('change') 134 | input.trigger('focusout') 135 | assert.ok(input.closest('.control-group').find('span.help-inline').length === 0) 136 | }) 137 | } 138 | -------------------------------------------------------------------------------- /test/javascript/public/test/form_builders/validateSimpleFormBootstrap4.js: -------------------------------------------------------------------------------- 1 | QUnit.module('Validate SimpleForm Bootstrap 4', { 2 | before: function () { 3 | currentFormBuilder = window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] 4 | window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = BS4_FORM_BUILDER 5 | }, 6 | 7 | after: function () { 8 | window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = currentFormBuilder 9 | }, 10 | 11 | beforeEach: function () { 12 | dataCsv = { 13 | html_settings: { 14 | type: 'SimpleForm::FormBuilder', 15 | error_class: null, 16 | error_tag: 'div', 17 | wrapper_error_class: 'form-group-invalid', 18 | wrapper_tag: 'div', 19 | wrapper_class: 'form-group' 20 | }, 21 | validators: { 22 | 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] }, 23 | 'user[username]': { presence: [{ message: 'must be present' }] }, 24 | 'user[password]': { presence: [{ message: 'must be present' }] } 25 | } 26 | } 27 | 28 | $('#qunit-fixture') 29 | .append( 30 | $('', { 31 | action: '/users', 32 | 'data-client-side-validations': JSON.stringify(dataCsv), 33 | method: 'post', 34 | id: 'new_user' 35 | }) 36 | .append( 37 | $('
', { 'class': 'form-group' }) 38 | .append( 39 | $('')) 40 | .append( 41 | $('', { 'class': 'form-control', name: 'user[name]', id: 'user_name', type: 'text' }))) 42 | .append( 43 | $('
', { 'class': 'form-group' }) 44 | .append( 45 | $('')) 46 | .append( 47 | $('', { 'class': 'form-control', name: 'user[password]', id: 'user_password', type: 'password' })) 48 | .append( 49 | $('', { 'class': 'form-text text-muted', text: 'Minimum 8 characters' }))) 50 | .append( 51 | $('
', { 'class': 'form-group' }) 52 | .append( 53 | $('')) 54 | .append( 55 | $('
', { 'class': 'input-group' }) 56 | .append( 57 | $('
', { 'class': 'input-group-prepend' }) 58 | .append( 59 | $('', { 'class': 'input-group-text', text: '@' }))) 60 | .append( 61 | $('', { 'class': 'form-control', name: 'user[username]', id: 'user_username', type: 'text' }))))) 62 | 63 | $('form#new_user').validate() 64 | } 65 | }) 66 | 67 | var wrappers = ['horizontal_form', 'vertical_form', 'inline_form'] 68 | 69 | for (var i = 0; i < wrappers.length; i++) { 70 | var wrapper = wrappers[i] 71 | 72 | QUnit.test(wrapper + ' - Validate error attaching and detaching', function (assert) { 73 | var form = $('form#new_user') 74 | var input = form.find('input#user_name') 75 | var label = $('label[for="user_name"]') 76 | form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper 77 | 78 | input.trigger('focusout') 79 | assert.ok(input.parent().hasClass('form-group-invalid')) 80 | assert.ok(label.parent().hasClass('form-group-invalid')) 81 | assert.ok(input.parent().find('div.invalid-feedback:contains("must be present")')[0]) 82 | 83 | input.val('abc') 84 | input.trigger('change') 85 | input.trigger('focusout') 86 | assert.ok(input.parent().hasClass('form-group-invalid')) 87 | assert.ok(input.parent().find('div.invalid-feedback:contains("is invalid")')[0]) 88 | assert.ok(input.hasClass('is-invalid')) 89 | 90 | input.val('123') 91 | input.trigger('change') 92 | input.trigger('focusout') 93 | assert.notOk(input.parent().parent().hasClass('form-group-invalid')) 94 | assert.notOk(input.parent().parent().find('span.help-inline')[0]) 95 | assert.notOk(input.hasClass('is-invalid')) 96 | }) 97 | 98 | QUnit.test(wrapper + ' - Validate pre-existing error blocks are re-used', function (assert) { 99 | var form = $('form#new_user'); var input = form.find('input#user_name') 100 | var label = $('label[for="user_name"]') 101 | form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper 102 | 103 | input.parent().append($('
Error from Server')) 104 | assert.ok(input.parent().find('div.invalid-feedback:contains("Error from Server")')[0]) 105 | input.val('abc') 106 | input.trigger('change') 107 | input.trigger('focusout') 108 | assert.ok(input.parent().hasClass('form-group-invalid')) 109 | assert.ok(label.parent().hasClass('form-group-invalid')) 110 | assert.ok(input.parent().find('div.invalid-feedback:contains("is invalid")').length === 1) 111 | assert.ok(form.find('div.invalid-feedback').length === 1) 112 | }) 113 | 114 | QUnit.test(wrapper + ' - Validate input-group', function (assert) { 115 | var form = $('form#new_user'); var input = form.find('input#user_username') 116 | form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper 117 | 118 | input.trigger('change') 119 | input.trigger('focusout') 120 | assert.ok(input.closest('.input-group-prepend').find('div.invalid-feedback').length === 0) 121 | assert.ok(input.closest('.input-group').find('div.invalid-feedback').length === 1) 122 | 123 | input.val('abc') 124 | input.trigger('change') 125 | input.trigger('focusout') 126 | assert.ok(input.closest('.input-group').find('div.invalid-feedback').length === 0) 127 | }) 128 | 129 | QUnit.test(wrapper + ' - Inserts before form texts', function (assert) { 130 | var form = $('form#new_user') 131 | var input = form.find('input#user_password') 132 | form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper 133 | 134 | input.trigger('focusout') 135 | assert.ok(input.parent().find('.invalid-feedback:contains("must be present") + .form-text')[0]) 136 | }) 137 | } 138 | -------------------------------------------------------------------------------- /test/javascript/public/test/form_builders/validateSimpleFormBootstrap5.js: -------------------------------------------------------------------------------- 1 | QUnit.module('Validate SimpleForm Bootstrap 5', { 2 | before: function () { 3 | currentFormBuilder = window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] 4 | window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = BS4_FORM_BUILDER 5 | }, 6 | 7 | after: function () { 8 | window.ClientSideValidations.formBuilders['SimpleForm::FormBuilder'] = currentFormBuilder 9 | }, 10 | 11 | beforeEach: function () { 12 | dataCsv = { 13 | html_settings: { 14 | type: 'SimpleForm::FormBuilder', 15 | error_class: null, 16 | error_tag: 'div', 17 | wrapper_error_class: 'form-group-invalid', 18 | wrapper_tag: 'div', 19 | wrapper_class: 'mb-3' 20 | }, 21 | validators: { 22 | 'user[name]': { presence: [{ message: 'must be present' }], format: [{ message: 'is invalid', 'with': { options: 'g', source: '\\d+' } }] }, 23 | 'user[username]': { presence: [{ message: 'must be present' }] }, 24 | 'user[password]': { presence: [{ message: 'must be present' }] } 25 | } 26 | } 27 | 28 | $('#qunit-fixture') 29 | .append( 30 | $('', { 31 | action: '/users', 32 | 'data-client-side-validations': JSON.stringify(dataCsv), 33 | method: 'post', 34 | id: 'new_user' 35 | }) 36 | .append( 37 | $('
', { 'class': 'mb-3' }) 38 | .append( 39 | $('')) 40 | .append( 41 | $('', { 'class': 'form-control', name: 'user[name]', id: 'user_name', type: 'text' }))) 42 | .append( 43 | $('
', { 'class': 'mb-3' }) 44 | .append( 45 | $('')) 46 | .append( 47 | $('', { 'class': 'form-control', name: 'user[password]', id: 'user_password', type: 'password' })) 48 | .append( 49 | $('
', { 'class': 'form-text', text: 'Minimum 8 characters' }))) 50 | .append( 51 | $('
', { 'class': 'mb-3' }) 52 | .append( 53 | $('')) 54 | .append( 55 | $('
', { 'class': 'input-group' }) 56 | .append( 57 | $('', { 'class': 'input-group-text', text: '@' })) 58 | .append( 59 | $('', { 'class': 'form-control', name: 'user[username]', id: 'user_username', type: 'text' }))))) 60 | 61 | $('form#new_user').validate() 62 | } 63 | }) 64 | 65 | var wrappers = ['horizontal_form', 'vertical_form', 'inline_form'] 66 | 67 | for (var i = 0; i < wrappers.length; i++) { 68 | var wrapper = wrappers[i] 69 | 70 | QUnit.test(wrapper + ' - Validate error attaching and detaching', function (assert) { 71 | var form = $('form#new_user') 72 | var input = form.find('input#user_name') 73 | var label = $('label[for="user_name"]') 74 | form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper 75 | 76 | input.trigger('focusout') 77 | assert.ok(input.parent().hasClass('form-group-invalid')) 78 | assert.ok(label.parent().hasClass('form-group-invalid')) 79 | assert.ok(input.parent().find('div.invalid-feedback:contains("must be present")')[0]) 80 | 81 | input.val('abc') 82 | input.trigger('change') 83 | input.trigger('focusout') 84 | assert.ok(input.parent().hasClass('form-group-invalid')) 85 | assert.ok(input.parent().find('div.invalid-feedback:contains("is invalid")')[0]) 86 | assert.ok(input.hasClass('is-invalid')) 87 | 88 | input.val('123') 89 | input.trigger('change') 90 | input.trigger('focusout') 91 | assert.notOk(input.parent().parent().hasClass('form-group-invalid')) 92 | assert.notOk(input.parent().parent().find('span.help-inline')[0]) 93 | assert.notOk(input.hasClass('is-invalid')) 94 | }) 95 | 96 | QUnit.test(wrapper + ' - Validate pre-existing error blocks are re-used', function (assert) { 97 | var form = $('form#new_user'); var input = form.find('input#user_name') 98 | var label = $('label[for="user_name"]') 99 | form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper 100 | 101 | input.parent().append($('
Error from Server')) 102 | assert.ok(input.parent().find('div.invalid-feedback:contains("Error from Server")')[0]) 103 | input.val('abc') 104 | input.trigger('change') 105 | input.trigger('focusout') 106 | assert.ok(input.parent().hasClass('form-group-invalid')) 107 | assert.ok(label.parent().hasClass('form-group-invalid')) 108 | assert.ok(input.parent().find('div.invalid-feedback:contains("is invalid")').length === 1) 109 | assert.ok(form.find('div.invalid-feedback').length === 1) 110 | }) 111 | 112 | QUnit.test(wrapper + ' - Validate input-group', function (assert) { 113 | var form = $('form#new_user'); var input = form.find('input#user_username') 114 | form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper 115 | 116 | input.trigger('change') 117 | input.trigger('focusout') 118 | assert.ok(input.closest('.input-group-prepend').find('div.invalid-feedback').length === 0) 119 | assert.ok(input.closest('.input-group').find('div.invalid-feedback').length === 1) 120 | 121 | input.val('abc') 122 | input.trigger('change') 123 | input.trigger('focusout') 124 | assert.ok(input.closest('.input-group').find('div.invalid-feedback').length === 0) 125 | }) 126 | 127 | QUnit.test(wrapper + ' - Inserts before form texts', function (assert) { 128 | var form = $('form#new_user') 129 | var input = form.find('input#user_password') 130 | form[0].ClientSideValidations.settings.html_settings.wrapper = wrapper 131 | 132 | input.trigger('focusout') 133 | assert.ok(input.parent().find('.invalid-feedback:contains("must be present") + .form-text')[0]) 134 | }) 135 | } 136 | -------------------------------------------------------------------------------- /test/javascript/public/test/settings.js: -------------------------------------------------------------------------------- 1 | QUnit.config.autostart = window.location.search.search('autostart=false') < 0 2 | 3 | /* Hijacks normal form submit; lets it submit to an iframe to prevent 4 | * navigating away from the test suite 5 | */ 6 | $(document).on('submit', function (e) { 7 | if (!e.isDefaultPrevented()) { 8 | var form = $(e.target) 9 | var action = form.attr('action') 10 | var name = 'form-frame' + jQuery.guid++ 11 | var iframe = $('