├── .gitattributes
├── lib
├── client_side_validations
│ ├── version.rb
│ ├── engine.rb
│ ├── core_ext.rb
│ ├── active_model
│ │ ├── exclusion.rb
│ │ ├── inclusion.rb
│ │ ├── absence.rb
│ │ ├── presence.rb
│ │ ├── acceptance.rb
│ │ ├── format.rb
│ │ ├── length.rb
│ │ ├── conditionals.rb
│ │ └── numericality.rb
│ ├── core_ext
│ │ ├── range.rb
│ │ └── regexp.rb
│ ├── active_record.rb
│ ├── config.rb
│ ├── generators.rb
│ ├── files.rb
│ ├── generators
│ │ └── rails_validations.rb
│ ├── extender.rb
│ ├── action_view.rb
│ ├── active_record
│ │ └── uniqueness.rb
│ ├── action_view
│ │ ├── form_with_helper.rb
│ │ ├── form_helper.rb
│ │ └── form_builder.rb
│ └── active_model.rb
├── client_side_validations.rb
└── generators
│ ├── client_side_validations
│ ├── install_generator.rb
│ └── copy_assets_generator.rb
│ └── templates
│ └── client_side_validations
│ └── initializer.rb
├── .babelrc
├── test
├── javascript
│ ├── config.ru
│ ├── views
│ │ ├── layout.erb
│ │ └── index.erb
│ ├── public
│ │ └── test
│ │ │ ├── settings.js
│ │ │ ├── validators
│ │ │ ├── presence.js
│ │ │ ├── format.js
│ │ │ ├── exclusion.js
│ │ │ ├── inclusion.js
│ │ │ ├── confirmation.js
│ │ │ ├── absence.js
│ │ │ ├── acceptance.js
│ │ │ ├── length.js
│ │ │ └── uniqueness.js
│ │ │ ├── callbacks
│ │ │ ├── formAfter.js
│ │ │ ├── formBefore.js
│ │ │ ├── formPass.js
│ │ │ ├── formFail.js
│ │ │ ├── elementBefore.js
│ │ │ ├── elementAfter.js
│ │ │ ├── elementPass.js
│ │ │ └── elementFail.js
│ │ │ └── form_builders
│ │ │ └── validateForm.js
│ ├── server.rb
│ └── run-qunit.mjs
├── active_model
│ ├── cases
│ │ ├── helper.rb
│ │ ├── test_base.rb
│ │ ├── test_absence_validator.rb
│ │ ├── test_presence_validator.rb
│ │ ├── test_acceptance_validator.rb
│ │ ├── test_confirmation_validator.rb
│ │ ├── test_exclusion_validator.rb
│ │ ├── test_inclusion_validator.rb
│ │ ├── test_format_validator.rb
│ │ ├── test_length_validator.rb
│ │ └── test_numericality_validator.rb
│ └── models
│ │ └── person.rb
├── active_record
│ ├── models
│ │ ├── guid.rb
│ │ ├── thing.rb
│ │ └── user.rb
│ └── cases
│ │ ├── helper.rb
│ │ ├── test_base.rb
│ │ └── test_uniqueness_validator.rb
├── test_loader.rb
├── action_view
│ ├── models.rb
│ ├── models
│ │ ├── tag.rb
│ │ ├── category.rb
│ │ ├── comment.rb
│ │ ├── post.rb
│ │ └── format_thing.rb
│ └── cases
│ │ ├── test_legacy_form_for_helpers.rb
│ │ ├── test_legacy_form_with_helpers.rb
│ │ └── helper.rb
├── base_helper.rb
├── generators
│ └── cases
│ │ └── test_generators.rb
└── core_ext
│ └── cases
│ └── test_core_ext.rb
├── .gitignore
├── eslint.config.mjs
├── gemfiles
├── rails_7.1.gemfile
├── rails_7.2.gemfile
├── rails_8.0.gemfile
├── rails_6.1.gemfile
├── rails_7.0.gemfile
└── rails_edge.gemfile
├── Gemfile
├── src
├── validators
│ └── local
│ │ ├── confirmation.js
│ │ ├── absence_presence.js
│ │ ├── format.js
│ │ ├── acceptance.js
│ │ ├── length.js
│ │ ├── exclusion_inclusion.js
│ │ ├── uniqueness.js
│ │ └── numericality.js
├── utils.js
├── index.js
└── core.js
├── .github
├── dependabot.yml
├── workflows
│ ├── eslint.yml
│ ├── rubocop.yml
│ ├── javascript.yml
│ └── ruby.yml
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── Appraisals
├── LICENSE.md
├── CONTRIBUTING.md
├── client_side_validations.gemspec
├── rollup.config.mjs
├── .rubocop.yml
├── package.json
└── Rakefile
/.gitattributes:
--------------------------------------------------------------------------------
1 | dist/** linguist-generated
2 |
--------------------------------------------------------------------------------
/lib/client_side_validations/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | VERSION = '23.0.0.alpha1'
5 | end
6 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules": false
7 | }
8 | ]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/client_side_validations/engine.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | class Engine < ::Rails::Engine
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/client_side_validations/core_ext.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'active_support/json'
4 |
5 | require_relative 'core_ext/range'
6 | require_relative 'core_ext/regexp'
7 |
--------------------------------------------------------------------------------
/test/active_model/cases/helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'base_helper'
4 | require 'active_model'
5 | require 'client_side_validations/active_model'
6 | require 'active_model/models/person'
7 |
--------------------------------------------------------------------------------
/lib/client_side_validations/active_model/exclusion.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | module ActiveModel
5 | module Exclusion
6 | include EnumerableValidator
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/client_side_validations/active_model/inclusion.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | module ActiveModel
5 | module Inclusion
6 | include EnumerableValidator
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/active_record/models/guid.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | guids_table = %{CREATE TABLE guids (id INTEGER PRIMARY KEY, key text);}
4 | ActiveRecord::Base.connection.execute(guids_table)
5 |
6 | class Guid < ActiveRecord::Base
7 | end
8 |
--------------------------------------------------------------------------------
/lib/client_side_validations/core_ext/range.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Range
4 | def as_json(*)
5 | [first, last]
6 | end
7 |
8 | def to_json(options = nil)
9 | as_json(options).inspect
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/test_loader.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Sanity check to make sure the entire library loads OK
4 |
5 | require 'base_helper'
6 | require 'client_side_validations'
7 | require 'client_side_validations/files'
8 | require 'client_side_validations/version'
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.lock
3 | *.swp
4 |
5 | .*rc
6 | .bundle
7 | .byebug_history
8 |
9 | bundler_stubs/*
10 | bin/*
11 | binstubs/*
12 | coverage
13 | pkg/*
14 | tags
15 | test/generators/tmp/*
16 | test/log
17 |
18 | node_modules
19 |
20 | !.babelrc
21 |
22 | pnpm-lock.yaml
23 |
--------------------------------------------------------------------------------
/lib/client_side_validations/active_model/absence.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | module ActiveModel
5 | module Absence
6 | private
7 |
8 | def message_type
9 | :present
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/client_side_validations/active_model/presence.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | module ActiveModel
5 | module Presence
6 | private
7 |
8 | def message_type
9 | :blank
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/client_side_validations/active_model/acceptance.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | module ActiveModel
5 | module Acceptance
6 | private
7 |
8 | def message_type
9 | :accepted
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/client_side_validations/active_record.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'active_model'
4 |
5 | ActiveSupport.on_load(:active_record) { include ClientSideValidations::ActiveModel::Validations }
6 |
7 | ClientSideValidations::Extender.extend 'ActiveRecord', %w[Uniqueness]
8 |
--------------------------------------------------------------------------------
/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/category'
6 | require 'action_view/models/comment'
7 | require 'action_view/models/format_thing'
8 | require 'action_view/models/post'
9 |
--------------------------------------------------------------------------------
/test/active_record/cases/helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'base_helper'
4 | require 'active_record'
5 | require 'client_side_validations/active_record'
6 |
7 | require 'active_record/models/user'
8 | require 'active_record/models/guid'
9 | require 'active_record/models/thing'
10 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/client_side_validations/core_ext/regexp.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'js_regex'
4 |
5 | class Regexp
6 | def as_json(*)
7 | JsRegex.new(self).to_h
8 | end
9 |
10 | def to_json(options = nil)
11 | as_json(options)
12 | end
13 |
14 | def encode_json(_encoder)
15 | inspect
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/active_model/cases/test_base.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'active_model/cases/helper'
4 |
5 | module ClientSideValidations
6 | class ActiveModelTestBase < ::ActiveSupport::TestCase
7 | include ::ActiveModel::Validations
8 |
9 | def setup
10 | @person = Person.new
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/active_record/cases/test_base.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'active_record/cases/helper'
4 |
5 | module ClientSideValidations
6 | class ActiveRecordTestBase < ::ActiveSupport::TestCase
7 | include ::ActiveRecord::Validations
8 |
9 | def setup
10 | @user = User.new
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/active_record/models/thing.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | things_table = %{CREATE TABLE things (id INTEGER PRIMARY KEY, name text);}
4 | ActiveRecord::Base.connection.execute(things_table)
5 |
6 | class AbstractThing < ActiveRecord::Base
7 | self.abstract_class = true
8 | end
9 |
10 | class Thing < AbstractThing
11 | validates_uniqueness_of :name
12 | end
13 |
--------------------------------------------------------------------------------
/test/javascript/views/layout.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= @title %>
7 |
8 |
9 |
10 | <%= yield %>
11 |
12 |
13 |
--------------------------------------------------------------------------------
/lib/client_side_validations/config.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | module Config
5 | class << self
6 | attr_accessor :disabled_validators, :number_format_with_locale, :root_path
7 | end
8 |
9 | self.disabled_validators = []
10 | self.number_format_with_locale = false
11 | self.root_path = nil
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/client_side_validations/generators.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | module Generators
5 | @@assets = []
6 |
7 | def self.register_assets(klass)
8 | @@assets.concat(klass.assets)
9 | end
10 |
11 | def self.assets
12 | @@assets
13 | end
14 | end
15 | end
16 |
17 | require_relative 'generators/rails_validations'
18 |
--------------------------------------------------------------------------------
/lib/client_side_validations/files.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This is only used by dependent libraries that need to find the files
4 |
5 | module ClientSideValidations
6 | module Files
7 | Initializer = File.expand_path('../generators/templates/client_side_validations/initializer.rb', __dir__)
8 | Javascript = File.expand_path('../../vendor/assets/javascripts/rails.validations.js', __dir__)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/client_side_validations/generators/rails_validations.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | module Generators
5 | class RailsValidations
6 | def self.assets
7 | [{
8 | path: File.expand_path('../../../vendor/assets/javascripts', __dir__),
9 | file: 'rails.validations.js'
10 | }]
11 | end
12 |
13 | Generators.register_assets(self)
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/active_model/models/person.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PersonValidator < ActiveModel::Validator
4 | def validate(record); end
5 | end
6 |
7 | class Person
8 | include ActiveModel::Validations
9 |
10 | attr_accessor :first_name, :last_name, :email, :age
11 |
12 | validates_presence_of :first_name
13 | validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
14 |
15 | def new_record?
16 | true
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/action_view/models/tag.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Tag
4 | extend ActiveModel::Naming
5 | extend ActiveModel::Translation
6 | include ActiveModel::Validations
7 | include ActiveModel::Conversion
8 |
9 | attr_reader :id, :title, :description
10 |
11 | def initialize(params = {})
12 | params.each do |attr, value|
13 | public_send(:"#{attr}=", value)
14 | end
15 | end
16 |
17 | def persisted?
18 | false
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/javascript/views/index.erb:
--------------------------------------------------------------------------------
1 | <% @title = "client_side_validations test" %>
2 |
3 |
4 |
5 |
6 | <%= script_tag "https://code.jquery.com/jquery-#{jquery_version}.min.js" %>
7 | <%= script_tag '/vendor/assets/javascripts/rails.validations.js' %>
8 | <%= script_tag "https://code.jquery.com/qunit/qunit-#{qunit_version}.js" %>
9 | <%= script_tag 'settings' %>
10 | <%= script_tag 'validateElement' %>
11 | <%= test :validators, :form_builders, :callbacks %>
12 |
--------------------------------------------------------------------------------
/lib/client_side_validations/extender.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | module Extender
5 | module_function
6 |
7 | def extend(klass, validators)
8 | validators.each do |validator|
9 | require "client_side_validations/#{klass.underscore}/#{validator.downcase}"
10 |
11 | const_get(klass)::Validations.const_get(:"#{validator}Validator").include ClientSideValidations.const_get(klass).const_get(validator)
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/client_side_validations.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'client_side_validations/config'
4 | require_relative 'client_side_validations/active_model' if defined?(ActiveModel)
5 | require_relative 'client_side_validations/active_record' if defined?(ActiveRecord)
6 | require_relative 'client_side_validations/action_view' if defined?(ActionView)
7 |
8 | if defined?(Rails)
9 | require_relative 'client_side_validations/engine'
10 | require_relative 'client_side_validations/generators'
11 | end
12 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "byebug"
7 | gem "m"
8 | gem "minitest"
9 | gem "mocha"
10 | gem "rake"
11 | gem "rubocop"
12 | gem "rubocop-minitest"
13 | gem "rubocop-packaging"
14 | gem "rubocop-performance"
15 | gem "rubocop-rails"
16 | gem "rubocop-rake"
17 | gem "shotgun"
18 | gem "simplecov"
19 | gem "simplecov-lcov"
20 | gem "sinatra"
21 | gem "sqlite3"
22 | gem "webrick"
23 | gem "rails", "~> 7.1.0"
24 |
25 | gemspec path: "../"
26 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "byebug"
7 | gem "m"
8 | gem "minitest"
9 | gem "mocha"
10 | gem "rake"
11 | gem "rubocop"
12 | gem "rubocop-minitest"
13 | gem "rubocop-packaging"
14 | gem "rubocop-performance"
15 | gem "rubocop-rails"
16 | gem "rubocop-rake"
17 | gem "shotgun"
18 | gem "simplecov"
19 | gem "simplecov-lcov"
20 | gem "sinatra"
21 | gem "sqlite3"
22 | gem "webrick"
23 | gem "rails", "~> 7.2.0"
24 |
25 | gemspec path: "../"
26 |
--------------------------------------------------------------------------------
/gemfiles/rails_8.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "byebug"
7 | gem "m"
8 | gem "minitest"
9 | gem "mocha"
10 | gem "rake"
11 | gem "rubocop"
12 | gem "rubocop-minitest"
13 | gem "rubocop-packaging"
14 | gem "rubocop-performance"
15 | gem "rubocop-rails"
16 | gem "rubocop-rake"
17 | gem "shotgun"
18 | gem "simplecov"
19 | gem "simplecov-lcov"
20 | gem "sinatra"
21 | gem "sqlite3"
22 | gem "webrick"
23 | gem "rails", "~> 8.0.0"
24 |
25 | gemspec path: "../"
26 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | # Specify your gem's dependencies in client_side_validations.gemspec
6 | gemspec
7 |
8 | gem 'appraisal'
9 | gem 'byebug'
10 | gem 'm'
11 | gem 'minitest'
12 | gem 'mocha'
13 | gem 'rake'
14 | gem 'rubocop'
15 | gem 'rubocop-minitest'
16 | gem 'rubocop-packaging'
17 | gem 'rubocop-performance'
18 | gem 'rubocop-rails'
19 | gem 'rubocop-rake'
20 | gem 'shotgun'
21 | gem 'simplecov'
22 | gem 'simplecov-lcov'
23 | gem 'sinatra'
24 | gem 'sqlite3'
25 | gem 'webrick'
26 |
--------------------------------------------------------------------------------
/src/validators/local/confirmation.js:
--------------------------------------------------------------------------------
1 | export const confirmationLocalValidator = ($element, options) => {
2 | const element = $element[0]
3 | let value = element.value
4 | let confirmationValue = document.getElementById(`${element.id}_confirmation`).value
5 |
6 | if (!options.case_sensitive) {
7 | value = value.toLowerCase()
8 | confirmationValue = confirmationValue.toLowerCase()
9 | }
10 |
11 | if (value !== confirmationValue) {
12 | return options.message
13 | }
14 | }
15 |
16 | export default {
17 | confirmationLocalValidator
18 | }
19 |
--------------------------------------------------------------------------------
/.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 | ignore:
13 | - dependency-name: sqlite3
14 | versions: ">= 2" # FIXME: Remove when rails/rails#51636 will be released
15 | - package-ecosystem: npm
16 | directory: "/"
17 | schedule:
18 | interval: daily
19 | open-pull-requests-limit: 10
20 |
--------------------------------------------------------------------------------
/src/validators/local/absence_presence.js:
--------------------------------------------------------------------------------
1 | import { isValuePresent } from '../../utils'
2 |
3 | export const absenceLocalValidator = ($element, options) => {
4 | const element = $element[0]
5 |
6 | if (isValuePresent(element.value)) {
7 | return options.message
8 | }
9 | }
10 |
11 | export const presenceLocalValidator = ($element, options) => {
12 | const element = $element[0]
13 |
14 | if (!isValuePresent(element.value)) {
15 | return options.message
16 | }
17 | }
18 |
19 | export default {
20 | absenceLocalValidator,
21 | presenceLocalValidator
22 | }
23 |
--------------------------------------------------------------------------------
/gemfiles/rails_6.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "byebug"
7 | gem "m"
8 | gem "minitest"
9 | gem "mocha"
10 | gem "rake"
11 | gem "rubocop"
12 | gem "rubocop-minitest"
13 | gem "rubocop-packaging"
14 | gem "rubocop-performance"
15 | gem "rubocop-rails"
16 | gem "rubocop-rake"
17 | gem "shotgun"
18 | gem "simplecov"
19 | gem "simplecov-lcov"
20 | gem "sinatra"
21 | gem "sqlite3", "~> 1.7"
22 | gem "webrick"
23 | gem "rails", "~> 6.1.0"
24 | gem "concurrent-ruby", "< 1.3.5"
25 |
26 | gemspec path: "../"
27 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "byebug"
7 | gem "m"
8 | gem "minitest"
9 | gem "mocha"
10 | gem "rake"
11 | gem "rubocop"
12 | gem "rubocop-minitest"
13 | gem "rubocop-packaging"
14 | gem "rubocop-performance"
15 | gem "rubocop-rails"
16 | gem "rubocop-rake"
17 | gem "shotgun"
18 | gem "simplecov"
19 | gem "simplecov-lcov"
20 | gem "sinatra"
21 | gem "sqlite3", "~> 1.7"
22 | gem "webrick"
23 | gem "rails", "~> 7.0.0"
24 | gem "concurrent-ruby", "< 1.3.5"
25 |
26 | gemspec path: "../"
27 |
--------------------------------------------------------------------------------
/gemfiles/rails_edge.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "byebug"
7 | gem "m"
8 | gem "minitest"
9 | gem "mocha"
10 | gem "rake"
11 | gem "rubocop"
12 | gem "rubocop-minitest"
13 | gem "rubocop-packaging"
14 | gem "rubocop-performance"
15 | gem "rubocop-rails"
16 | gem "rubocop-rake"
17 | gem "shotgun"
18 | gem "simplecov"
19 | gem "simplecov-lcov"
20 | gem "sinatra"
21 | gem "sqlite3"
22 | gem "webrick"
23 | gem "rails", git: "https://github.com/rails/rails.git", branch: "main"
24 |
25 | gemspec path: "../"
26 |
--------------------------------------------------------------------------------
/.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@v6
18 | - uses: pnpm/action-setup@v4
19 | - name: Set up Node
20 | uses: actions/setup-node@v6
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 |
--------------------------------------------------------------------------------
/lib/client_side_validations/action_view.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | module ActionView
5 | module Helpers
6 | end
7 | end
8 | end
9 |
10 | require_relative 'core_ext'
11 | require_relative 'action_view/form_helper'
12 |
13 | if ActionView::Helpers::FormHelper.method_defined?(:form_with)
14 | require_relative 'action_view/form_with_helper'
15 | end
16 |
17 | require_relative 'action_view/form_builder'
18 |
19 | ActiveSupport.on_load(:action_view) { include ClientSideValidations::ActionView::Helpers::FormHelper }
20 | ActionView::Helpers::FormBuilder.prepend ClientSideValidations::ActionView::Helpers::FormBuilder
21 |
--------------------------------------------------------------------------------
/lib/generators/client_side_validations/install_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'copy_assets_generator'
4 |
5 | module ClientSideValidations
6 | module Generators
7 | class InstallGenerator < CopyAssetsGenerator
8 | def copy_initializer
9 | source_paths << File.expand_path('../templates/client_side_validations', __dir__)
10 | copy_file 'initializer.rb', 'config/initializers/client_side_validations.rb'
11 | end
12 |
13 | def self.installation_message
14 | "Copies initializer into config/initializers and #{super.downcase}"
15 | end
16 |
17 | desc installation_message
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | appraise 'rails-6.1' do
4 | gem 'rails', '~> 6.1.0'
5 | gem 'sqlite3', '~> 1.7'
6 | gem 'concurrent-ruby', '< 1.3.5' # Ref: rails/rails#54260
7 | end
8 |
9 | appraise 'rails-7.0' do
10 | gem 'rails', '~> 7.0.0'
11 | gem 'sqlite3', '~> 1.7'
12 | gem 'concurrent-ruby', '< 1.3.5' # Ref: rails/rails#54260
13 | end
14 |
15 | appraise 'rails-7.1' do
16 | gem 'rails', '~> 7.1.0'
17 | end
18 |
19 | appraise 'rails-7.2' do
20 | gem 'rails', '~> 7.2.0'
21 | end
22 |
23 | appraise 'rails-8.0' do
24 | gem 'rails', '~> 8.0.0'
25 | end
26 |
27 | appraise 'rails-edge' do
28 | gem 'rails', git: 'https://github.com/rails/rails.git', branch: 'main'
29 | end
30 |
--------------------------------------------------------------------------------
/.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@v6
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 |
--------------------------------------------------------------------------------
/test/active_model/cases/test_absence_validator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'active_model/cases/test_base'
4 |
5 | module ActiveModel
6 | class AbsenceValidatorTest < ClientSideValidations::ActiveModelTestBase
7 | def test_absence_client_side_hash
8 | expected_hash = { message: 'must be blank' }
9 |
10 | assert_equal expected_hash, AbsenceValidator.new(attributes: [:name]).client_side_hash(@person, :age)
11 | end
12 |
13 | def test_absence_client_side_hash_with_custom_message
14 | expected_hash = { message: 'is required to be blank' }
15 |
16 | assert_equal expected_hash, AbsenceValidator.new(attributes: [:name], message: 'is required to be blank').client_side_hash(@person, :age)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/active_model/cases/test_presence_validator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'active_model/cases/test_base'
4 |
5 | module ActiveModel
6 | class PresenceValidatorTest < ClientSideValidations::ActiveModelTestBase
7 | def test_presence_client_side_hash
8 | expected_hash = { message: I18n.t('errors.messages.blank') }
9 |
10 | assert_equal expected_hash, PresenceValidator.new(attributes: [:name]).client_side_hash(@person, :age)
11 | end
12 |
13 | def test_presence_client_side_hash_with_custom_message
14 | expected_hash = { message: 'is required' }
15 |
16 | assert_equal expected_hash, PresenceValidator.new(attributes: [:name], message: 'is required').client_side_hash(@person, :age)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/.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@v6
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@v6
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 |
--------------------------------------------------------------------------------
/src/validators/local/format.js:
--------------------------------------------------------------------------------
1 | import { isValuePresent } from '../../utils'
2 |
3 | const isMatching = (value, regExpOptions) => {
4 | return new RegExp(regExpOptions.source, regExpOptions.options).test(value)
5 | }
6 |
7 | const hasValidFormat = (value, withOptions, withoutOptions) => {
8 | return (withOptions && isMatching(value, withOptions)) || (withoutOptions && !isMatching(value, withoutOptions))
9 | }
10 |
11 | export const formatLocalValidator = ($element, options) => {
12 | const element = $element[0]
13 | const value = element.value
14 |
15 | if (options.allow_blank && !isValuePresent(value)) {
16 | return
17 | }
18 |
19 | if (!hasValidFormat(value, options.with, options.without)) {
20 | return options.message
21 | }
22 | }
23 |
24 | export default {
25 | formatLocalValidator
26 | }
27 |
--------------------------------------------------------------------------------
/test/active_model/cases/test_acceptance_validator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'active_model/cases/test_base'
4 |
5 | module ActiveModel
6 | class AcceptanceValidatorTest < ClientSideValidations::ActiveModelTestBase
7 | def test_acceptance_client_side_hash
8 | expected_hash = { message: 'must be accepted', accept: ['1', true] }
9 |
10 | assert_equal expected_hash, AcceptanceValidator.new(attributes: [:name], class: Person).client_side_hash(@person, :age)
11 | end
12 |
13 | def test_acceptance_client_side_hash_with_custom_message
14 | expected_hash = { message: 'you must accept', accept: ['1', true] }
15 |
16 | assert_equal expected_hash, AcceptanceValidator.new(attributes: [:name], class: Person, message: 'you must accept').client_side_hash(@person, :age)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/client_side_validations/active_model/format.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ClientSideValidations
4 | module ActiveModel
5 | module Format
6 | def client_side_hash(model, attribute, force = nil)
7 | options = self.options.dup
8 | if options[:with].respond_to?(:call)
9 | return unless force
10 |
11 | options[:with] = options[:with].call(model)
12 | build_client_side_hash(model, attribute, options)
13 | elsif options[:without].respond_to?(:call)
14 | return unless force
15 |
16 | options[:without] = options[:without].call(model)
17 | build_client_side_hash(model, attribute, options)
18 | else
19 | super
20 | end
21 | end
22 |
23 | private
24 |
25 | def message_type
26 | :invalid
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/src/validators/local/acceptance.js:
--------------------------------------------------------------------------------
1 | import { arrayHasValue } from '../../utils'
2 |
3 | const DEFAULT_ACCEPT_OPTION = ['1', true]
4 |
5 | const isTextAccepted = (value, acceptOption) => {
6 | if (!acceptOption) {
7 | acceptOption = DEFAULT_ACCEPT_OPTION
8 | }
9 |
10 | if (Array.isArray(acceptOption)) {
11 | return arrayHasValue(value, acceptOption)
12 | }
13 |
14 | return value === acceptOption
15 | }
16 |
17 | export const acceptanceLocalValidator = ($element, options) => {
18 | const element = $element[0]
19 | let valid = true
20 |
21 | if (element.type === 'checkbox') {
22 | valid = element.checked
23 | }
24 |
25 | if (element.type === 'text') {
26 | valid = isTextAccepted(element.value, options.accept)
27 | }
28 |
29 | if (!valid) {
30 | return options.message
31 | }
32 | }
33 |
34 | export default {
35 | acceptanceLocalValidator
36 | }
37 |
--------------------------------------------------------------------------------
/test/javascript/public/test/settings.js:
--------------------------------------------------------------------------------
1 | QUnit.config.autostart = window.location.search.search('autostart=false') < 0
2 |
3 | QUnit.config.urlConfig.push({
4 | id: 'jquery',
5 | label: 'jQuery version',
6 | value: ['3.7.1', '3.7.1.slim'],
7 | tooltip: 'What jQuery Core version to test against'
8 | })
9 |
10 | /* Hijacks normal form submit; lets it submit to an iframe to prevent
11 | * navigating away from the test suite
12 | */
13 | $(document).on('submit', function (e) {
14 | if (!e.isDefaultPrevented()) {
15 | var form = $(e.target)
16 | var action = form.attr('action')
17 | var name = 'form-frame' + jQuery.guid++
18 | var iframe = $('