├── .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 = $('