├── lib
├── form_props
│ ├── version.rb
│ ├── inputs
│ │ ├── tel_field.rb
│ │ ├── url_field.rb
│ │ ├── file_field.rb
│ │ ├── email_field.rb
│ │ ├── range_field.rb
│ │ ├── hidden_field.rb
│ │ ├── time_field.rb
│ │ ├── month_field.rb
│ │ ├── week_field.rb
│ │ ├── date_field.rb
│ │ ├── datetime_local_field.rb
│ │ ├── password_field.rb
│ │ ├── number_field.rb
│ │ ├── submit.rb
│ │ ├── color_field.rb
│ │ ├── search_field.rb
│ │ ├── text_field.rb
│ │ ├── datetime_field.rb
│ │ ├── text_area.rb
│ │ ├── time_zone_select.rb
│ │ ├── collection_radio_buttons.rb
│ │ ├── weekday_select.rb
│ │ ├── collection_check_boxes.rb
│ │ ├── collection_select.rb
│ │ ├── grouped_collection_select.rb
│ │ ├── radio_button.rb
│ │ ├── select.rb
│ │ ├── collection_helpers.rb
│ │ ├── check_box.rb
│ │ └── base.rb
│ ├── select_renderer.rb
│ ├── action_view_extensions
│ │ └── form_helper.rb
│ ├── form_options_helper.rb
│ └── form_builder.rb
└── form_props.rb
├── .gitignore
├── Gemfile.70
├── Gemfile.71
├── Gemfile.72
├── Gemfile.80
├── Gemfile.main
├── CODE_OF_CONDUCT.md
├── Rakefile
├── components
├── Extras.js
├── CheckBox.js
├── CollectionRadioButtons.js
├── CollectionCheckBoxes.js
└── Select.js
├── .github
└── workflows
│ ├── dynamic-security.yml
│ └── build.yml
├── package.json
├── CODEOWNERS
├── test
├── inputs
│ ├── tel_field_test.rb
│ ├── email_field_test.rb
│ ├── url_field_test.rb
│ ├── password_field_test.rb
│ ├── search_field_test.rb
│ ├── range_field_test.rb
│ ├── number_field_test.rb
│ ├── color_field_test.rb
│ ├── hidden_field_test.rb
│ ├── week_field_test.rb
│ ├── month_field_test.rb
│ ├── submit_test.rb
│ ├── radio_button_test.rb
│ ├── grouped_collection_select_test.rb
│ ├── weekday_select_test.rb
│ ├── time_field_test.rb
│ ├── input_options_test.rb
│ ├── file_field_test.rb
│ ├── date_field_test.rb
│ ├── datetime_field_test.rb
│ ├── collection_select_test.rb
│ ├── text_area_test.rb
│ ├── text_field_test.rb
│ ├── checkbox_field_test.rb
│ └── time_zone_select_test.rb
├── test_helper.rb
└── form_props_test.rb
├── SECURITY.md
├── form_props.gemspec
├── LICENSE.md
└── yarn.lock
/lib/form_props/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | VERSION = "0.2.2"
5 | end
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .ruby-version
2 | .byebug_history
3 | benchmark.rb
4 | performance/*/*.png
5 | *.gem
6 | .tool-versions
7 | node_modules
8 | build
9 |
--------------------------------------------------------------------------------
/Gemfile.70:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gem "mocha"
4 | gem "pry-byebug"
5 | gem "standard", ">= 1.0"
6 | gem "rails", "~> 7.0.0"
7 |
8 | gemspec
9 |
--------------------------------------------------------------------------------
/Gemfile.71:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gem "mocha"
4 | gem "pry-byebug"
5 | gem "standard", ">= 1.0"
6 | gem "rails", "~> 7.1.0"
7 | gem "uri", ">= 0.13.1"
8 |
9 | gemspec
10 |
--------------------------------------------------------------------------------
/Gemfile.72:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gem "mocha"
4 | gem "pry-byebug"
5 | gem "standard", ">= 1.0"
6 | gem "rails", "~> 7.2.0"
7 | gem "uri", ">= 0.13.1"
8 |
9 | gemspec
10 |
--------------------------------------------------------------------------------
/Gemfile.80:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gem "mocha"
4 | gem "pry-byebug"
5 | gem "standard", ">= 1.0"
6 | gem "rails", "~> 8.0.0"
7 | gem "uri", ">= 0.13.1"
8 |
9 | gemspec
10 |
--------------------------------------------------------------------------------
/Gemfile.main:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gem "mocha"
4 | gem "pry-byebug"
5 | gem "standard", ">= 1.0"
6 | gem "rails", git: 'https://github.com/rails/rails', ref: 'main'
7 |
8 | gemspec
9 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of conduct
2 |
3 | By participating in this project, you agree to abide by the
4 | [thoughtbot code of conduct][1].
5 |
6 | [1]: https://thoughtbot.com/open-source-code-of-conduct
7 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "minitest/test_task"
2 | require "standard/rake"
3 |
4 | Minitest::TestTask.create(:test) do |t|
5 | t.libs << "test"
6 | t.libs << "lib"
7 | t.test_globs = ["test/**/*_test.rb"]
8 | end
9 |
10 | task default: %i[test]
11 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/tel_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class TelField < TextField
6 | private
7 |
8 | def field_type
9 | "tel"
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/url_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class UrlField < TextField
6 | private
7 |
8 | def field_type
9 | "url"
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/file_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class FileField < TextField
6 | private
7 |
8 | def field_type
9 | "file"
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/email_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class EmailField < TextField
6 | private
7 |
8 | def field_type
9 | "email"
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/range_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class RangeField < NumberField
6 | private
7 |
8 | def field_type
9 | "range"
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/components/Extras.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export default (hiddenInputAttributes) => {
4 | const hiddenProps = Object.values(hiddenInputAttributes);
5 | const hiddenInputs = hiddenProps.map((props) => (
6 |
7 | ));
8 |
9 | return (
10 | <>{hiddenInputs}>
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/hidden_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class HiddenField < TextField
6 | def render
7 | @options[:auto_complete] = "off"
8 | super
9 | end
10 |
11 | def field_type
12 | "hidden"
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/time_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class TimeField < DatetimeField
6 | private
7 |
8 | def format_date(value)
9 | value&.strftime("%T.%L")
10 | end
11 |
12 | def field_type
13 | "time"
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/month_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class MonthField < DatetimeField
6 | private
7 |
8 | def format_date(value)
9 | value&.strftime("%Y-%m")
10 | end
11 |
12 | def field_type
13 | "month"
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/week_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class WeekField < DatetimeField
6 | private
7 |
8 | def format_date(value)
9 | value&.strftime("%Y-W%V")
10 | end
11 |
12 | def field_type
13 | "week"
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/date_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class DateField < DatetimeField
6 | private
7 |
8 | def format_date(value)
9 | value.presence&.strftime("%Y-%m-%d")
10 | end
11 |
12 | def field_type
13 | "date"
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/datetime_local_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class DatetimeLocalField < DatetimeField
6 | private
7 |
8 | def format_date(value)
9 | value&.strftime("%Y-%m-%dT%T")
10 | end
11 |
12 | def field_type
13 | "datetime-local"
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/password_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class PasswordField < TextField
6 | def render
7 | @options = {value: nil}.merge!(@options)
8 | super
9 | end
10 |
11 | private
12 |
13 | def field_type
14 | "password"
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/components/CheckBox.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export default ({includeHidden = true, name=null, uncheckedValue=null, children, ...rest}) => {
4 | return (
5 | <>
6 | {includeHidden && }
7 |
8 | {children}
9 |
10 | >
11 | )
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/.github/workflows/dynamic-security.yml:
--------------------------------------------------------------------------------
1 | name: update-security
2 |
3 | on:
4 | push:
5 | paths:
6 | - SECURITY.md
7 | branches:
8 | - main
9 | workflow_dispatch:
10 |
11 | jobs:
12 | update-security:
13 | permissions:
14 | contents: write
15 | pull-requests: write
16 | pages: write
17 | uses: thoughtbot/templates/.github/workflows/dynamic-security.yaml@main
18 | secrets:
19 | token: ${{ secrets.GITHUB_TOKEN }}
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "form_props",
3 | "version": "0.0.1",
4 | "main": "index.js",
5 | "repository": "git@github.com:thoughtbot/form_props.git",
6 | "scripts": {
7 | "build": "esbuild index.js --bundle --loader:.js=jsx --outfile=build/out.js"
8 | },
9 | "author": "Johny Ho ",
10 | "license": "MIT",
11 | "dependencies": {
12 | "esbuild": "^0.17.5",
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/number_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class NumberField < TextField
6 | def render
7 | if (range = @options.delete("in") || @options.delete("within"))
8 | @options.update("min" => range.min, "max" => range.max)
9 | end
10 |
11 | super
12 | end
13 |
14 | private
15 |
16 | def field_type
17 | "number"
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Lines starting with '#' are comments.
2 | # Each line is a file pattern followed by one or more owners.
3 |
4 | # More details are here: https://help.github.com/articles/about-codeowners/
5 |
6 | # The '*' pattern is global owners.
7 |
8 | # Order is important. The last matching pattern has the most precedence.
9 | # The folders are ordered as follows:
10 |
11 | # In each subsection folders are ordered first by depth, then alphabetically.
12 | # This should make it easy to add new rules without breaking existing ones.
13 |
14 | # Global rule:
15 | * @jho406
16 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/submit.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class Submit < Base
6 | def initialize(template_object, options)
7 | @template_object = template_object
8 | @options = options.with_indifferent_access
9 | end
10 |
11 | def render
12 | @options[:type] = field_type
13 |
14 | json.set!(:submit) do
15 | input_props(@options)
16 | end
17 | end
18 |
19 | private
20 |
21 | def field_type
22 | "submit"
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/color_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class ColorField < TextField
6 | def render
7 | @options["value"] ||= validate_color_string(value)
8 | super
9 | end
10 |
11 | private
12 |
13 | def validate_color_string(string)
14 | regex = /#[0-9a-fA-F]{6}/
15 | if regex.match?(string)
16 | string.downcase
17 | else
18 | "#000000"
19 | end
20 | end
21 |
22 | def field_type
23 | "color"
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/test/inputs/tel_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class TelFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_tel_field
9 | @post.title = "2125559090"
10 | form_props(model: @post) do |f|
11 | f.tel_field(:title)
12 | end
13 |
14 | result = json.result!.strip
15 | expected = {
16 | "type" => "tel",
17 | "defaultValue" => "2125559090",
18 | "name" => "post[title]",
19 | "id" => "post_title"
20 | }
21 |
22 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/inputs/email_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class EmailFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_url_field
9 | @post.title = "james@smith.com"
10 | form_props(model: @post) do |f|
11 | f.email_field(:title)
12 | end
13 |
14 | result = json.result!.strip
15 | expected = {
16 | "type" => "email",
17 | "defaultValue" => "james@smith.com",
18 | "name" => "post[title]",
19 | "id" => "post_title"
20 | }
21 |
22 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/inputs/url_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class UrlFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_url_field
9 | @post.title = "http://example.com"
10 | form_props(model: @post) do |f|
11 | f.url_field(:title)
12 | end
13 |
14 | result = json.result!.strip
15 | expected = {
16 | "type" => "url",
17 | "defaultValue" => "http://example.com",
18 | "name" => "post[title]",
19 | "id" => "post_title"
20 | }
21 |
22 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/search_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class SearchField < FormProps::Inputs::TextField
6 | def render
7 | if @options[:autosave]
8 | if @options[:autosave] == true
9 | @options[:autosave] = request.host.split(".").reverse.join(".")
10 | end
11 | @options[:results] ||= 10
12 | end
13 |
14 | if @options[:onsearch]
15 | @options[:incremental] = true unless @options.has_key?(:incremental)
16 | end
17 |
18 | super
19 | end
20 |
21 | private
22 |
23 | def field_type
24 | "search"
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 | # Security Policy
3 |
4 | ## Supported Versions
5 |
6 | Only the the latest version of this project is supported at a given time. If
7 | you find a security issue with an older version, please try updating to the
8 | latest version first.
9 |
10 | If for some reason you can't update to the latest version, please let us know
11 | your reasons so that we can have a better understanding of your situation.
12 |
13 | ## Reporting a Vulnerability
14 |
15 | For security inquiries or vulnerability reports, visit
16 | .
17 |
18 | If you have any suggestions to improve this policy, visit .
19 |
20 |
21 |
--------------------------------------------------------------------------------
/components/CollectionRadioButtons.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export default ({includeHidden = true, collection=[], ...rest}) => {
4 | if (collection.length == 0) {
5 | return null;
6 | }
7 |
8 | const checkboxes = collection.map((options) => {
9 | const { id } = options;
10 | const {label, ...inputOptions} = options;
11 |
12 | return (
13 | <>
14 |
15 |
16 | >
17 | )
18 | });
19 |
20 | const {name} = collection[0]
21 |
22 | return (
23 | <>
24 | {includeHidden && }
25 | {checkboxes}
26 | >
27 | )
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/components/CollectionCheckBoxes.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export default ({includeHidden = true, collection=[], ...rest}) => {
4 | if (collection.length == 0) {
5 | return null;
6 | }
7 |
8 | const checkboxes = collection.map((options) => {
9 | const { id } = options;
10 | const {uncheckedValue, label, ...inputOptions} = options;
11 |
12 | return (
13 | <>
14 |
15 |
16 | >
17 | )
18 | });
19 |
20 | const {name} = collection[0]
21 |
22 | return (
23 | <>
24 | {includeHidden && }
25 | {checkboxes}
26 | >
27 | )
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/text_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "action_view/helpers/tags/placeholderable"
4 |
5 | module FormProps
6 | module Inputs
7 | class TextField < Base
8 | include ActionView::Helpers::Tags::Placeholderable
9 |
10 | def render
11 | @options[:size] = @options[:max_length] unless @options.key?(:size)
12 | @options[:type] ||= field_type
13 | @options[:value] = @options.fetch(:value) { value_before_type_cast } unless field_type == "file"
14 |
15 | json.set!(sanitized_key) do
16 | add_default_name_and_field(@options)
17 | input_props(@options)
18 | end
19 | end
20 |
21 | private
22 |
23 | def field_type
24 | "text"
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/datetime_field.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class DatetimeField < TextField
6 | def render
7 | @options[:value] ||= format_date(value)
8 | @options[:min] = format_date(datetime_value(@options["min"]))
9 | @options[:max] = format_date(datetime_value(@options["max"]))
10 | super
11 | end
12 |
13 | private
14 |
15 | def format_date(value)
16 | raise NotImplementedError
17 | end
18 |
19 | def datetime_value(value)
20 | if value.is_a? String
21 | begin
22 | DateTime.parse(value)
23 | rescue
24 | nil
25 | end
26 | else
27 | value
28 | end
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/form_props.gemspec:
--------------------------------------------------------------------------------
1 | $LOAD_PATH << File.expand_path("lib", __dir__)
2 | require "form_props/version"
3 |
4 | Gem::Specification.new do |s|
5 | s.name = "form_props"
6 | s.version = FormProps::VERSION
7 | s.author = "Johny Ho"
8 | s.email = "johny@thoughtbot.com"
9 | s.license = "MIT"
10 | s.homepage = "https://github.com/thoughtbot/form_props/"
11 | s.summary = "Form props is a Rails form builder that renders form attributes in JSON"
12 | s.description = "Form props is a Rails form builder that renders form attributes in JSON"
13 | s.files = Dir["MIT-LICENSE", "README.md", "lib/**/*"]
14 |
15 | s.required_ruby_version = ">= 2.7"
16 |
17 | s.add_dependency "activesupport", ">= 7.0", "< 9.0"
18 | s.add_dependency "actionview", ">= 7.0", "< 9.0"
19 | s.add_dependency "props_template", ">= 0.30.0"
20 | end
21 |
--------------------------------------------------------------------------------
/components/Select.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export default ({includeHidden= true, name=null, id=null, children, options=[], multiple=false, disabled=false, type=null, ...rest}) => {
4 | const addHidden = includeHidden && multiple
5 |
6 | const optionElements = options.map((option) => {
7 | if (option.hasOwnProperty('options')) {
8 | return (
9 |
12 | )
13 | } else {
14 | return
15 | }
16 | });
17 |
18 | return (
19 | <>
20 | {addHidden && }
21 |
25 | >
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/test/inputs/password_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class PasswordFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_password_field
9 | @post.title = "does not show"
10 | form_props(model: @post) do |f|
11 | f.password_field(:title)
12 | end
13 |
14 | result = json.result!.strip
15 | expected = {
16 | "type" => "password",
17 | "name" => "post[title]",
18 | "id" => "post_title"
19 | }
20 |
21 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
22 |
23 | form_props(model: @post) do |f|
24 | f.password_field(:title, value: "shows")
25 | end
26 |
27 | result = json.result!.strip
28 | expected = {
29 | "type" => "password",
30 | "name" => "post[title]",
31 | "id" => "post_title",
32 | "defaultValue" => "shows"
33 | }
34 |
35 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/text_area.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "action_view/helpers/tags/placeholderable"
4 |
5 | module FormProps
6 | module Inputs
7 | class TextArea < Base
8 | include ActionView::Helpers::Tags::Placeholderable
9 |
10 | def render
11 | json.set!(sanitized_key) do
12 | add_default_name_and_field(@options)
13 | @options[:type] ||= field_type
14 | @options[:value] = @options.fetch(:value) { value_before_type_cast }
15 |
16 | if (size = @options.delete(:size))
17 | @options[:cols], @options[:rows] = size.split("x") if size.respond_to?(:split)
18 | end
19 |
20 | input_props(@options)
21 | end
22 | end
23 |
24 | class << self
25 | def field_type
26 | @field_type ||= name.split("::").last.sub("Field", "").downcase
27 | end
28 | end
29 |
30 | private
31 |
32 | def field_type
33 | self.class.field_type
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/time_zone_select.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class TimeZoneSelect < Base
6 | if ActionView::VERSION::STRING >= "7.1"
7 | include ActionView::Helpers::Tags::SelectRenderer
8 | end
9 | include ActionView::Helpers::FormOptionsHelper
10 | include FormOptionsHelper
11 | include SelectRenderer
12 |
13 | def initialize(object_name, method_name, template_object, priority_zones, options, html_options)
14 | @priority_zones = priority_zones
15 | @html_options = html_options
16 |
17 | super(object_name, method_name, template_object, options)
18 | end
19 |
20 | def render
21 | select_content_props(
22 | time_zone_options_for_select(value || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options
23 | )
24 | end
25 |
26 | private
27 |
28 | def field_type
29 | "select"
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | push:
4 | pull_request:
5 | schedule:
6 | - cron: '0 0 * * 0'
7 | workflow_dispatch:
8 |
9 | jobs:
10 | build:
11 | name: Ruby ${{ matrix.ruby }}. Rails ${{ matrix.version }}
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | ruby: ['3.3', '3.2', '3.1']
16 | version: ['70', '71', '72' , '80', 'main']
17 | exclude:
18 | - ruby: 3.1
19 | version: main
20 | - ruby: 3.1
21 | version: 80
22 |
23 | runs-on: 'ubuntu-latest'
24 |
25 | steps:
26 | - uses: actions/checkout@v3
27 | - uses: actions/setup-node@v3
28 | with:
29 | node-version: 18
30 | - uses: ruby/setup-ruby@v1
31 | with:
32 | ruby-version: ${{ matrix.ruby }}
33 | - name: Setup project
34 | run: |
35 | mv Gemfile.${{ matrix.version }} Gemfile
36 | npm install -g yarn
37 | yarn install
38 | bundle install
39 | bundle update
40 | - name: Run test
41 | run: bundle exec rake test
42 |
--------------------------------------------------------------------------------
/test/inputs/search_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class SearchFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_search_field
9 | form_props(model: @post) do |f|
10 | f.search_field(:title)
11 | end
12 | result = json.result!.strip
13 | expected = {
14 | "type" => "search",
15 | "defaultValue" => "Hello World",
16 | "name" => "post[title]",
17 | "id" => "post_title"
18 | }
19 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
20 | end
21 |
22 | def test_search_field_with_onsearch_value
23 | form_props(model: @post) do |f|
24 | f.search_field(:title, onsearch: true)
25 | end
26 | result = json.result!.strip
27 | expected = {
28 | "onsearch" => true,
29 | "incremental" => true,
30 | "type" => "search",
31 | "defaultValue" => "Hello World",
32 | "name" => "post[title]",
33 | "id" => "post_title"
34 | }
35 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/inputs/range_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class RangeFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_range_field
9 | @post.favs = 2
10 | form_props(model: @post) do |f|
11 | f.range_field(:favs, in: 1...10)
12 | end
13 |
14 | result = json.result!.strip
15 | expected = {
16 | "type" => "range",
17 | "defaultValue" => "2",
18 | "name" => "post[favs]",
19 | "min" => 1,
20 | "max" => 9,
21 | "id" => "post_favs"
22 | }
23 |
24 | assert_equal(JSON.parse(result)["inputs"]["favs"], expected)
25 |
26 | form_props(model: @post) do |f|
27 | f.range_field(:favs, size: 30, in: 1...10)
28 | end
29 |
30 | result = json.result!.strip
31 | expected = {
32 | "type" => "range",
33 | "defaultValue" => "2",
34 | "size" => 30,
35 | "name" => "post[favs]",
36 | "min" => 1,
37 | "max" => 9,
38 | "id" => "post_favs"
39 | }
40 |
41 | assert_equal(JSON.parse(result)["inputs"]["favs"], expected)
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/test/inputs/number_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class NumberFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_number_field
9 | @post.favs = 2
10 | form_props(model: @post) do |f|
11 | f.number_field(:favs, in: 1...10)
12 | end
13 |
14 | result = json.result!.strip
15 | expected = {
16 | "type" => "number",
17 | "defaultValue" => "2",
18 | "name" => "post[favs]",
19 | "min" => 1,
20 | "max" => 9,
21 | "id" => "post_favs"
22 | }
23 |
24 | assert_equal(JSON.parse(result)["inputs"]["favs"], expected)
25 |
26 | form_props(model: @post) do |f|
27 | f.number_field(:favs, size: 30, in: 1...10)
28 | end
29 |
30 | result = json.result!.strip
31 | expected = {
32 | "type" => "number",
33 | "defaultValue" => "2",
34 | "size" => 30,
35 | "name" => "post[favs]",
36 | "min" => 1,
37 | "max" => 9,
38 | "id" => "post_favs"
39 | }
40 |
41 | assert_equal(JSON.parse(result)["inputs"]["favs"], expected)
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/collection_radio_buttons.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "action_view/helpers/tags/collection_helpers"
4 |
5 | module FormProps
6 | module Inputs
7 | class CollectionRadioButtons < Base
8 | include ActionView::Helpers::Tags::CollectionHelpers
9 | include ActionView::Helpers::FormOptionsHelper
10 | include CollectionHelpers
11 |
12 | class RadioButtonBuilder < Builder
13 | def render(extra_html_options = {})
14 | html_options = extra_html_options.merge(@input_html_options)
15 | html_options[:skip_default_ids] = false
16 |
17 | checkbox = RadioButton.new(@object_name, @method_name, @template_object, @value, html_options)
18 | checkbox.render(true)
19 | checkbox.json.label @text
20 | end
21 | end
22 |
23 | def render
24 | json.set!(sanitized_key) do
25 | json.collection do
26 | render_collection_for(RadioButtonBuilder)
27 | end
28 |
29 | json.includeHidden(@options.fetch(:include_hidden) { true })
30 |
31 | input_props(@html_options)
32 | end
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 thoughtbot, inc.
4 | Copyright (c) David Heinemeier Hansson
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/weekday_select.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class WeekdaySelect < Base
6 | if ActionView::VERSION::STRING >= "7.1"
7 | include ActionView::Helpers::Tags::SelectRenderer
8 | end
9 | include ActionView::Helpers::FormOptionsHelper
10 | include FormOptionsHelper
11 | include SelectRenderer
12 |
13 | def initialize(object_name, method_name, template_object, options, html_options)
14 | @html_options = html_options
15 |
16 | super(object_name, method_name, template_object, options)
17 | end
18 |
19 | def render
20 | select_content_props(
21 | weekday_options_for_select(
22 | value || @options[:selected],
23 | index_as_value: @options.fetch(:index_as_value, false),
24 | day_format: @options.fetch(:day_format, :day_names),
25 | beginning_of_week: @options.fetch(:beginning_of_week, Date.beginning_of_week)
26 | ),
27 | @options,
28 | @html_options
29 | )
30 | end
31 |
32 | private
33 |
34 | def field_type
35 | "select"
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/collection_check_boxes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "action_view/helpers/tags/collection_helpers"
4 |
5 | module FormProps
6 | module Inputs
7 | class CollectionCheckBoxes < Base
8 | include ActionView::Helpers::Tags::CollectionHelpers
9 | include ActionView::Helpers::FormOptionsHelper
10 |
11 | include CollectionHelpers
12 |
13 | class CheckBoxBuilder < Builder
14 | def render(extra_html_options = {})
15 | html_options = extra_html_options.merge(@input_html_options)
16 | html_options[:multiple] = true
17 | html_options[:skip_default_ids] = false
18 |
19 | checkbox = CheckBox.new(@object_name, @method_name, @template_object, @value, nil, html_options)
20 | checkbox.render(true)
21 | checkbox.json.label @text
22 | end
23 | end
24 |
25 | def render
26 | json.set!(sanitized_key) do
27 | json.collection do
28 | render_collection_for(CheckBoxBuilder)
29 | end
30 |
31 | json.includeHidden(@options.fetch(:include_hidden) { true })
32 |
33 | input_props(@html_options)
34 | end
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/collection_select.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class CollectionSelect < Base
6 | if ActionView::VERSION::STRING >= "7.1"
7 | include ActionView::Helpers::Tags::SelectRenderer
8 | end
9 | include ActionView::Helpers::FormOptionsHelper
10 | include FormOptionsHelper
11 | include SelectRenderer
12 |
13 | def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
14 | @collection = collection
15 | @value_method = value_method
16 | @text_method = text_method
17 | @html_options = html_options
18 |
19 | super(object_name, method_name, template_object, options)
20 | end
21 |
22 | def render
23 | option_tags_options = {
24 | selected: @options.fetch(:selected) { value },
25 | disabled: @options[:disabled]
26 | }
27 |
28 | select_content_props(
29 | options_from_collection_for_select(@collection, @value_method, @text_method, option_tags_options),
30 | @options, @html_options
31 | )
32 | end
33 |
34 | private
35 |
36 | def field_type
37 | "select"
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/grouped_collection_select.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class GroupedCollectionSelect < Base
6 | if ActionView::VERSION::STRING >= "7.1"
7 | include ActionView::Helpers::Tags::SelectRenderer
8 | end
9 | include ActionView::Helpers::FormOptionsHelper
10 | include FormOptionsHelper
11 | include SelectRenderer
12 |
13 | def initialize(object_name, method_name, template_object, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
14 | @collection = collection
15 | @group_method = group_method
16 | @group_label_method = group_label_method
17 | @option_key_method = option_key_method
18 | @option_value_method = option_value_method
19 | @html_options = html_options
20 |
21 | super(object_name, method_name, template_object, options)
22 | end
23 |
24 | def render
25 | option_tags_options = {
26 | selected: @options.fetch(:selected) { value },
27 | disabled: @options[:disabled]
28 | }
29 |
30 | select_content_props(
31 | option_groups_from_collection_for_select(@collection, @group_method, @group_label_method, @option_key_method, @option_value_method, option_tags_options), @options, @html_options
32 | )
33 | end
34 |
35 | private
36 |
37 | def field_type
38 | "select"
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/radio_button.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class RadioButton < Base
6 | def initialize(object_name, method_name, template_object, tag_value, options)
7 | @tag_value = tag_value
8 | super(object_name, method_name, template_object, options)
9 | end
10 |
11 | def input_checked?(options)
12 | if options.has_key?(:checked)
13 | checked = options.delete(:checked)
14 | checked == true || checked == "checked"
15 | else
16 | checked?(value)
17 | end
18 | end
19 |
20 | def render(flatten = false)
21 | @options[:type] = field_type
22 | @options[:value] = @tag_value
23 | @options[:checked] = true if input_checked?(@options)
24 |
25 | body_block = -> {
26 | add_default_name_and_field_for_value(@tag_value, @options)
27 | input_props(@options)
28 | }
29 |
30 | if flatten
31 | body_block.call
32 | else
33 | json.set!(sanitized_key) do
34 | body_block.call
35 | end
36 | end
37 | end
38 |
39 | private
40 |
41 | def field_type
42 | "radio"
43 | end
44 |
45 |
46 | def sanitized_key
47 | @key || (sanitized_method_name + "_#{sanitized_value(@tag_value)}").camelize(:lower)
48 | end
49 |
50 | def checked?(value)
51 | value.to_s == @tag_value.to_s
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/select.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class Select < Base
6 | if ActionView::VERSION::STRING >= "7.1"
7 | include ActionView::Helpers::Tags::SelectRenderer
8 | end
9 | include ActionView::Helpers::FormOptionsHelper
10 | include FormOptionsHelper
11 | include SelectRenderer
12 |
13 | def initialize(object_name, method_name, template_object, choices, options, html_options)
14 | @choices = choices
15 | @choices = @choices.to_a if @choices.is_a?(Range)
16 |
17 | @html_options = html_options
18 | super(object_name, method_name, template_object, options)
19 | end
20 |
21 | def render
22 | option_tags_options = {
23 | selected: @options.fetch(:selected) { value.nil? ? "" : value },
24 | disabled: @options[:disabled]
25 | }
26 |
27 | option_tags = if grouped_choices?
28 | grouped_options_for_select(@choices, option_tags_options)
29 | else
30 | options_for_select(@choices, option_tags_options)
31 | end
32 |
33 | select_content_props(option_tags, @options, @html_options)
34 | end
35 |
36 | private
37 |
38 | def field_type
39 | "select"
40 | end
41 |
42 | # Grouped choices look like this:
43 | #
44 | # [nil, []]
45 | # { nil => [] }
46 | def grouped_choices?
47 | !@choices.blank? && @choices.first.respond_to?(:last) && Array === @choices.first.last
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/test/inputs/color_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class ColorFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_color_field_with_valid_hex_color_string
9 | @post.title = "#000fff"
10 | form_props(model: @post) do |f|
11 | f.color_field(:title)
12 | end
13 |
14 | result = json.result!.strip
15 | expected = {
16 | "type" => "color",
17 | "defaultValue" => "#000fff",
18 | "name" => "post[title]",
19 | "id" => "post_title"
20 | }
21 |
22 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
23 | end
24 |
25 | def test_color_field_with_invalid_hex_color_string
26 | @post.title = "#1234TR"
27 | form_props(model: @post) do |f|
28 | f.color_field(:title)
29 | end
30 |
31 | result = json.result!.strip
32 | expected = {
33 | "type" => "color",
34 | "defaultValue" => "#000000",
35 | "name" => "post[title]",
36 | "id" => "post_title"
37 | }
38 |
39 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
40 | end
41 |
42 | def test_color_field_with_value_attr
43 | @post.title = "#1234TR"
44 | form_props(model: @post) do |f|
45 | f.color_field(:title, value: "#00FF00")
46 | end
47 |
48 | result = json.result!.strip
49 | expected = {
50 | "type" => "color",
51 | "defaultValue" => "#00FF00",
52 | "name" => "post[title]",
53 | "id" => "post_title"
54 | }
55 |
56 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/form_props.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "action_view"
4 | require "action_pack"
5 | require "form_props/helper"
6 | require "form_props/action_view_extensions/form_helper"
7 | require "form_props/form_options_helper"
8 | require "form_props/select_renderer"
9 | require "form_props/inputs/base"
10 | require "form_props/inputs/text_field"
11 | require "form_props/inputs/text_area"
12 | require "form_props/inputs/check_box"
13 | require "form_props/inputs/select"
14 | require "form_props/inputs/collection_helpers"
15 | require "form_props/inputs/collection_select"
16 | require "form_props/inputs/grouped_collection_select"
17 | require "form_props/inputs/collection_check_boxes"
18 | require "form_props/inputs/collection_radio_buttons"
19 | require "form_props/inputs/search_field"
20 | require "form_props/inputs/radio_button"
21 | require "form_props/inputs/url_field"
22 | require "form_props/inputs/email_field"
23 | require "form_props/inputs/number_field"
24 | require "form_props/inputs/range_field"
25 | require "form_props/inputs/tel_field"
26 | require "form_props/inputs/color_field"
27 | require "form_props/inputs/password_field"
28 | require "form_props/inputs/datetime_field"
29 | require "form_props/inputs/datetime_local_field"
30 | require "form_props/inputs/date_field"
31 | require "form_props/inputs/time_field"
32 | require "form_props/inputs/file_field"
33 | require "form_props/inputs/week_field"
34 | require "form_props/inputs/month_field"
35 | require "form_props/inputs/hidden_field"
36 | require "form_props/inputs/time_zone_select"
37 | require "form_props/inputs/weekday_select"
38 | require "form_props/inputs/submit"
39 | require "form_props/form_builder"
40 |
41 | module FormProps
42 | extend ActiveSupport::Autoload
43 |
44 | def self.set(json, template)
45 | template.instance_variable_set(:@__json, json)
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/collection_helpers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | module CollectionHelpers
6 | def render_collection
7 | json.array! @collection do |item|
8 | value = value_for_collection(item, @value_method)
9 | text = value_for_collection(item, @text_method)
10 | default_html_options = default_html_options_for_collection(item, value)
11 | additional_html_options = option_html_attributes(item)
12 |
13 | yield item, value, text, default_html_options.merge(additional_html_options)
14 | end
15 | end
16 |
17 | def default_html_options_for_collection(item, value)
18 | html_options = @html_options.dup
19 |
20 | [:checked, :selected, :disabled, :read_only].each do |option|
21 | current_value = @options[option]
22 | next if current_value.nil?
23 |
24 | accept = if current_value.respond_to?(:call)
25 | current_value.call(item)
26 | else
27 | Array(current_value).map(&:to_s).include?(value.to_s)
28 | end
29 |
30 | if accept
31 | html_options[option] = true
32 | elsif option == :checked
33 | html_options[option] = false
34 | end
35 | end
36 |
37 | html_options[:object] = @object
38 | html_options
39 | end
40 |
41 | def render_collection_for(builder_class, &block)
42 | render_collection do |item, value, text, default_html_options|
43 | builder = instantiate_builder(builder_class, item, value, text, default_html_options)
44 | builder.render
45 | end
46 | end
47 |
48 | def hidden_field_name
49 | @html_options[:name] || tag_name(false, @options[:index]).to_s
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/form_props/select_renderer.rb:
--------------------------------------------------------------------------------
1 | module FormProps
2 | module SelectRenderer
3 | def add_options(option_tags, options, value = nil)
4 | if options[:include_blank]
5 | content = (options[:include_blank] if options[:include_blank].is_a?(String))
6 | label = (" " unless content)
7 | option_tags = [{label: content || label, value: ""}] + option_tags
8 | end
9 |
10 | if value.blank? && options[:prompt]
11 | tag_options = {value: ""}.tap do |prompt_opts|
12 | prompt_opts[:disabled] = true if options[:disabled] == ""
13 | if options[:selected] == ""
14 | selected_values.push("")
15 | end
16 | prompt_opts[:label] = prompt_text(options[:prompt])
17 | end
18 | option_tags = [tag_options] + option_tags
19 | end
20 |
21 | option_tags
22 | end
23 |
24 | def select_content_props(option_tags, options, html_options)
25 | html_options = html_options.stringify_keys
26 | add_default_name_and_field(html_options)
27 |
28 | if placeholder_required?(html_options)
29 | raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
30 | options[:include_blank] ||= true unless options[:prompt]
31 | end
32 |
33 | html_options["type"] = field_type
34 | value_for_blank = options.fetch(:selected) { value }
35 | option_tags = add_options(option_tags, options, value_for_blank)
36 |
37 | if options[:multiple]
38 | html_options["multiple"] = options[:multiple]
39 | end
40 |
41 | if selected_values.any?
42 | html_options["value"] ||= if html_options["multiple"]
43 | Array(selected_values)
44 | else
45 | selected_values.first
46 | end
47 | end
48 |
49 | json.set!(sanitized_key) do
50 | input_props(html_options)
51 |
52 | if options.key?(:include_hidden)
53 | json.includeHidden options[:include_hidden]
54 | end
55 | json.options(option_tags)
56 | end
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/check_box.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class CheckBox < Base
6 | def initialize(object_name, method_name, template_object, checked_value, unchecked_value, options)
7 | @checked_value = checked_value
8 | @unchecked_value = unchecked_value
9 | super(object_name, method_name, template_object, options)
10 | end
11 |
12 | def input_checked?(options)
13 | if options.has_key?(:checked)
14 | checked = options.delete(:checked)
15 | checked == true || checked == "checked"
16 | else
17 | checked?(value)
18 | end
19 | end
20 |
21 | def render(flatten = false)
22 | options = @options.stringify_keys
23 | options[:type] = field_type
24 | options[:value] = @checked_value
25 | options[:checked] = input_checked?(options)
26 | options[:unchecked_value] = @unchecked_value || ""
27 | options[:include_hidden] = options.fetch(:include_hidden) { true }
28 |
29 | body_block = -> {
30 | if options[:multiple]
31 | add_default_name_and_field_for_value(@checked_value, options)
32 | options.delete(:multiple)
33 | else
34 | add_default_name_and_field(options)
35 | end
36 |
37 | input_props(options)
38 | }
39 |
40 | if flatten
41 | body_block.call
42 | else
43 | json.set!(sanitized_key) do
44 | body_block.call
45 | end
46 | end
47 | end
48 |
49 | private
50 |
51 | def field_type
52 | "checkbox"
53 | end
54 |
55 | def checked?(value)
56 | case value
57 | when TrueClass, FalseClass
58 | value == !!@checked_value
59 | when NilClass
60 | false
61 | when String
62 | value == @checked_value
63 | else
64 | if value.respond_to?(:include?)
65 | value.include?(@checked_value)
66 | else
67 | value.to_i == @checked_value.to_i
68 | end
69 | end
70 | end
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/test/inputs/hidden_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class HiddenFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_hidden_field
9 | form_props(model: @post) do |f|
10 | f.hidden_field(:title)
11 | end
12 |
13 | result = json.result!.strip
14 | expected = {
15 | "type" => "hidden",
16 | "defaultValue" => "Hello World",
17 | "name" => "post[title]",
18 | "id" => "post_title",
19 | "autoComplete" => "off"
20 | }
21 |
22 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
23 |
24 | form_props(model: @post) do |f|
25 | f.hidden_field(:secret?)
26 | end
27 |
28 | result = json.result!.strip
29 | expected = {
30 | "type" => "hidden",
31 | "defaultValue" => "1",
32 | "name" => "post[secret]",
33 | "id" => "post_secret",
34 | "autoComplete" => "off"
35 | }
36 |
37 | assert_equal(JSON.parse(result)["inputs"]["secret"], expected)
38 | end
39 |
40 | def test_hidden_field_with_nil_value
41 | @post.title = nil
42 | form_props(model: @post) do |f|
43 | f.hidden_field(:title)
44 | end
45 |
46 | result = json.result!.strip
47 | expected = {
48 | "type" => "hidden",
49 | "name" => "post[title]",
50 | "id" => "post_title",
51 | "autoComplete" => "off"
52 | }
53 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
54 | end
55 |
56 | def test_hidden_field_with_options
57 | assert_dom_equal(
58 | '',
59 | hidden_field("post", "title", value: "Something Else")
60 | )
61 | @post.title = nil
62 | form_props(model: @post) do |f|
63 | f.hidden_field(:title, value: "Something Else")
64 | end
65 |
66 | result = json.result!.strip
67 | expected = {
68 | "type" => "hidden",
69 | "name" => "post[title]",
70 | "defaultValue" => "Something Else",
71 | "id" => "post_title",
72 | "autoComplete" => "off"
73 | }
74 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/test/inputs/week_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class WeekFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_week_field
9 | form_props(model: @post) do |f|
10 | f.week_field(:written_on)
11 | end
12 |
13 | result = json.result!.strip
14 | expected = {
15 | "type" => "week",
16 | "name" => "post[written_on]",
17 | "id" => "post_written_on",
18 | "defaultValue" => "2004-W25"
19 | }
20 |
21 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
22 | end
23 |
24 | def test_week_field_with_nil_value
25 | @post.written_on = nil
26 |
27 | form_props(model: @post) do |f|
28 | f.week_field(:written_on)
29 | end
30 |
31 | result = json.result!.strip
32 | expected = {
33 | "type" => "week",
34 | "name" => "post[written_on]",
35 | "id" => "post_written_on"
36 | }
37 |
38 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
39 | end
40 |
41 | def test_week_field_with_extra_attrs
42 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
43 | min_value = DateTime.new(2000, 2, 13)
44 | max_value = DateTime.new(2010, 12, 23)
45 | step = 2
46 |
47 | form_props(model: @post) do |f|
48 | f.week_field(:written_on, min: min_value, max: max_value, step: step)
49 | end
50 |
51 | result = json.result!.strip
52 | expected = {
53 | "type" => "week",
54 | "defaultValue" => "2004-W25",
55 | "step" => 2,
56 | "min" => "2000-W06",
57 | "max" => "2010-W51",
58 | "name" => "post[written_on]",
59 | "id" => "post_written_on"
60 | }
61 |
62 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
63 | end
64 |
65 | def test_week_field_with_timewithzone_value
66 | previous_week_zone, Time.zone = Time.zone, "UTC"
67 | @post.written_on = Time.zone.parse("2004-06-15 15:30:45")
68 |
69 | form_props(model: @post) do |f|
70 | f.week_field(:written_on)
71 | end
72 |
73 | result = json.result!.strip
74 | expected = {
75 | "type" => "week",
76 | "defaultValue" => "2004-W25",
77 | "name" => "post[written_on]",
78 | "id" => "post_written_on"
79 | }
80 |
81 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
82 | ensure
83 | Time.zone = previous_week_zone
84 | end
85 |
86 | def test_week_field_week_number_base
87 | @post.written_on = DateTime.new(2015, 1, 1, 1, 2, 3)
88 |
89 | form_props(model: @post) do |f|
90 | f.week_field(:written_on)
91 | end
92 |
93 | result = json.result!.strip
94 | expected = {
95 | "type" => "week",
96 | "defaultValue" => "2015-W01",
97 | "name" => "post[written_on]",
98 | "id" => "post_written_on"
99 | }
100 |
101 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/test/inputs/month_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class MonthFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_month_field
9 | form_props(model: @post) do |f|
10 | f.month_field(:written_on)
11 | end
12 |
13 | result = json.result!.strip
14 | expected = {
15 | "type" => "month",
16 | "name" => "post[written_on]",
17 | "id" => "post_written_on",
18 | "defaultValue" => "2004-06"
19 | }
20 |
21 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
22 | end
23 |
24 | def test_month_field_with_nil_value
25 | @post.written_on = nil
26 |
27 | form_props(model: @post) do |f|
28 | f.month_field(:written_on)
29 | end
30 |
31 | result = json.result!.strip
32 | expected = {
33 | "type" => "month",
34 | "name" => "post[written_on]",
35 | "id" => "post_written_on"
36 | }
37 |
38 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
39 | end
40 |
41 | def test_month_field_with_datetime_value
42 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
43 |
44 | form_props(model: @post) do |f|
45 | f.month_field(:written_on)
46 | end
47 |
48 | result = json.result!.strip
49 | expected = {
50 | "type" => "month",
51 | "defaultValue" => "2004-06",
52 | "name" => "post[written_on]",
53 | "id" => "post_written_on"
54 | }
55 |
56 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
57 | end
58 |
59 | def test_month_field_with_extra_attrs
60 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
61 | min_value = DateTime.new(2000, 2, 13)
62 | max_value = DateTime.new(2010, 12, 23)
63 | step = 2
64 |
65 | form_props(model: @post) do |f|
66 | f.month_field(:written_on, min: min_value, max: max_value, step: step)
67 | end
68 |
69 | result = json.result!.strip
70 | expected = {
71 | "type" => "month",
72 | "defaultValue" => "2004-06",
73 | "step" => 2,
74 | "max" => "2010-12",
75 | "min" => "2000-02",
76 | "name" => "post[written_on]",
77 | "id" => "post_written_on"
78 | }
79 |
80 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
81 | end
82 |
83 | def test_month_field_with_timewithzone_value
84 | previous_month_zone, Time.zone = Time.zone, "UTC"
85 | @post.written_on = Time.zone.parse("2004-06-15 15:30:45")
86 |
87 | form_props(model: @post) do |f|
88 | f.month_field(:written_on)
89 | end
90 |
91 | result = json.result!.strip
92 | expected = {
93 | "type" => "month",
94 | "defaultValue" => "2004-06",
95 | "name" => "post[written_on]",
96 | "id" => "post_written_on"
97 | }
98 |
99 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
100 | ensure
101 | Time.zone = previous_month_zone
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/test/inputs/submit_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class SubmitTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_submit_with_object_as_new_record_and_locale_strings
9 | I18n.with_locale :submit do
10 | @post.persisted = false
11 | @post.stub(:to_key, nil) do
12 | form_props(model: @post) do |f|
13 | f.submit
14 | end
15 |
16 | result = json.result!.strip
17 |
18 | expected = {
19 | "type" => "submit",
20 | "text" => "Create Post",
21 | "name" => "commit"
22 | }
23 |
24 | assert_equal(JSON.parse(result)["inputs"]["submit"], expected)
25 | end
26 | end
27 | end
28 |
29 | def test_submit_with_object_as_existing_record_and_locale_strings
30 | I18n.with_locale :submit do
31 | form_props(model: @post, method: "patch") do |f|
32 | f.submit
33 | end
34 |
35 | result = json.result!.strip
36 |
37 | expected = {
38 | "type" => "submit",
39 | "text" => "Confirm Post changes",
40 | "name" => "commit"
41 | }
42 |
43 | assert_equal(JSON.parse(result)["inputs"]["submit"], expected)
44 | assert_equal(JSON.parse(result)["extras"]["method"]["defaultValue"], "patch")
45 | end
46 | end
47 |
48 | def test_submit_without_object_and_locale_strings
49 | I18n.with_locale :submit do
50 | form_props(scope: :post) do |f|
51 | f.submit
52 | end
53 |
54 | result = json.result!.strip
55 |
56 | expected = {
57 | "type" => "submit",
58 | "text" => "Save changes",
59 | "name" => "commit"
60 | }
61 |
62 | assert_equal(JSON.parse(result)["inputs"]["submit"], expected)
63 | end
64 | end
65 |
66 | def test_submit_with_object_which_is_overwritten_by_scope_option
67 | I18n.with_locale :submit do
68 | form_props(model: @post, scope: :another_post) do |f|
69 | f.submit
70 | end
71 | result = json.result!.strip
72 |
73 | expected = {
74 | "type" => "submit",
75 | "text" => "Update your Post",
76 | "name" => "commit"
77 | }
78 |
79 | assert_equal(JSON.parse(result)["extras"]["method"]["defaultValue"], "patch")
80 | assert_equal(JSON.parse(result)["inputs"]["submit"], expected)
81 | end
82 | end
83 |
84 | def test_submit_with_object_which_is_namespaced
85 | blog_post = Blog::Post.new("And his name will be forty and four.", 44)
86 | I18n.with_locale :submit do
87 | form_props(model: blog_post) do |f|
88 | f.submit
89 | end
90 |
91 | result = json.result!.strip
92 |
93 | expected = {
94 | "type" => "submit",
95 | "text" => "Update your Post",
96 | "name" => "commit"
97 | }
98 |
99 | assert_equal(JSON.parse(result)["extras"]["method"]["defaultValue"], "patch")
100 | assert_equal(JSON.parse(result)["inputs"]["submit"], expected)
101 | end
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/test/inputs/radio_button_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class RadioButtonTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_radio_button
9 | @post.title = "Hello World"
10 | form_props(model: @post) do |f|
11 | f.radio_button(:title, "Hello World")
12 | end
13 | result = json.result!.strip
14 | expected = {
15 | "type" => "radio",
16 | "value" => "Hello World",
17 | "defaultChecked" => true,
18 | "name" => "post[title]",
19 | "id" => "post_title_hello_world"
20 | }
21 | assert_equal(JSON.parse(result)["inputs"]["titleHelloWorld"], expected)
22 |
23 | form_props(model: @post) do |f|
24 | f.radio_button(:title, "Goodbye World")
25 | end
26 | result = json.result!.strip
27 | expected = {
28 | "type" => "radio",
29 | "value" => "Goodbye World",
30 | "name" => "post[title]",
31 | "id" => "post_title_goodbye_world"
32 | }
33 | assert_equal(JSON.parse(result)["inputs"]["titleGoodbyeWorld"], expected)
34 | end
35 |
36 | def test_radio_button_is_checked_with_integers
37 | @post.admin = 1
38 | form_props(model: @post) do |f|
39 | f.radio_button(:admin, "1")
40 | end
41 | result = json.result!.strip
42 | expected = {
43 | "type" => "radio",
44 | "value" => "1",
45 | "defaultChecked" => true,
46 | "name" => "post[admin]",
47 | "id" => "post_admin_1"
48 | }
49 | assert_equal(JSON.parse(result)["inputs"]["admin1"], expected)
50 | end
51 |
52 | def test_radio_button_with_negative_integer_value
53 | @post.admin = -1
54 | form_props(model: @post) do |f|
55 | f.radio_button(:admin, "-1")
56 | end
57 | result = json.result!.strip
58 | expected = {
59 | "type" => "radio",
60 | "value" => "-1",
61 | "defaultChecked" => true,
62 | "name" => "post[admin]",
63 | "id" => "post_admin_-1"
64 | }
65 | assert_equal(JSON.parse(result)["inputs"]["admin-1"], expected)
66 | end
67 |
68 | def test_radio_button_respects_passed_in_id
69 | @post.admin = 1
70 | form_props(model: @post) do |f|
71 | f.radio_button(:admin, "1", id: "foo")
72 | end
73 | result = json.result!.strip
74 | expected = {
75 | "type" => "radio",
76 | "value" => "1",
77 | "defaultChecked" => true,
78 | "name" => "post[admin]",
79 | "id" => "foo"
80 | }
81 | assert_equal(JSON.parse(result)["inputs"]["admin1"], expected)
82 | end
83 |
84 | def test_radio_button_with_booleans
85 | @post.admin = false
86 | form_props(model: @post) do |f|
87 | f.radio_button(:admin, true)
88 | end
89 | result = json.result!.strip
90 | expected = {
91 | "type" => "radio",
92 | "value" => "true",
93 | "name" => "post[admin]",
94 | "id" => "post_admin_true"
95 | }
96 | assert_equal(JSON.parse(result)["inputs"]["adminTrue"], expected)
97 |
98 | @post.admin = true
99 | form_props(model: @post) do |f|
100 | f.radio_button(:admin, false)
101 | end
102 | result = json.result!.strip
103 | expected = {
104 | "type" => "radio",
105 | "value" => "false",
106 | "name" => "post[admin]",
107 | "id" => "post_admin_false"
108 | }
109 | assert_equal(JSON.parse(result)["inputs"]["adminFalse"], expected)
110 | end
111 | end
112 |
--------------------------------------------------------------------------------
/lib/form_props/inputs/base.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module Inputs
5 | class Base < ::ActionView::Helpers::Tags::Base
6 | # Remove when no longer supporting Rails 7
7 | if ActionView::VERSION::STRING < "8"
8 | alias_method :add_default_name_and_field_for_value, :add_default_name_and_id_for_value
9 | alias_method :add_default_name_and_field, :add_default_name_and_id
10 | end
11 |
12 | def json
13 | @json ||= @template_object.instance_variable_get(:@__json)
14 | end
15 |
16 | def initialize(object_name, method_name, template_object, options = {})
17 | options = options.with_indifferent_access
18 |
19 | @controlled = options.delete(:controlled)
20 | @key = options.delete(:key)
21 |
22 | super
23 | end
24 |
25 | private
26 |
27 | def sanitized_key
28 | @key || sanitized_method_name.camelize(:lower)
29 | end
30 |
31 | def input_props(options)
32 | return if options.blank?
33 | # tag_type = options[:type]
34 |
35 | options.each_pair do |key, value|
36 | type = TAG_TYPES[key]
37 |
38 | if type == :data && value.is_a?(Hash)
39 | value.each_pair do |k, v|
40 | next if v.nil?
41 | prefix_tag_props(key, k, v)
42 | end
43 | elsif type == :aria && value.is_a?(Hash)
44 | value.each_pair do |k, v|
45 | next if v.nil?
46 |
47 | case v
48 | when Array, Hash
49 | tokens = build_values(v)
50 | next if tokens.none?
51 |
52 | v = safe_join(tokens, " ")
53 | else
54 | v = v.to_s
55 | end
56 |
57 | prefix_tag_props(key, k, v)
58 | end
59 | elsif key == "class" || key == :class
60 | value = build_values(value).join(" ")
61 | key = "class_name"
62 | tag_option(key, value)
63 | elsif !value.nil?
64 | if key.to_s == "value"
65 | key = key.to_s
66 | value = value.is_a?(Array) ? value : value.to_s
67 | end
68 | tag_option(key, value)
69 | end
70 | end
71 | end
72 |
73 | def tag_option(key, value)
74 | if value.is_a? Regexp
75 | value = value.source
76 | end
77 |
78 | is_checkable = respond_to?(:field_type, true) && (field_type == "checkbox" || field_type == "radio")
79 |
80 | @controlled ||= nil
81 |
82 | if !@controlled
83 | if key.to_sym == :value && !is_checkable
84 | key = "default_value"
85 | end
86 |
87 | if key.to_sym == :checked
88 | key = "default_checked"
89 | end
90 | end
91 |
92 | key = FormProps::Helper.format_key(key)
93 | json.set!(key, value)
94 | end
95 |
96 | def prefix_tag_props(prefix, key, value)
97 | key = "#{prefix}-#{key.to_s.dasherize}"
98 | unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
99 | value = value.to_json
100 | end
101 | tag_option(key, value)
102 | end
103 |
104 | def build_values(*args)
105 | tag_values = []
106 |
107 | args.each do |tag_value|
108 | case tag_value
109 | when Hash
110 | tag_value.each do |key, val|
111 | tag_values << key.to_s if val && key.present?
112 | end
113 | when Array
114 | tag_values.concat build_values(*tag_value)
115 | else
116 | tag_values << tag_value.to_s if tag_value.present?
117 | end
118 | end
119 |
120 | tag_values
121 | end
122 | end
123 | end
124 | end
125 |
--------------------------------------------------------------------------------
/lib/form_props/action_view_extensions/form_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module ActionViewExtensions
5 | module FormHelper
6 | def form_props(model: nil, scope: nil, url: nil, format: nil, **options, &block)
7 | json = @__json
8 |
9 | options = {
10 | allow_method_names_outside_object: true,
11 | controlled: false
12 | }.merge!(options)
13 |
14 | options.delete(:remote)
15 |
16 | options[:local] = true
17 | options[:skip_default_ids] = false
18 |
19 | if model
20 | if url != false
21 | url ||= if format.nil?
22 | polymorphic_path(model, {})
23 | else
24 | polymorphic_path(model, format: format)
25 | end
26 | end
27 |
28 | model = convert_to_model(_object_for_form_builder(model))
29 | scope ||= model_name_from_record_or_class(model).param_key
30 | end
31 |
32 | if block
33 | options[:builder] = FormProps::FormBuilder
34 |
35 | builder = instantiate_builder(scope, model, options)
36 | json.inputs do
37 | capture(builder, &block)
38 | end
39 | options[:multipart] ||= builder.multipart?
40 | else
41 | json.inputs({})
42 | end
43 |
44 | html_options = html_options_for_form_with(url, model, **options)
45 |
46 | json.extras do
47 | extra_props_for_form(json, html_options)
48 | end
49 |
50 | json.form(FormProps::Helper.format_keys(html_options))
51 | end
52 |
53 | private
54 |
55 | def token_props(json, token = nil, form_options: {})
56 | if token != false && defined?(protect_against_forgery?) && protect_against_forgery?
57 | token =
58 | if token == true || token.nil?
59 | form_authenticity_token(form_options: form_options.merge(authenticity_token: token))
60 | else
61 | token
62 | end
63 |
64 | json.set!("csrf") do
65 | json.name request_forgery_protection_token.to_s
66 | json.type "hidden"
67 | json.defaultValue token
68 | json.autoComplete "off"
69 | end
70 | end
71 | end
72 |
73 | def method_props(json, method)
74 | json.set!("method") do
75 | json.name "_method"
76 | json.type "hidden"
77 | json.defaultValue method.to_s
78 | json.autoComplete "off"
79 | end
80 | end
81 |
82 | def utf8_enforcer_props(json)
83 | json.set!("utf8") do
84 | json.name "utf8"
85 | json.type "hidden"
86 | json.defaultValue "✓"
87 | json.autoComplete "off"
88 | end
89 | end
90 |
91 | def extra_props_for_form(json, html_options)
92 | authenticity_token = html_options.delete("authenticity_token")
93 | method = html_options.delete("method").to_s.downcase
94 |
95 | case method
96 | when "get"
97 | html_options["method"] = "get"
98 | when "post", ""
99 | html_options["method"] = "post"
100 | token_props(json, authenticity_token, form_options: {
101 | action: html_options["action"],
102 | method: "post"
103 | })
104 | else
105 | html_options["method"] = "post"
106 | method_props(json, method)
107 | token_props(json, authenticity_token, form_options: {
108 | action: html_options["action"],
109 | method: method
110 | })
111 | end
112 |
113 | if html_options.delete("enforce_utf8") { default_enforce_utf8 }
114 | utf8_enforcer_props(json)
115 | end
116 | end
117 | end
118 | end
119 | end
120 |
121 | ActiveSupport.on_load(:action_view) do
122 | include FormProps::ActionViewExtensions::FormHelper
123 | end
124 |
--------------------------------------------------------------------------------
/test/inputs/grouped_collection_select_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class GroupedCollectionSelectTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | Country = Struct.new("Country", :country_id, :country_name)
7 |
8 | setup :setup_test_fixture
9 |
10 | def test_option_groups_from_collection_for_select
11 | @post = Post.new
12 | @post.country = "dk"
13 |
14 | form_props(model: @post) do |f|
15 | f.grouped_collection_select(
16 | :country, dummy_continents, "countries", "id", "country_id", "country_name"
17 | )
18 | end
19 | result = json.result!
20 |
21 | expected = {
22 | "name" => "post[country]",
23 | "id" => "post_country",
24 | "type" => "select",
25 | "defaultValue" => "dk",
26 | "options" => [
27 | {
28 | "label" => "",
29 | "options" => [
30 | {"value" => "", "label" => ""},
31 | {"value" => "so", "label" => "Somalia"}
32 | ]
33 | }, {
34 | "label" => "Europe",
35 | "options" => [
36 | {"value" => "dk", "label" => "Denmark"},
37 | {"value" => "ie", "label" => "Ireland"}
38 | ]
39 | }
40 | ]
41 | }
42 | assert_equal(JSON.parse(result)["inputs"]["country"], expected)
43 | end
44 |
45 | def test_option_groups_from_collection_for_select_with_callable_group_method
46 | group_proc = proc { |c| c.countries }
47 | @post = Post.new
48 | @post.country = "dk"
49 |
50 | form_props(model: @post) do |f|
51 | f.grouped_collection_select(
52 | :country, dummy_continents, group_proc, "id", "country_id", "country_name"
53 | )
54 | end
55 | result = json.result!
56 |
57 | expected = {
58 | "name" => "post[country]",
59 | "id" => "post_country",
60 | "type" => "select",
61 | "defaultValue" => "dk",
62 | "options" => [
63 | {
64 | "label" => "",
65 | "options" => [
66 | {"value" => "", "label" => ""},
67 | {"value" => "so", "label" => "Somalia"}
68 | ]
69 | }, {
70 | "label" => "Europe",
71 | "options" => [
72 | {"value" => "dk", "label" => "Denmark"},
73 | {"value" => "ie", "label" => "Ireland"}
74 | ]
75 | }
76 | ]
77 | }
78 | assert_equal(JSON.parse(result)["inputs"]["country"], expected)
79 | end
80 |
81 | def test_option_groups_from_collection_for_select_with_callable_group_label_method
82 | label_proc = proc { |c| c.id }
83 | @post = Post.new
84 | @post.country = "dk"
85 |
86 | form_props(model: @post) do |f|
87 | f.grouped_collection_select(
88 | :country, dummy_continents, "countries", label_proc, "country_id", "country_name"
89 | )
90 | end
91 | result = json.result!
92 |
93 | expected = {
94 | "name" => "post[country]",
95 | "id" => "post_country",
96 | "type" => "select",
97 | "defaultValue" => "dk",
98 | "options" => [
99 | {
100 | "label" => "",
101 | "options" => [
102 | {"value" => "", "label" => ""},
103 | {"value" => "so", "label" => "Somalia"}
104 | ]
105 | }, {
106 | "label" => "Europe",
107 | "options" => [
108 | {"value" => "dk", "label" => "Denmark"},
109 | {"value" => "ie", "label" => "Ireland"}
110 | ]
111 | }
112 | ]
113 | }
114 | assert_equal(JSON.parse(result)["inputs"]["country"], expected)
115 | end
116 |
117 | private
118 |
119 | def dummy_continents
120 | [Continent.new("", [Country.new("", ""), Country.new("so", "Somalia")]),
121 | Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")])]
122 | end
123 | end
124 |
--------------------------------------------------------------------------------
/test/inputs/weekday_select_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class WeekdaySelectTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_weekday_select
9 | @post = Post.new
10 | @post.weekday = nil
11 |
12 | form_props(model: @post) do |f|
13 | f.weekday_select(:weekday)
14 | end
15 | result = json.result!
16 |
17 | expected = {
18 | "name" => "post[weekday]",
19 | "id" => "post_weekday",
20 | "type" => "select",
21 | "options" => [
22 | {"value" => "Monday", "label" => "Monday"},
23 | {"value" => "Tuesday", "label" => "Tuesday"},
24 | {"value" => "Wednesday", "label" => "Wednesday"},
25 | {"value" => "Thursday", "label" => "Thursday"},
26 | {"value" => "Friday", "label" => "Friday"},
27 | {"value" => "Saturday", "label" => "Saturday"},
28 | {"value" => "Sunday", "label" => "Sunday"}
29 | ]
30 | }
31 | assert_equal(JSON.parse(result)["inputs"]["weekday"], expected)
32 | end
33 |
34 | def test_weekday_select_with_selected_value
35 | @post = Post.new
36 | @post.weekday = "Monday"
37 |
38 | form_props(model: @post) do |f|
39 | f.weekday_select(:weekday)
40 | end
41 | result = json.result!
42 |
43 | expected = {
44 | "name" => "post[weekday]",
45 | "id" => "post_weekday",
46 | "type" => "select",
47 | "defaultValue" => "Monday",
48 | "options" => [
49 | {"value" => "Monday", "label" => "Monday"},
50 | {"value" => "Tuesday", "label" => "Tuesday"},
51 | {"value" => "Wednesday", "label" => "Wednesday"},
52 | {"value" => "Thursday", "label" => "Thursday"},
53 | {"value" => "Friday", "label" => "Friday"},
54 | {"value" => "Saturday", "label" => "Saturday"},
55 | {"value" => "Sunday", "label" => "Sunday"}
56 | ]
57 | }
58 | assert_equal(JSON.parse(result)["inputs"]["weekday"], expected)
59 | end
60 |
61 | def test_weekday_select_under_fields_for
62 | @post = Post.new
63 |
64 | json.output do
65 | fields_for :post, @post, builder: FormProps::FormBuilder do |f|
66 | f.weekday_select(:weekday)
67 | end
68 | end
69 | result = json.result!.strip
70 |
71 | expected = {
72 | "name" => "post[weekday]",
73 | "id" => "post_weekday",
74 | "type" => "select",
75 | "options" => [
76 | {"value" => "Monday", "label" => "Monday"},
77 | {"value" => "Tuesday", "label" => "Tuesday"},
78 | {"value" => "Wednesday", "label" => "Wednesday"},
79 | {"value" => "Thursday", "label" => "Thursday"},
80 | {"value" => "Friday", "label" => "Friday"},
81 | {"value" => "Saturday", "label" => "Saturday"},
82 | {"value" => "Sunday", "label" => "Sunday"}
83 | ]
84 | }
85 |
86 | assert_equal(JSON.parse(result)["output"]["weekday"], expected)
87 | end
88 |
89 | def test_weekday_select_under_fields_for_with_value
90 | @post = Post.new
91 | @post.weekday = "Monday"
92 |
93 | json.output do
94 | fields_for :post, @post, builder: FormProps::FormBuilder do |f|
95 | f.weekday_select(:weekday)
96 | end
97 | end
98 | result = json.result!.strip
99 |
100 | expected = {
101 | "name" => "post[weekday]",
102 | "id" => "post_weekday",
103 | "type" => "select",
104 | "defaultValue" => "Monday",
105 | "options" => [
106 | {"value" => "Monday", "label" => "Monday"},
107 | {"value" => "Tuesday", "label" => "Tuesday"},
108 | {"value" => "Wednesday", "label" => "Wednesday"},
109 | {"value" => "Thursday", "label" => "Thursday"},
110 | {"value" => "Friday", "label" => "Friday"},
111 | {"value" => "Saturday", "label" => "Saturday"},
112 | {"value" => "Sunday", "label" => "Sunday"}
113 | ]
114 | }
115 |
116 | assert_equal(JSON.parse(result)["output"]["weekday"], expected)
117 | end
118 | end
119 |
--------------------------------------------------------------------------------
/test/inputs/time_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class TimeFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_time_field
9 | @post.written_on = nil
10 |
11 | form_props(model: @post) do |f|
12 | f.time_field(:written_on)
13 | end
14 |
15 | result = json.result!.strip
16 | expected = {
17 | "type" => "time",
18 | "name" => "post[written_on]",
19 | "id" => "post_written_on"
20 | }
21 |
22 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
23 | end
24 |
25 | def test_time_field_with_datetime_value
26 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
27 |
28 | form_props(model: @post) do |f|
29 | f.time_field(:written_on)
30 | end
31 |
32 | result = json.result!.strip
33 | expected = {
34 | "type" => "time",
35 | "defaultValue" => "01:02:03.000",
36 | "name" => "post[written_on]",
37 | "id" => "post_written_on"
38 | }
39 |
40 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
41 | end
42 |
43 | def test_time_field_with_extra_attrs
44 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
45 | min_value = DateTime.new(2000, 6, 15, 20, 45, 30)
46 | max_value = DateTime.new(2010, 8, 15, 10, 25, 0o0)
47 | step = 60
48 |
49 | form_props(model: @post) do |f|
50 | f.time_field(:written_on, min: min_value, max: max_value, step: step)
51 | end
52 |
53 | result = json.result!.strip
54 | expected = {
55 | "type" => "time",
56 | "defaultValue" => "01:02:03.000",
57 | "step" => 60,
58 | "max" => "10:25:00.000",
59 | "min" => "20:45:30.000",
60 | "name" => "post[written_on]",
61 | "id" => "post_written_on"
62 | }
63 |
64 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
65 | end
66 |
67 | def test_time_field_with_timewithzone_value
68 | previous_time_zone, Time.zone = Time.zone, "UTC"
69 | @post.written_on = Time.zone.parse("2004-06-15 01:02:03")
70 |
71 | form_props(model: @post) do |f|
72 | f.time_field(:written_on)
73 | end
74 |
75 | result = json.result!.strip
76 | expected = {
77 | "type" => "time",
78 | "defaultValue" => "01:02:03.000",
79 | "name" => "post[written_on]",
80 | "id" => "post_written_on"
81 | }
82 |
83 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
84 | ensure
85 | Time.zone = previous_time_zone
86 | end
87 |
88 | def test_time_field_with_nil_value
89 | @post.written_on = nil
90 |
91 | form_props(model: @post) do |f|
92 | f.time_field(:written_on)
93 | end
94 |
95 | result = json.result!.strip
96 | expected = {
97 | "type" => "time",
98 | "name" => "post[written_on]",
99 | "id" => "post_written_on"
100 | }
101 |
102 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
103 | end
104 |
105 | def test_time_field_with_string_values_for_min_and_max
106 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
107 | min_value = "20:45:30.000"
108 | max_value = "10:25:00.000"
109 |
110 | form_props(model: @post) do |f|
111 | f.time_field(:written_on, min: min_value, max: max_value)
112 | end
113 |
114 | result = json.result!.strip
115 | expected = {
116 | "type" => "time",
117 | "defaultValue" => "01:02:03.000",
118 | "max" => "10:25:00.000",
119 | "min" => "20:45:30.000",
120 | "name" => "post[written_on]",
121 | "id" => "post_written_on"
122 | }
123 |
124 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
125 | end
126 |
127 | def test_time_field_with_invalid_string_values_for_min_and_max
128 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
129 | min_value = "foo"
130 | max_value = "bar"
131 |
132 | form_props(model: @post) do |f|
133 | f.time_field(:written_on, min: min_value, max: max_value)
134 | end
135 |
136 | result = json.result!.strip
137 | expected = {
138 | "type" => "time",
139 | "defaultValue" => "01:02:03.000",
140 | "name" => "post[written_on]",
141 | "id" => "post_written_on"
142 | }
143 |
144 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
145 | end
146 | end
147 |
--------------------------------------------------------------------------------
/test/inputs/input_options_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class InputOptionsTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_tag_builder_with_conditional_hash_classes
9 | form_props(model: @post) do |f|
10 | f.text_field(:body, class: [{song: true}, {play: false}])
11 | end
12 | result = json.result!.strip
13 |
14 | assert_equal(JSON.parse(result)["inputs"]["body"]["className"], "song")
15 |
16 | form_props(model: @post) do |f|
17 | f.text_field(:body, class: {song: true, play: false})
18 | end
19 | result = json.result!.strip
20 |
21 | assert_equal(JSON.parse(result)["inputs"]["body"]["className"], "song")
22 |
23 | form_props(model: @post) do |f|
24 | f.text_field(:body, class: [{song: true}, nil, false])
25 | end
26 | result = json.result!.strip
27 |
28 | assert_equal(JSON.parse(result)["inputs"]["body"]["className"], "song")
29 |
30 | form_props(model: @post) do |f|
31 | f.text_field(:body, class: ["song", {foo: false}])
32 | end
33 | result = json.result!.strip
34 |
35 | assert_equal(JSON.parse(result)["inputs"]["body"]["className"], "song")
36 |
37 | form_props(model: @post) do |f|
38 | f.text_field(:body, class: {song: true, play: true})
39 | end
40 | result = json.result!.strip
41 |
42 | assert_equal(JSON.parse(result)["inputs"]["body"]["className"], "song play")
43 |
44 | form_props(model: @post) do |f|
45 | f.text_field(:body, class: {song: false, play: false})
46 | end
47 | result = json.result!.strip
48 |
49 | assert_equal(JSON.parse(result)["inputs"]["body"]["className"], "")
50 | end
51 |
52 | def test_tag_builder_with_empty_array_class
53 | @post = Post.new
54 | @post.body = "test"
55 | form_props(model: @post) do |f|
56 | f.text_field(:body, class: [])
57 | end
58 |
59 | result = json.result!.strip
60 |
61 | expected = {
62 | "type" => "text",
63 | "name" => "post[body]",
64 | "id" => "post_body",
65 | "defaultValue" => "test",
66 | "className" => ""
67 | }
68 |
69 | assert_equal(JSON.parse(result)["inputs"]["body"], expected)
70 | end
71 |
72 | def test_data_attributes
73 | @post = Post.new
74 | @post.body = "test"
75 | form_props(model: @post) do |f|
76 | f.text_field(:body, data: {a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: {key: "value"}, string_with_quotes: 'double"quote"party"'})
77 | end
78 |
79 | result = json.result!.strip
80 |
81 | expected = {
82 | "type" => "text",
83 | "name" => "post[body]",
84 | "id" => "post_body",
85 | "defaultValue" => "test",
86 | "data-a-float" => "3.14",
87 | "data-a-big-decimal" => "-123.456",
88 | "data-a-number" => "1",
89 | "data-array" => "[1,2,3]",
90 | "data-hash" => "{\"key\":\"value\"}",
91 | "data-string-with-quotes" => "double\"quote\"party\"",
92 | "data-string" => "hello",
93 | "data-symbol" => "foo"
94 | }
95 |
96 | assert_equal(JSON.parse(result)["inputs"]["body"], expected)
97 | end
98 |
99 | def test_aria_attributes
100 | @post = Post.new
101 | @post.body = "test"
102 |
103 | form_props(model: @post) do |f|
104 | f.text_field(:body, aria: {nil: nil, a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, truthy: true, falsey: false, string: "hello", symbol: :foo, array: [1, 2, 3], empty_array: [], hash: {a: true, b: "truthy", falsey: false, nil: nil}, empty_hash: {}, tokens: ["a", {b: true, c: false}], empty_tokens: [{a: false}], string_with_quotes: 'double"quote"party"'})
105 | end
106 |
107 | result = json.result!.strip
108 |
109 | expected = {
110 | "type" => "text",
111 | "name" => "post[body]",
112 | "id" => "post_body",
113 | "defaultValue" => "test",
114 | "aria-a-float" => "3.14",
115 | "aria-a-big-decimal" => "-123.456",
116 | "aria-a-number" => "1",
117 | "aria-truthy" => "true",
118 | "aria-falsey" => "false",
119 | "aria-array" => "1 2 3",
120 | "aria-hash" => "a b",
121 | "aria-tokens" => "a b",
122 | "aria-string-with-quotes" => "double\"quote\"party\"",
123 | "aria-string" => "hello",
124 | "aria-symbol" => "foo"
125 | }
126 |
127 | assert_equal(JSON.parse(result)["inputs"]["body"], expected)
128 | end
129 | end
130 |
--------------------------------------------------------------------------------
/test/inputs/file_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | module ActionController
4 | class Base
5 | def self.test_routes(&block)
6 | routes = ActionDispatch::Routing::RouteSet.new
7 | routes.draw(&block)
8 | include routes.url_helpers
9 | routes
10 | end
11 | end
12 | end
13 |
14 | class WithActiveStorageRoutesControllers < ActionController::Base
15 | test_routes do
16 | post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", :as => :rails_direct_uploads
17 | end
18 |
19 | def url_options
20 | {host: "testtwo.host"}
21 | end
22 | end
23 |
24 | class FileFieldTest < ActionView::TestCase
25 | include FormProps::ActionViewExtensions::FormHelper
26 |
27 | setup :setup_test_fixture
28 |
29 | def test_file_field
30 | @post.splash = nil
31 |
32 | form_props(model: @post) do |f|
33 | f.file_field(:splash)
34 | end
35 |
36 | result = json.result!.strip
37 | expected = {
38 | "type" => "file",
39 | "name" => "post[splash]",
40 | "id" => "post_splash"
41 | }
42 |
43 | assert_equal(JSON.parse(result)["inputs"]["splash"], expected)
44 | end
45 |
46 | def test_file_field_with_options
47 | @post.splash = nil
48 |
49 | form_props(model: @post) do |f|
50 | f.file_field(:splash, class: "pix")
51 | end
52 |
53 | result = json.result!.strip
54 | expected = {
55 | "type" => "file",
56 | "name" => "post[splash]",
57 | "className" => "pix",
58 | "id" => "post_splash"
59 | }
60 |
61 | assert_equal(JSON.parse(result)["inputs"]["splash"], expected)
62 | end
63 |
64 | def test_file_field_tag_with_direct_upload_when_rails_direct_uploads_url_is_not_defined
65 | @post.splash = nil
66 |
67 | form_props(model: @post) do |f|
68 | f.file_field(:splash, class: "pix", direct_upload: true)
69 | end
70 |
71 | result = json.result!.strip
72 | expected = {
73 | "type" => "file",
74 | "name" => "post[splash]",
75 | "className" => "pix",
76 | "id" => "post_splash"
77 | }
78 |
79 | assert_equal(JSON.parse(result)["inputs"]["splash"], expected)
80 | end
81 |
82 | def test_file_field_tag_with_direct_upload_when_rails_direct_uploads_url_is_defined
83 | @post.splash = nil
84 | @controller = WithActiveStorageRoutesControllers.new
85 |
86 | form_props(model: @post) do |f|
87 | f.file_field(:splash, class: "pix", direct_upload: true)
88 | end
89 |
90 | result = json.result!.strip
91 | expected = {
92 | "type" => "file",
93 | "name" => "post[splash]",
94 | "className" => "pix",
95 | "data-direct-upload-url" => "http://testtwo.host/rails/active_storage/direct_uploads",
96 | "id" => "post_splash"
97 | }
98 |
99 | assert_equal(JSON.parse(result)["inputs"]["splash"], expected)
100 | end
101 |
102 | def test_file_field_tag_with_direct_upload_dont_mutate_arguments
103 | original_options = {class: "pix", direct_upload: true}
104 |
105 | form_props(model: @post) do |f|
106 | f.file_field(:splash, class: "pix", direct_upload: true)
107 | end
108 |
109 | json.result!.strip
110 |
111 | assert_equal({class: "pix", direct_upload: true}, original_options)
112 | end
113 |
114 | def test_file_field_has_no_size
115 | @post.splash = nil
116 |
117 | form_props(model: @post) do |f|
118 | f.file_field(:splash)
119 | end
120 |
121 | result = json.result!.strip
122 |
123 | assert_nil(JSON.parse(result)["inputs"]["splash"]["size"])
124 | end
125 |
126 | def test_file_field_with_multiple_behavior
127 | @post.splash = nil
128 |
129 | form_props(model: @post) do |f|
130 | f.file_field(:splash, multiple: true)
131 | end
132 |
133 | result = json.result!.strip
134 | expected = {
135 | "type" => "file",
136 | "name" => "post[splash][]",
137 | "id" => "post_splash",
138 | "multiple" => true
139 | }
140 |
141 | assert_equal(JSON.parse(result)["inputs"]["splash"], expected)
142 | end
143 |
144 | def test_file_field_with_multiple_behavior_and_explicit_name
145 | @post.splash = nil
146 |
147 | form_props(model: @post) do |f|
148 | f.file_field(:splash, multiple: true, name: "custom")
149 | end
150 |
151 | result = json.result!.strip
152 | expected = {
153 | "type" => "file",
154 | "id" => "post_splash",
155 | "multiple" => true,
156 | "name" => "custom"
157 | }
158 |
159 | assert_equal(JSON.parse(result)["inputs"]["splash"], expected)
160 | end
161 | end
162 |
--------------------------------------------------------------------------------
/test/inputs/date_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class DateFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_date_field
9 | @post.written_on = nil
10 |
11 | form_props(model: @post) do |f|
12 | f.date_field(:written_on)
13 | end
14 |
15 | result = json.result!.strip
16 | expected = {
17 | "type" => "date",
18 | "name" => "post[written_on]",
19 | "id" => "post_written_on"
20 | }
21 |
22 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
23 | end
24 |
25 | def test_date_field_with_datetime_value
26 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
27 |
28 | form_props(model: @post) do |f|
29 | f.date_field(:written_on)
30 | end
31 |
32 | result = json.result!.strip
33 | expected = {
34 | "type" => "date",
35 | "defaultValue" => "2004-06-15",
36 | "name" => "post[written_on]",
37 | "id" => "post_written_on"
38 | }
39 |
40 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
41 | end
42 |
43 | def test_date_field_with_extra_attrs
44 | @post.written_on = DateTime.new(2004, 6, 15)
45 | min_value = DateTime.new(2000, 6, 15)
46 | max_value = DateTime.new(2010, 8, 15)
47 | step = 60
48 |
49 | form_props(model: @post) do |f|
50 | f.date_field(:written_on, min: min_value, max: max_value, step: step)
51 | end
52 |
53 | result = json.result!.strip
54 | expected = {
55 | "type" => "date",
56 | "defaultValue" => "2004-06-15",
57 | "step" => 60,
58 | "max" => "2010-08-15",
59 | "min" => "2000-06-15",
60 | "name" => "post[written_on]",
61 | "id" => "post_written_on"
62 | }
63 |
64 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
65 | end
66 |
67 | def test_date_field_with_value_attr
68 | @post.written_on = DateTime.new(2004, 6, 15)
69 |
70 | form_props(model: @post) do |f|
71 | f.date_field(:written_on, value: Date.new(2013, 6, 29))
72 | end
73 |
74 | result = json.result!.strip
75 | expected = {
76 | "type" => "date",
77 | "defaultValue" => "2013-06-29",
78 | "name" => "post[written_on]",
79 | "id" => "post_written_on"
80 | }
81 |
82 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
83 | end
84 |
85 | def test_date_field_with_timewithzone_value
86 | previous_time_zone, Time.zone = Time.zone, "UTC"
87 | @post.written_on = Time.zone.parse("2004-06-15 15:30:45")
88 |
89 | form_props(model: @post) do |f|
90 | f.date_field(:written_on)
91 | end
92 |
93 | result = json.result!.strip
94 | expected = {
95 | "type" => "date",
96 | "defaultValue" => "2004-06-15",
97 | "name" => "post[written_on]",
98 | "id" => "post_written_on"
99 | }
100 |
101 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
102 | ensure
103 | Time.zone = previous_time_zone
104 | end
105 |
106 | def test_date_field_with_nil_value
107 | @post.written_on = nil
108 |
109 | form_props(model: @post) do |f|
110 | f.date_field(:written_on)
111 | end
112 |
113 | result = json.result!.strip
114 | expected = {
115 | "type" => "date",
116 | "name" => "post[written_on]",
117 | "id" => "post_written_on"
118 | }
119 |
120 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
121 | end
122 |
123 | def test_date_field_with_string_values_for_min_and_max
124 | @post.written_on = DateTime.new(2004, 6, 15)
125 | min_value = "2000-06-15"
126 | max_value = "2010-08-15"
127 |
128 | form_props(model: @post) do |f|
129 | f.date_field(:written_on, min: min_value, max: max_value)
130 | end
131 |
132 | result = json.result!.strip
133 | expected = {
134 | "type" => "date",
135 | "defaultValue" => "2004-06-15",
136 | "max" => "2010-08-15",
137 | "min" => "2000-06-15",
138 | "name" => "post[written_on]",
139 | "id" => "post_written_on"
140 | }
141 |
142 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
143 | end
144 |
145 | def test_date_field_with_invalid_string_values_for_min_and_max
146 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
147 | min_value = "foo"
148 | max_value = "bar"
149 |
150 | form_props(model: @post) do |f|
151 | f.date_field(:written_on, min: min_value, max: max_value)
152 | end
153 |
154 | result = json.result!.strip
155 | expected = {
156 | "type" => "date",
157 | "defaultValue" => "2004-06-15",
158 | "name" => "post[written_on]",
159 | "id" => "post_written_on"
160 | }
161 |
162 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
163 | end
164 | end
165 |
--------------------------------------------------------------------------------
/lib/form_props/form_options_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | module FormOptionsHelper
5 | def selected_values
6 | @selected_values ||= []
7 | @selected_values
8 | end
9 |
10 | def grouped_options_for_select(grouped_options, selected_key = nil, options = {})
11 | prompt = options[:prompt]
12 | divider = options[:divider]
13 |
14 | options = []
15 |
16 | if prompt
17 | options.push({
18 | label: prompt_text(prompt),
19 | value: ""
20 | })
21 | end
22 |
23 | grouped_options.each do |container|
24 | html_attributes = option_html_attributes(container)
25 |
26 | if divider
27 | label = divider
28 | else
29 | label, container = container
30 | end
31 |
32 | options.push({label: label, options: options_for_select(container, selected_key)}
33 | .merge!(html_attributes))
34 | end
35 |
36 | options
37 | end
38 |
39 | def option_html_attributes(element)
40 | if Array === element
41 | element.select { |e| Hash === e }.reduce({}, :merge!)
42 | else
43 | {}
44 | end
45 | end
46 |
47 | def options_for_select(container, selected = nil)
48 | selected, disabled = extract_selected_and_disabled(selected).map do |r|
49 | Array(r).map(&:to_s)
50 | end
51 |
52 | container.map do |element|
53 | html_attributes = option_html_attributes(element)
54 | text, value = option_text_and_value(element).map(&:to_s)
55 |
56 | if !html_attributes[:selected] && option_value_selected?(value, selected)
57 | selected_values.push(value)
58 | end
59 |
60 | if html_attributes[:selected]
61 | selected_values.push(value)
62 | end
63 |
64 | if !html_attributes[:disabled] && (disabled && option_value_selected?(value, disabled))
65 | html_attributes[:disabled] = true
66 | end
67 |
68 | html_attributes[:value] = value
69 | html_attributes[:label] = text
70 |
71 | html_attributes
72 | end
73 | end
74 |
75 | def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
76 | collection.map do |group|
77 | option_tags = options_from_collection_for_select(
78 | value_for_collection(group, group_method),
79 | option_key_method,
80 | option_value_method,
81 | selected_key
82 | )
83 |
84 | {
85 | options: option_tags,
86 | label: value_for_collection(group, group_label_method)
87 | }
88 | end
89 | end
90 |
91 | def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
92 | options = collection.map do |element|
93 | [value_for_collection(element, text_method), value_for_collection(element, value_method), option_html_attributes(element)]
94 | end
95 | selected, disabled = extract_selected_and_disabled(selected)
96 | select_deselect = {
97 | selected: extract_values_from_collection(collection, value_method, selected),
98 | disabled: extract_values_from_collection(collection, value_method, disabled)
99 | }
100 |
101 | options_for_select(options, select_deselect)
102 | end
103 |
104 | def value_for_collection(item, value)
105 | value.respond_to?(:call) ? value.call(item) : item.public_send(value)
106 | end
107 |
108 | def extract_selected_and_disabled(selected)
109 | if selected.is_a?(Proc)
110 | [selected, nil]
111 | else
112 | selected = Array.wrap(selected)
113 | options = selected.extract_options!.symbolize_keys
114 | selected_items = options.fetch(:selected, selected)
115 | [selected_items, options[:disabled]]
116 | end
117 | end
118 |
119 | def extract_values_from_collection(collection, value_method, selected)
120 | if selected.is_a?(Proc)
121 | collection.map do |element|
122 | element.public_send(value_method) if selected.call(element)
123 | end.compact
124 | else
125 | selected
126 | end
127 | end
128 |
129 | def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone)
130 | zone_options = []
131 |
132 | zones = model.all
133 | convert_zones = lambda { |list| list.map { |z| [z.to_s, z.name] } }
134 |
135 | if priority_zones
136 | if priority_zones.is_a?(Regexp)
137 | priority_zones = zones.select { |z| z.match?(priority_zones) }
138 | end
139 |
140 | zone_options.concat(options_for_select(convert_zones[priority_zones], selected))
141 | zone_options.push({label: "-------------", value: "", disabled: true})
142 |
143 | zones -= priority_zones
144 | end
145 |
146 | zone_options.concat(options_for_select(convert_zones[zones], selected))
147 | zone_options
148 | end
149 |
150 | def weekday_options_for_select(selected = nil, index_as_value: false, day_format: :day_names, beginning_of_week: Date.beginning_of_week)
151 | day_names = I18n.translate("date.#{day_format}")
152 | day_names = day_names.map.with_index.to_a if index_as_value
153 | day_names = day_names.rotate(Date::DAYS_INTO_WEEK.fetch(beginning_of_week))
154 |
155 | options_for_select(day_names, selected)
156 | end
157 | end
158 | end
159 |
--------------------------------------------------------------------------------
/test/inputs/datetime_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class DateTimeFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_datetime_field
9 | @post.written_on = nil
10 |
11 | form_props(model: @post) do |f|
12 | f.datetime_field(:written_on)
13 | end
14 |
15 | result = json.result!.strip
16 | expected = {
17 | "type" => "datetime-local",
18 | "name" => "post[written_on]",
19 | "id" => "post_written_on"
20 | }
21 |
22 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
23 | end
24 |
25 | def test_datetime_field_with_datetime_value
26 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
27 |
28 | form_props(model: @post) do |f|
29 | f.datetime_field(:written_on)
30 | end
31 |
32 | result = json.result!.strip
33 | expected = {
34 | "type" => "datetime-local",
35 | "defaultValue" => "2004-06-15T01:02:03",
36 | "name" => "post[written_on]",
37 | "id" => "post_written_on"
38 | }
39 |
40 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
41 | end
42 |
43 | def test_datetime_field_with_extra_attrs
44 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
45 | min_value = DateTime.new(2000, 6, 15, 20, 45, 30)
46 | max_value = DateTime.new(2010, 8, 15, 10, 25, 0o0)
47 | step = 60
48 |
49 | form_props(model: @post) do |f|
50 | f.datetime_field(:written_on, min: min_value, max: max_value, step: step)
51 | end
52 |
53 | result = json.result!.strip
54 | expected = {
55 | "type" => "datetime-local",
56 | "defaultValue" => "2004-06-15T01:02:03",
57 | "step" => 60,
58 | "max" => "2010-08-15T10:25:00",
59 | "min" => "2000-06-15T20:45:30",
60 | "name" => "post[written_on]",
61 | "id" => "post_written_on"
62 | }
63 |
64 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
65 | end
66 |
67 | def test_datetime_field_with_value_attr
68 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
69 |
70 | form_props(model: @post) do |f|
71 | f.datetime_field(:written_on, value: DateTime.new(2013, 6, 29, 13, 37))
72 | end
73 |
74 | result = json.result!.strip
75 | expected = {
76 | "type" => "datetime-local",
77 | "defaultValue" => "2013-06-29T13:37:00+00:00",
78 | "name" => "post[written_on]",
79 | "id" => "post_written_on"
80 | }
81 |
82 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
83 | end
84 |
85 | def test_datetime_field_with_timewithzone_value
86 | previous_time_zone, Time.zone = Time.zone, "UTC"
87 | @post.written_on = Time.zone.parse("2004-06-15 15:30:45")
88 |
89 | form_props(model: @post) do |f|
90 | f.datetime_field(:written_on)
91 | end
92 |
93 | result = json.result!.strip
94 | expected = {
95 | "type" => "datetime-local",
96 | "defaultValue" => "2004-06-15T15:30:45",
97 | "name" => "post[written_on]",
98 | "id" => "post_written_on"
99 | }
100 |
101 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
102 | ensure
103 | Time.zone = previous_time_zone
104 | end
105 |
106 | def test_datetime_field_with_nil_value
107 | @post.written_on = nil
108 |
109 | form_props(model: @post) do |f|
110 | f.datetime_field(:written_on)
111 | end
112 |
113 | result = json.result!.strip
114 | expected = {
115 | "type" => "datetime-local",
116 | "name" => "post[written_on]",
117 | "id" => "post_written_on"
118 | }
119 |
120 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
121 | end
122 |
123 | def test_datetime_field_with_string_values_for_min_and_max
124 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
125 | min_value = "2000-06-15T20:45:30"
126 | max_value = "2010-08-15T10:25:00"
127 |
128 | form_props(model: @post) do |f|
129 | f.datetime_field(:written_on, min: min_value, max: max_value)
130 | end
131 |
132 | result = json.result!.strip
133 | expected = {
134 | "type" => "datetime-local",
135 | "defaultValue" => "2004-06-15T01:02:03",
136 | "max" => "2010-08-15T10:25:00",
137 | "min" => "2000-06-15T20:45:30",
138 | "name" => "post[written_on]",
139 | "id" => "post_written_on"
140 | }
141 |
142 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
143 | end
144 |
145 | def test_datetime_field_with_invalid_string_values_for_min_and_max
146 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
147 | min_value = "foo"
148 | max_value = "bar"
149 |
150 | form_props(model: @post) do |f|
151 | f.datetime_field(:written_on, min: min_value, max: max_value)
152 | end
153 |
154 | result = json.result!.strip
155 | expected = {
156 | "type" => "datetime-local",
157 | "defaultValue" => "2004-06-15T01:02:03",
158 | "name" => "post[written_on]",
159 | "id" => "post_written_on"
160 | }
161 |
162 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
163 | end
164 |
165 | def test_datetime_local_field
166 | @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
167 |
168 | form_props(model: @post) do |f|
169 | f.datetime_local_field(:written_on)
170 | end
171 |
172 | result = json.result!.strip
173 | expected = {
174 | "type" => "datetime-local",
175 | "defaultValue" => "2004-06-15T01:02:03",
176 | "name" => "post[written_on]",
177 | "id" => "post_written_on"
178 | }
179 |
180 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
181 | end
182 | end
183 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@esbuild/android-arm64@0.17.19":
6 | version "0.17.19"
7 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd"
8 | integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==
9 |
10 | "@esbuild/android-arm@0.17.19":
11 | version "0.17.19"
12 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d"
13 | integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==
14 |
15 | "@esbuild/android-x64@0.17.19":
16 | version "0.17.19"
17 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1"
18 | integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==
19 |
20 | "@esbuild/darwin-arm64@0.17.19":
21 | version "0.17.19"
22 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276"
23 | integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==
24 |
25 | "@esbuild/darwin-x64@0.17.19":
26 | version "0.17.19"
27 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb"
28 | integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==
29 |
30 | "@esbuild/freebsd-arm64@0.17.19":
31 | version "0.17.19"
32 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2"
33 | integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==
34 |
35 | "@esbuild/freebsd-x64@0.17.19":
36 | version "0.17.19"
37 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4"
38 | integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==
39 |
40 | "@esbuild/linux-arm64@0.17.19":
41 | version "0.17.19"
42 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb"
43 | integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==
44 |
45 | "@esbuild/linux-arm@0.17.19":
46 | version "0.17.19"
47 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a"
48 | integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==
49 |
50 | "@esbuild/linux-ia32@0.17.19":
51 | version "0.17.19"
52 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a"
53 | integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==
54 |
55 | "@esbuild/linux-loong64@0.17.19":
56 | version "0.17.19"
57 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72"
58 | integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==
59 |
60 | "@esbuild/linux-mips64el@0.17.19":
61 | version "0.17.19"
62 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289"
63 | integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==
64 |
65 | "@esbuild/linux-ppc64@0.17.19":
66 | version "0.17.19"
67 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7"
68 | integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==
69 |
70 | "@esbuild/linux-riscv64@0.17.19":
71 | version "0.17.19"
72 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09"
73 | integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==
74 |
75 | "@esbuild/linux-s390x@0.17.19":
76 | version "0.17.19"
77 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829"
78 | integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==
79 |
80 | "@esbuild/linux-x64@0.17.19":
81 | version "0.17.19"
82 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4"
83 | integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==
84 |
85 | "@esbuild/netbsd-x64@0.17.19":
86 | version "0.17.19"
87 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462"
88 | integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==
89 |
90 | "@esbuild/openbsd-x64@0.17.19":
91 | version "0.17.19"
92 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691"
93 | integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==
94 |
95 | "@esbuild/sunos-x64@0.17.19":
96 | version "0.17.19"
97 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273"
98 | integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==
99 |
100 | "@esbuild/win32-arm64@0.17.19":
101 | version "0.17.19"
102 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f"
103 | integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==
104 |
105 | "@esbuild/win32-ia32@0.17.19":
106 | version "0.17.19"
107 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03"
108 | integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==
109 |
110 | "@esbuild/win32-x64@0.17.19":
111 | version "0.17.19"
112 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061"
113 | integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==
114 |
115 | esbuild@^0.17.5:
116 | version "0.17.19"
117 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955"
118 | integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==
119 | optionalDependencies:
120 | "@esbuild/android-arm" "0.17.19"
121 | "@esbuild/android-arm64" "0.17.19"
122 | "@esbuild/android-x64" "0.17.19"
123 | "@esbuild/darwin-arm64" "0.17.19"
124 | "@esbuild/darwin-x64" "0.17.19"
125 | "@esbuild/freebsd-arm64" "0.17.19"
126 | "@esbuild/freebsd-x64" "0.17.19"
127 | "@esbuild/linux-arm" "0.17.19"
128 | "@esbuild/linux-arm64" "0.17.19"
129 | "@esbuild/linux-ia32" "0.17.19"
130 | "@esbuild/linux-loong64" "0.17.19"
131 | "@esbuild/linux-mips64el" "0.17.19"
132 | "@esbuild/linux-ppc64" "0.17.19"
133 | "@esbuild/linux-riscv64" "0.17.19"
134 | "@esbuild/linux-s390x" "0.17.19"
135 | "@esbuild/linux-x64" "0.17.19"
136 | "@esbuild/netbsd-x64" "0.17.19"
137 | "@esbuild/openbsd-x64" "0.17.19"
138 | "@esbuild/sunos-x64" "0.17.19"
139 | "@esbuild/win32-arm64" "0.17.19"
140 | "@esbuild/win32-ia32" "0.17.19"
141 | "@esbuild/win32-x64" "0.17.19"
142 |
143 | "js-tokens@^3.0.0 || ^4.0.0":
144 | version "4.0.0"
145 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
146 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
147 |
148 | loose-envify@^1.1.0:
149 | version "1.4.0"
150 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
151 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
152 | dependencies:
153 | js-tokens "^3.0.0 || ^4.0.0"
154 |
155 | react-dom@^18.2.0:
156 | version "18.2.0"
157 | resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
158 | integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
159 | dependencies:
160 | loose-envify "^1.1.0"
161 | scheduler "^0.23.0"
162 |
163 | react@^18.2.0:
164 | version "18.2.0"
165 | resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
166 | integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
167 | dependencies:
168 | loose-envify "^1.1.0"
169 |
170 | scheduler@^0.23.0:
171 | version "0.23.0"
172 | resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
173 | integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
174 | dependencies:
175 | loose-envify "^1.1.0"
176 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] ||= "test"
2 | require "uri"
3 | require_relative "../lib/form_props"
4 | require "open3"
5 | require "byebug"
6 | require "props_template"
7 | require "minitest"
8 | require "minitest/autorun"
9 | require "mocha/minitest"
10 | require "rails"
11 |
12 | Rails.backtrace_cleaner.remove_silencers!
13 |
14 | require "active_model"
15 |
16 | Continent = Struct.new(:id, :countries) do
17 | extend ActiveModel::Naming
18 | include ActiveModel::Conversion
19 |
20 | def errors
21 | Class.new {
22 | def [](field)
23 | end
24 |
25 | def empty?
26 | true
27 | end
28 |
29 | def count
30 | 0
31 | end
32 |
33 | def full_messages
34 | []
35 | end
36 | }.new
37 | end
38 |
39 | def persisted?
40 | false
41 | end
42 | end
43 |
44 | Post = Struct.new(:id, :title, :author_name, :body, :category, :secret, :favs, :allow_comments, :splash, :persisted, :written_on, :cost, :admin, :author, :time_zone, :country, :weekday) do
45 | extend ActiveModel::Naming
46 | include ActiveModel::Conversion
47 | extend ActiveModel::Translation
48 |
49 | alias_method :secret?, :secret
50 | alias_method :persisted?, :persisted
51 |
52 | def initialize(*args)
53 | super
54 | @persisted = false
55 | end
56 |
57 | attr_accessor :author
58 | def author_attributes=(attributes)
59 | end
60 |
61 | attr_accessor :comments, :comment_ids
62 | def comments_attributes=(attributes)
63 | end
64 |
65 | attr_accessor :tags
66 | def tags_attributes=(attributes)
67 | end
68 |
69 | attr_accessor :time_zone
70 | end
71 |
72 | class PostDelegator < Post
73 | def to_model
74 | PostDelegate.new
75 | end
76 | end
77 |
78 | class PostDelegate < Post
79 | def self.human_attribute_name(attribute)
80 | "Delegate #{super}"
81 | end
82 |
83 | def model_name
84 | ActiveModel::Name.new(self.class)
85 | end
86 | end
87 |
88 | class Comment
89 | extend ActiveModel::Naming
90 | include ActiveModel::Conversion
91 |
92 | attr_accessor :id
93 | attr_reader :post_id
94 | def initialize(id = nil, post_id = nil, body = nil)
95 | @id, @post_id, @body = id, post_id, body
96 | end
97 |
98 | def to_key
99 | id ? [id] : nil
100 | end
101 |
102 | def save
103 | @id = 1
104 | @post_id = 1
105 | end
106 |
107 | def persisted?
108 | @id.present?
109 | end
110 |
111 | def to_param
112 | @id&.to_s
113 | end
114 |
115 | def name
116 | @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
117 | end
118 |
119 | attr_accessor :relevances
120 | def relevances_attributes=(attributes)
121 | end
122 |
123 | attr_accessor :body
124 | end
125 |
126 | class Tag
127 | extend ActiveModel::Naming
128 | include ActiveModel::Conversion
129 |
130 | attr_reader :id
131 | attr_reader :post_id
132 | def initialize(id = nil, post_id = nil)
133 | @id, @post_id = id, post_id
134 | end
135 |
136 | def to_key
137 | id ? [id] : nil
138 | end
139 |
140 | def save
141 | @id = 1
142 | @post_id = 1
143 | end
144 |
145 | def persisted?
146 | @id.present?
147 | end
148 |
149 | def to_param
150 | @id&.to_s
151 | end
152 |
153 | def value
154 | @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
155 | end
156 |
157 | attr_accessor :relevances
158 | def relevances_attributes=(attributes)
159 | end
160 | end
161 |
162 | class ActionView::TestCase
163 | def json
164 | @__json ||= Props::Template.new(self)
165 | end
166 |
167 | def render_js(str)
168 | str = <<-JS
169 | import * as React from 'react'
170 | import * as Server from 'react-dom/server'
171 | import Select from './components/Select'
172 | import CheckBox from './components/CheckBox'
173 | import CollectionCheckBoxes from './components/CollectionCheckBoxes'
174 | import CollectionRadioButtons from './components/CollectionRadioButtons'
175 |
176 | function out() {
177 | #{str}
178 | }
179 |
180 | console.log(Server.renderToString(out()))
181 | JS
182 |
183 | file = Tempfile.new(["test", ".js"], Dir.pwd)
184 | file.path
185 | file.write(str)
186 | file.close
187 |
188 | esbuild_cmd = "yarn run esbuild #{file.path} --bundle --loader:.js=jsx --outfile=build/out.js"
189 | _stdout, stderr, status = Open3.capture3(esbuild_cmd)
190 |
191 | if !status.success?
192 | raise stderr
193 | end
194 |
195 | stdout, stderr, status = Open3.capture3("node build/out.js")
196 |
197 | if !status.success?
198 | raise stderr
199 | end
200 |
201 | stdout.strip
202 | end
203 |
204 | RecordForm = Struct.new(:to_model, keyword_init: true)
205 | Routes = ActionDispatch::Routing::RouteSet.new
206 | Routes.draw do
207 | resources :posts do
208 | resources :comments
209 | end
210 |
211 | resources :continents
212 |
213 | namespace :admin do
214 | resources :posts do
215 | resources :comments
216 | end
217 | end
218 |
219 | get "/foo", to: "controller#action"
220 | root to: "main#index"
221 | end
222 |
223 | include Routes.url_helpers
224 |
225 | def url_for(object)
226 | @url_for_options = object
227 |
228 | if object.is_a?(Hash) && object[:use_route].blank? && object[:controller].blank?
229 | object[:controller] = "main"
230 | object[:action] = "index"
231 | object[:host] = "http://localhost:3000"
232 | end
233 |
234 | super
235 | end
236 | VALID_HTML_ID = /^[A-Za-z][-_:.A-Za-z0-9]*$/ # see http://www.w3.org/TR/html4/types.html#type-name
237 | def setup_test_fixture
238 | @__json ||= Props::Template.new(self)
239 | # @user = User.new(email: "steve@example.com")
240 | ActionView::Helpers::FormTagHelper.default_enforce_utf8 = true
241 | ActionView::Helpers::FormHelper.form_with_generates_ids = true
242 | # Create "label" locale for testing I18n label helpers
243 | I18n.backend.store_translations "label",
244 | activemodel: {
245 | attributes: {
246 | post: {
247 | cost: "Total cost"
248 | },
249 | "post/language": {
250 | spanish: "Espanol"
251 | }
252 | }
253 | },
254 | helpers: {
255 | label: {
256 | post: {
257 | body: "Write entire text here",
258 | color: {
259 | red: "Rojo"
260 | },
261 | comments: {
262 | body: "Write body here"
263 | }
264 | },
265 | tag: {
266 | value: "Tag"
267 | },
268 | post_delegate: {
269 | title: "Delegate model_name title"
270 | }
271 | }
272 | }
273 |
274 | # Create "submit" locale for testing I18n submit helpers
275 | I18n.backend.store_translations "submit",
276 | helpers: {
277 | submit: {
278 | create: "Create %{model}",
279 | update: "Confirm %{model} changes",
280 | submit: "Save changes",
281 | another_post: {
282 | update: "Update your %{model}"
283 | },
284 | "blog/post": {
285 | update: "Update your %{model}"
286 | }
287 | }
288 | }
289 |
290 | I18n.backend.store_translations "placeholder",
291 | activemodel: {
292 | attributes: {
293 | post: {
294 | cost: "Total cost"
295 | },
296 | "post/cost": {
297 | uk: "Pounds"
298 | }
299 | }
300 | },
301 | helpers: {
302 | placeholder: {
303 | post: {
304 | title: "What is this about?",
305 | written_on: {
306 | spanish: "Escrito en"
307 | },
308 | comments: {
309 | body: "Write body here"
310 | }
311 | },
312 | post_delegate: {
313 | title: "Delegate model_name title"
314 | },
315 | tag: {
316 | value: "Tag"
317 | }
318 | }
319 | }
320 |
321 | @post = Post.new
322 | @comment = Comment.new
323 | @post.instance_eval do
324 | def errors
325 | Class.new {
326 | def [](field)
327 | (field == "author_name") ? ["can't be empty"] : []
328 | end
329 |
330 | def empty?
331 | false
332 | end
333 |
334 | def count
335 | 1
336 | end
337 |
338 | def full_messages
339 | ["Author name can't be empty"]
340 | end
341 | }.new
342 | end
343 |
344 | def to_key
345 | [123]
346 | end
347 |
348 | def id
349 | 0
350 | end
351 |
352 | def id_before_type_cast
353 | "omg"
354 | end
355 |
356 | def id_came_from_user?
357 | true
358 | end
359 |
360 | def to_param
361 | "123"
362 | end
363 | end
364 |
365 | @post.persisted = true
366 | @post.admin = true
367 | @post.title = "Hello World"
368 | @post.author_name = ""
369 | @post.splash = ""
370 | @post.body = "Back to the hill and over it again!"
371 | @post.secret = 1
372 | @post.favs = 1
373 | @post.written_on = Date.new(2004, 6, 15)
374 |
375 | @post.comments = []
376 | @post.comments << @comment
377 |
378 | @post.tags = []
379 | @post.tags << Tag.new
380 |
381 | @post_delegator = PostDelegator.new
382 |
383 | @post_delegator.title = "Hello World"
384 |
385 | @controller.singleton_class.include Routes.url_helpers
386 | end
387 | end
388 |
389 | module Blog
390 | def self.use_relative_model_naming?
391 | true
392 | end
393 |
394 | Post = Struct.new(:title, :id) do
395 | extend ActiveModel::Naming
396 | include ActiveModel::Conversion
397 |
398 | def persisted?
399 | id.present?
400 | end
401 | end
402 | end
403 |
--------------------------------------------------------------------------------
/lib/form_props/form_builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FormProps
4 | class FormBuilder < ActionView::Helpers::FormBuilder
5 | undef_method :button,
6 | :label,
7 | :datetime_select,
8 | :time_select,
9 | :date_select
10 | # :rich_text_area
11 |
12 | def initialize(*args)
13 | super
14 | options = args.last || {}
15 | @default_options[:controlled] = options[:controlled]
16 | @default_html_options = @default_html_options.except(:controlled)
17 | end
18 |
19 | def file_field(method, options = {})
20 | Inputs::FileField.new(
21 | @object_name,
22 | method,
23 | @template,
24 | @template.send(:convert_direct_upload_option_to_url, objectify_options(options).dup)
25 | ).render
26 | end
27 |
28 | def select(method, choices = [], options = {}, html_options = {})
29 | Inputs::Select.new(@object_name, method, @template, choices, objectify_options(options), @default_html_options.merge(html_options)).render
30 | end
31 |
32 | def hidden_field(method, options = {})
33 | Inputs::HiddenField.new(@object_name, method, @template, objectify_options(options)).render
34 | end
35 |
36 | def text_field(method, options = {})
37 | Inputs::TextField.new(@object_name, method, @template, objectify_options(options)).render
38 | end
39 |
40 | def date_field(method, options = {})
41 | Inputs::DateField.new(@object_name, method, @template, objectify_options(options)).render
42 | end
43 |
44 | def time_field(method, options = {})
45 | Inputs::TimeField.new(@object_name, method, @template, objectify_options(options)).render
46 | end
47 |
48 | def week_field(method, options = {})
49 | Inputs::WeekField.new(@object_name, method, @template, objectify_options(options)).render
50 | end
51 |
52 | def month_field(method, options = {})
53 | Inputs::MonthField.new(@object_name, method, @template, objectify_options(options)).render
54 | end
55 |
56 | def datetime_field(method, options = {})
57 | Inputs::DatetimeLocalField.new(@object_name, method, @template, objectify_options(options)).render
58 | end
59 |
60 | def datetime_local_field(method, options = {})
61 | Inputs::DatetimeLocalField.new(@object_name, method, @template, objectify_options(options)).render
62 | end
63 |
64 | def url_field(method, options = {})
65 | Inputs::UrlField.new(@object_name, method, @template, objectify_options(options)).render
66 | end
67 |
68 | def tel_field(method, options = {})
69 | Inputs::TelField.new(@object_name, method, @template, objectify_options(options)).render
70 | end
71 |
72 | def color_field(method, options = {})
73 | Inputs::ColorField.new(@object_name, method, @template, objectify_options(options)).render
74 | end
75 |
76 | def password_field(method, options = {})
77 | Inputs::PasswordField.new(@object_name, method, @template, objectify_options(options)).render
78 | end
79 |
80 | def number_field(method, options = {})
81 | Inputs::NumberField.new(@object_name, method, @template, objectify_options(options)).render
82 | end
83 |
84 | def range_field(method, options = {})
85 | Inputs::RangeField.new(@object_name, method, @template, objectify_options(options)).render
86 | end
87 |
88 | def email_field(method, options = {})
89 | Inputs::EmailField.new(@object_name, method, @template, objectify_options(options)).render
90 | end
91 |
92 | def search_field(method, options = {})
93 | Inputs::SearchField.new(@object_name, method, @template, objectify_options(options)).render
94 | end
95 |
96 | def text_area(method, options = {})
97 | Inputs::TextArea.new(@object_name, method, @template, objectify_options(options)).render
98 | end
99 |
100 | def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
101 | Inputs::CheckBox.new(@object_name, method, @template, checked_value, unchecked_value, objectify_options(options)).render
102 | end
103 |
104 | def radio_button(method, tag_value, options = {})
105 | Inputs::RadioButton.new(@object_name, method, @template, tag_value, objectify_options(options)).render
106 | end
107 |
108 | def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
109 | Inputs::CollectionSelect.new(@object_name, method, @template, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options)).render
110 | end
111 |
112 | def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
113 | Inputs::CollectionCheckBoxes.new(@object_name, method, @template, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block).render
114 | end
115 |
116 | def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
117 | Inputs::CollectionRadioButtons.new(@object_name, method, @template, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block).render
118 | end
119 |
120 | def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
121 | Inputs::GroupedCollectionSelect.new(@object_name, method, @template, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_html_options.merge(html_options)).render
122 | end
123 |
124 | def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
125 | Inputs::TimeZoneSelect.new(@object_name, method, @template, priority_zones, objectify_options(options), @default_html_options.merge(html_options)).render
126 | end
127 |
128 | def weekday_select(method, options = {}, html_options = {})
129 | Inputs::WeekdaySelect.new(@object_name, method, @template, objectify_options(options), @default_html_options.merge(html_options)).render
130 | end
131 |
132 | def submit(value = nil, options = {})
133 | value, options = nil, value if value.is_a?(Hash)
134 | value ||= submit_default_value
135 | options = {name: "commit", text: value}.update(options)
136 |
137 | Inputs::Submit.new(@template, options).render
138 | end
139 |
140 | def fields_for_with_nested_attributes(association_name, association, options, block)
141 | name = "#{object_name}[#{association_name}_attributes]"
142 | association_key = "#{association_name.to_s.camelize(:lower)}Attributes"
143 | association = convert_to_model(association)
144 | json = @template.instance_variable_get(:@__json)
145 |
146 | if association.respond_to?(:persisted?)
147 | association = [association] if @object.public_send(association_name).respond_to?(:to_ary)
148 | elsif !association.respond_to?(:to_ary)
149 | association = @object.public_send(association_name)
150 | end
151 |
152 | if association.respond_to?(:to_ary)
153 | explicit_child_index = options[:child_index]
154 |
155 | json.set!(association_key) do
156 | json.array! association do |child|
157 | if explicit_child_index
158 | options[:child_index] = explicit_child_index.call if explicit_child_index.respond_to?(:call)
159 | else
160 | options[:child_index] = nested_child_index(name)
161 | end
162 |
163 | fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
164 | end
165 | end
166 | elsif association
167 | json.set!(association_key) do
168 | fields_for_nested_model(name, association, options, block)
169 | end
170 | end
171 | end
172 |
173 | def fields_for_nested_model(name, object, fields_options, block)
174 | object = convert_to_model(object)
175 | emit_hidden_id = object.persisted? && fields_options.fetch(:include_id) {
176 | options.fetch(:include_id, true)
177 | }
178 |
179 | @template.fields_for(name, object, fields_options) do |f|
180 | block.call(f)
181 | f.hidden_field(:id) if emit_hidden_id && !f.emitted_hidden_id?
182 | end
183 | end
184 |
185 | def fields_for(record_name, record_object = nil, fields_options = {}, &block)
186 | fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options?
187 | fields_options[:builder] ||= self.class
188 | fields_options[:namespace] = options[:namespace]
189 | fields_options[:parent_builder] = self
190 |
191 | case record_name
192 | when String, Symbol
193 | if nested_attributes_association?(record_name)
194 | return fields_for_with_nested_attributes(record_name, record_object, fields_options, block)
195 | end
196 | else
197 | record_object = record_name.is_a?(Array) ? record_name.last : record_name
198 | record_name = model_name_from_record_or_class(record_object).param_key
199 | end
200 |
201 | object_name = @object_name
202 | index = if options.has_key?(:index)
203 | options[:index]
204 | elsif defined?(@auto_index)
205 | object_name = object_name.to_s.delete_suffix("[]")
206 | @auto_index
207 | end
208 |
209 | record_name = if index
210 | "#{object_name}[#{index}][#{record_name}]"
211 | elsif record_name.end_with?("[]")
212 | "#{object_name}[#{record_name[0..-3]}][#{record_object.id}]"
213 | else
214 | "#{object_name}[#{record_name}]"
215 | end
216 | fields_options[:child_index] = index
217 |
218 | @template.fields_for(record_name, record_object, fields_options, &block)
219 | end
220 |
221 | def default_form_builder_class
222 | self.class
223 | end
224 | end
225 | end
226 |
--------------------------------------------------------------------------------
/test/inputs/collection_select_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class CollectionSelectTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_collection_select
9 | @post = Post.new
10 | @post.author_name = "Babe"
11 |
12 | form_props(model: @post) do |f|
13 | f.collection_select(:author_name, dummy_posts, "author_name", "author_name")
14 | end
15 | result = json.result!.strip
16 |
17 | expected = {
18 | "type" => "select",
19 | "name" => "post[author_name]",
20 | "id" => "post_author_name",
21 | "defaultValue" => "Babe",
22 | "options" => [
23 | {"value" => "", "label" => ""},
24 | {"value" => "Babe", "label" => "Babe"},
25 | {"value" => "Cabe", "label" => "Cabe"}
26 | ]
27 | }
28 | assert_equal(JSON.parse(result)["inputs"]["authorName"], expected)
29 | end
30 |
31 | def test_collection_select_under_fields_for
32 | @post = Post.new
33 | @post.author_name = "Babe"
34 |
35 | json.output do
36 | fields_for :post, @post, builder: FormProps::FormBuilder do |f|
37 | f.collection_select(:author_name, dummy_posts, :author_name, :author_name)
38 | end
39 | end
40 | result = json.result!.strip
41 |
42 | expected = {
43 | "type" => "select",
44 | "name" => "post[author_name]",
45 | "id" => "post_author_name",
46 | "defaultValue" => "Babe",
47 | "options" => [
48 | {"value" => "", "label" => ""},
49 | {"value" => "Babe", "label" => "Babe"},
50 | {"value" => "Cabe", "label" => "Cabe"}
51 | ]
52 | }
53 |
54 | assert_equal(JSON.parse(result)["output"]["authorName"], expected)
55 | end
56 |
57 | def test_collection_select_under_fields_for_with_index
58 | @post = Post.new
59 | @post.author_name = "Babe"
60 |
61 | json.output do
62 | fields_for :post, @post, index: 815, builder: FormProps::FormBuilder do |f|
63 | f.collection_select(:author_name, dummy_posts, :author_name, :author_name)
64 | end
65 | end
66 | result = json.result!.strip
67 |
68 | expected = {
69 | "type" => "select",
70 | "name" => "post[815][author_name]",
71 | "id" => "post_815_author_name",
72 | "defaultValue" => "Babe",
73 | "options" => [
74 | {"value" => "", "label" => ""},
75 | {"value" => "Babe", "label" => "Babe"},
76 | {"value" => "Cabe", "label" => "Cabe"}
77 | ]
78 | }
79 |
80 | assert_equal(JSON.parse(result)["output"]["authorName"], expected)
81 | end
82 |
83 | def test_collection_select_under_fields_for_with_auto_index
84 | @post = Post.new
85 | @post.author_name = "Babe"
86 | @post.instance_eval do
87 | def to_param
88 | 815
89 | end
90 | end
91 |
92 | json.output do
93 | fields_for "post[]", @post, builder: FormProps::FormBuilder do |f|
94 | f.collection_select(:author_name, dummy_posts, :author_name, :author_name)
95 | end
96 | end
97 | result = json.result!.strip
98 |
99 | expected = {
100 | "type" => "select",
101 | "name" => "post[815][author_name]",
102 | "id" => "post_815_author_name",
103 | "defaultValue" => "Babe",
104 | "options" => [
105 | {"value" => "", "label" => ""},
106 | {"value" => "Babe", "label" => "Babe"},
107 | {"value" => "Cabe", "label" => "Cabe"}
108 | ]
109 | }
110 |
111 | assert_equal(JSON.parse(result)["output"]["authorName"], expected)
112 | end
113 |
114 | def test_collection_select_with_blank_and_style
115 | @post = Post.new
116 | @post.author_name = "Babe"
117 |
118 | form_props(model: @post) do |f|
119 | f.collection_select(:author_name, dummy_posts, "author_name", "author_name", {include_blank: true}, {"style" => "width: 200px"})
120 | end
121 | result = json.result!.strip
122 |
123 | expected = {
124 | "type" => "select",
125 | "style" => "width: 200px",
126 | "name" => "post[author_name]",
127 | "id" => "post_author_name",
128 | "defaultValue" => "Babe",
129 | "options" => [
130 | {"value" => "", "label" => " "},
131 | {"value" => "", "label" => ""},
132 | {"value" => "Babe", "label" => "Babe"},
133 | {"value" => "Cabe", "label" => "Cabe"}
134 | ]
135 | }
136 | assert_equal(JSON.parse(result)["inputs"]["authorName"], expected)
137 | end
138 |
139 | def test_collection_select_with_blank_as_string_and_style
140 | @post = Post.new
141 | @post.author_name = "Babe"
142 |
143 | form_props(model: @post) do |f|
144 | f.collection_select(:author_name, dummy_posts, "author_name", "author_name", {include_blank: "No Selection"}, {"style" => "width: 200px"})
145 | end
146 | result = json.result!.strip
147 |
148 | expected = {
149 | "type" => "select",
150 | "style" => "width: 200px",
151 | "name" => "post[author_name]",
152 | "id" => "post_author_name",
153 | "defaultValue" => "Babe",
154 | "options" => [
155 | {"value" => "", "label" => "No Selection"},
156 | {"value" => "", "label" => ""},
157 | {"value" => "Babe", "label" => "Babe"},
158 | {"value" => "Cabe", "label" => "Cabe"}
159 | ]
160 | }
161 | assert_equal(JSON.parse(result)["inputs"]["authorName"], expected)
162 | end
163 |
164 | def test_collection_select_with_multiple_option_appends_array_brackets_and_hidden_input
165 | @post = Post.new
166 | @post.author_name = "Babe"
167 |
168 | # Should suffix default name with [].
169 | form_props(model: @post) do |f|
170 | f.collection_select(:author_name, dummy_posts, "author_name", "author_name", {include_blank: true}, {multiple: true})
171 | end
172 | result = json.result!.strip
173 |
174 | expected = {
175 | "type" => "select",
176 | "multiple" => true,
177 | "name" => "post[author_name][]",
178 | "id" => "post_author_name",
179 | "defaultValue" => ["Babe"],
180 | "options" => [
181 | {"value" => "", "label" => " "},
182 | {"value" => "", "label" => ""},
183 | {"value" => "Babe", "label" => "Babe"},
184 | {"value" => "Cabe", "label" => "Cabe"}
185 | ]
186 | }
187 | assert_equal(JSON.parse(result)["inputs"]["authorName"], expected)
188 |
189 | # Shouldn't suffix custom name with [].
190 | form_props(model: @post) do |f|
191 | f.collection_select(:author_name, dummy_posts, "author_name", "author_name", {include_blank: true, name: "post[author_name][]"}, {multiple: true})
192 | end
193 | result = json.result!.strip
194 |
195 | expected = {
196 | "type" => "select",
197 | "multiple" => true,
198 | "name" => "post[author_name][]",
199 | "id" => "post_author_name",
200 | "defaultValue" => ["Babe"],
201 | "options" => [
202 | {"value" => "", "label" => " "},
203 | {"value" => "", "label" => ""},
204 | {"value" => "Babe", "label" => "Babe"},
205 | {"value" => "Cabe", "label" => "Cabe"}
206 | ]
207 | }
208 | assert_equal(JSON.parse(result)["inputs"]["authorName"], expected)
209 | end
210 |
211 | def test_collection_select_with_blank_and_selected
212 | @post = Post.new
213 | @post.author_name = "Babe"
214 |
215 | form_props(model: @post) do |f|
216 | f.collection_select(:author_name, dummy_posts, "author_name", "author_name", include_blank: true, selected: "")
217 | end
218 | result = json.result!.strip
219 |
220 | expected = {
221 | "type" => "select",
222 | "name" => "post[author_name]",
223 | "id" => "post_author_name",
224 | "defaultValue" => "",
225 | "options" => [
226 | {"value" => "", "label" => " "},
227 | {"value" => "", "label" => ""},
228 | {"value" => "Babe", "label" => "Babe"},
229 | {"value" => "Cabe", "label" => "Cabe"}
230 | ]
231 | }
232 | assert_equal(JSON.parse(result)["inputs"]["authorName"], expected)
233 | end
234 |
235 | def test_collection_select_with_disabled
236 | @post = Post.new
237 | @post.author_name = "Babe"
238 |
239 | form_props(model: @post) do |f|
240 | f.collection_select(:author_name, dummy_posts, "author_name", "author_name", disabled: "Cabe")
241 | end
242 | result = json.result!.strip
243 |
244 | expected = {
245 | "type" => "select",
246 | "name" => "post[author_name]",
247 | "id" => "post_author_name",
248 | "defaultValue" => "Babe",
249 | "options" => [
250 | {"value" => "", "label" => ""},
251 | {"value" => "Babe", "label" => "Babe"},
252 | {"value" => "Cabe", "label" => "Cabe", "disabled" => true}
253 | ]
254 | }
255 | assert_equal(JSON.parse(result)["inputs"]["authorName"], expected)
256 | end
257 |
258 | def test_collection_select_with_proc_for_value_method
259 | @post = Post.new
260 |
261 | form_props(model: @post) do |f|
262 | f.collection_select(:author_name, dummy_posts, lambda { |p| p.author_name }, "title")
263 | end
264 | result = json.result!.strip
265 |
266 | expected = {
267 | "type" => "select",
268 | "name" => "post[author_name]",
269 | "id" => "post_author_name",
270 | "options" => [
271 | {"value" => "", "label" => " went home"},
272 | {"value" => "Babe", "label" => "Babe went home"},
273 | {"value" => "Cabe", "label" => "Cabe went home"}
274 | ]
275 | }
276 | assert_equal(JSON.parse(result)["inputs"]["authorName"], expected)
277 | end
278 |
279 | def test_collection_select_with_proc_for_text_method
280 | @post = Post.new
281 |
282 | form_props(model: @post) do |f|
283 | f.collection_select(:author_name, dummy_posts, "author_name", lambda { |p| p.title })
284 | end
285 | result = json.result!.strip
286 |
287 | expected = {
288 | "type" => "select",
289 | "name" => "post[author_name]",
290 | "id" => "post_author_name",
291 | "options" => [
292 | {"value" => "", "label" => " went home"},
293 | {"value" => "Babe", "label" => "Babe went home"},
294 | {"value" => "Cabe", "label" => "Cabe went home"}
295 | ]
296 | }
297 | assert_equal(JSON.parse(result)["inputs"]["authorName"], expected)
298 | end
299 |
300 | private
301 |
302 | def dummy_posts
303 | [Post.new(1, " went home", "", "To a little house", "shh!"),
304 | Post.new(2, "Babe went home", "Babe", "To a little house", "shh!"),
305 | Post.new(3, "Cabe went home", "Cabe", "To a little house", "shh!")]
306 | end
307 | end
308 |
--------------------------------------------------------------------------------
/test/inputs/text_area_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class TextAreaTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_text_area_placeholder_without_locales
9 | @post = Post.new
10 | @post.body = "Back to the hill and over it again!"
11 |
12 | I18n.with_locale :placeholder do
13 | form_props(model: @post) do |f|
14 | f.text_area(:body, placeholder: true)
15 | end
16 | end
17 | result = json.result!.strip
18 |
19 | expected = {
20 | "type" => "textarea",
21 | "name" => "post[body]",
22 | "placeholder" => "Body",
23 | "id" => "post_body",
24 | "defaultValue" => "Back to the hill and over it again!"
25 | }
26 |
27 | assert_equal(JSON.parse(result)["inputs"]["body"], expected)
28 | end
29 |
30 | def test_text_area_placeholder_with_locales
31 | @post = Post.new
32 | @post.title = "Hello World"
33 |
34 | I18n.with_locale :placeholder do
35 | form_props(model: @post) do |f|
36 | f.text_area(:title, placeholder: true)
37 | end
38 | end
39 | result = json.result!.strip
40 |
41 | expected = {
42 | "type" => "textarea",
43 | "name" => "post[title]",
44 | "placeholder" => "What is this about?",
45 | "id" => "post_title",
46 | "defaultValue" => "Hello World"
47 | }
48 |
49 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
50 | end
51 |
52 | def test_text_area_placeholder_with_human_attribute_name
53 | @post = Post.new
54 |
55 | I18n.with_locale :placeholder do
56 | form_props(model: @post) do |f|
57 | f.text_area(:cost, placeholder: true)
58 | end
59 | end
60 | result = json.result!.strip
61 |
62 | expected = {
63 | "type" => "textarea",
64 | "name" => "post[cost]",
65 | "placeholder" => "Total cost",
66 | "id" => "post_cost"
67 | }
68 |
69 | assert_equal(JSON.parse(result)["inputs"]["cost"], expected)
70 | end
71 |
72 | def test_text_area_placeholder_with_string_value
73 | @post = Post.new
74 |
75 | I18n.with_locale :placeholder do
76 | form_props(model: @post) do |f|
77 | f.text_area(:cost, placeholder: "HOW MUCH?")
78 | end
79 | end
80 | result = json.result!.strip
81 |
82 | expected = {
83 | "type" => "textarea",
84 | "name" => "post[cost]",
85 | "placeholder" => "HOW MUCH?",
86 | "id" => "post_cost"
87 | }
88 |
89 | assert_equal(JSON.parse(result)["inputs"]["cost"], expected)
90 | end
91 |
92 | def test_text_area_placeholder_with_human_attribute_name_and_value
93 | @post = Post.new
94 |
95 | I18n.with_locale :placeholder do
96 | form_props(model: @post) do |f|
97 | f.text_area(:cost, placeholder: :uk)
98 | end
99 | end
100 | result = json.result!.strip
101 |
102 | expected = {
103 | "type" => "textarea",
104 | "name" => "post[cost]",
105 | "placeholder" => "Pounds",
106 | "id" => "post_cost"
107 | }
108 |
109 | assert_equal(JSON.parse(result)["inputs"]["cost"], expected)
110 | end
111 |
112 | def test_text_area_placeholder_with_locales_and_value
113 | @post = Post.new
114 |
115 | I18n.with_locale :placeholder do
116 | form_props(model: @post) do |f|
117 | f.text_area(:written_on, placeholder: :spanish)
118 | end
119 | end
120 | result = json.result!.strip
121 |
122 | expected = {
123 | "type" => "textarea",
124 | "name" => "post[written_on]",
125 | "placeholder" => "Escrito en",
126 | "id" => "post_written_on"
127 | }
128 |
129 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
130 | end
131 |
132 | def test_text_area_placeholder_with_locales_and_nested_attributes
133 | I18n.with_locale :placeholder do
134 | form_props(model: @post, method: :patch) do |f|
135 | f.fields_for(:comments) do |cf|
136 | cf.text_area(:body, placeholder: true)
137 | end
138 | end
139 | end
140 | result = json.result!.strip
141 |
142 | expected = {
143 | "inputs" => {
144 | "commentsAttributes" => [
145 | {
146 | "body" => {
147 | "placeholder" => "Write body here",
148 | "type" => "textarea",
149 | "name" => "post[comments_attributes][0][body]",
150 | "id" => "post_comments_attributes_0_body"
151 | }
152 | }
153 | ]
154 | },
155 | "extras" => {
156 | "utf8" => {"name" => "utf8", "type" => "hidden", "defaultValue" => "✓", "autoComplete" => "off"},
157 | "method" => {"name" => "_method", "type" => "hidden", "defaultValue" => "patch", "autoComplete" => "off"}
158 | },
159 | "form" => {"action" => "/posts/123", "acceptCharset" => "UTF-8", "method" => "post"}
160 | }
161 |
162 | assert_equal(JSON.parse(result), expected)
163 | end
164 |
165 | def test_text_area_placeholder_with_locales_fallback_and_nested_attributes
166 | I18n.with_locale :placeholder do
167 | form_props(model: @post, method: :patch) do |f|
168 | f.fields_for(:tags) do |cf|
169 | cf.text_area(:value, placeholder: true)
170 | end
171 | end
172 | end
173 | result = json.result!.strip
174 |
175 | expected = {
176 | "inputs" => {
177 | "tagsAttributes" => [
178 | {
179 | "value" => {
180 | "placeholder" => "Tag",
181 | "type" => "textarea",
182 | "defaultValue" => "new tag",
183 | "name" => "post[tags_attributes][0][value]",
184 | "id" => "post_tags_attributes_0_value"
185 | }
186 | }
187 | ]
188 | },
189 | "extras" => {
190 | "utf8" => {"name" => "utf8", "type" => "hidden", "defaultValue" => "✓", "autoComplete" => "off"},
191 | "method" => {"name" => "_method", "type" => "hidden", "defaultValue" => "patch", "autoComplete" => "off"}
192 | },
193 | "form" => {"action" => "/posts/123", "acceptCharset" => "UTF-8", "method" => "post"}
194 | }
195 |
196 | assert_equal(JSON.parse(result), expected)
197 | end
198 |
199 | def test_text_area
200 | form_props(model: @post) do |f|
201 | f.text_area(:title)
202 | end
203 | result = json.result!.strip
204 |
205 | expected = {
206 | "type" => "textarea",
207 | "name" => "post[title]",
208 | "id" => "post_title",
209 | "defaultValue" => "Hello World"
210 | }
211 |
212 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
213 | end
214 |
215 | def test_text_area_with_escapes
216 | @post.title = "Back to the hill and over it again!"
217 | form_props(model: @post) do |f|
218 | f.text_area(:title)
219 | end
220 | result = json.result!.strip
221 |
222 | expected = {
223 | "type" => "textarea",
224 | "name" => "post[title]",
225 | "id" => "post_title",
226 | "defaultValue" => "Back to the hill and over it again!"
227 | }
228 |
229 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
230 | end
231 |
232 | def test_text_area_with_alternate_value
233 | form_props(model: @post) do |f|
234 | f.text_area(:title, value: "Testing alternate values.")
235 | end
236 | result = json.result!.strip
237 |
238 | expected = {
239 | "type" => "textarea",
240 | "name" => "post[title]",
241 | "id" => "post_title",
242 | "defaultValue" => "Testing alternate values."
243 | }
244 |
245 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
246 | end
247 |
248 | def test_text_area_with_nil_alternate_value
249 | form_props(model: @post) do |f|
250 | f.text_area(:title, value: nil)
251 | end
252 | result = json.result!.strip
253 |
254 | expected = {
255 | "type" => "textarea",
256 | "name" => "post[title]",
257 | "id" => "post_title"
258 | }
259 |
260 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
261 | end
262 |
263 | def test_text_area_with_size_option
264 | form_props(model: @post) do |f|
265 | f.text_area(:title, size: "183x820")
266 | end
267 | result = json.result!.strip
268 |
269 | expected = {
270 | "type" => "textarea",
271 | "name" => "post[title]",
272 | "cols" => "183",
273 | "rows" => "820",
274 | "id" => "post_title",
275 | "defaultValue" => "Hello World"
276 | }
277 |
278 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
279 | end
280 |
281 | def test_text_area_tag_size_string
282 | form_props(model: @post) do |f|
283 | f.text_area(:title, "size" => "20x40")
284 | end
285 | result = json.result!.strip
286 |
287 | expected = {
288 | "type" => "textarea",
289 | "name" => "post[title]",
290 | "cols" => "20",
291 | "rows" => "40",
292 | "id" => "post_title",
293 | "defaultValue" => "Hello World"
294 | }
295 |
296 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
297 | end
298 |
299 | def test_text_area_tag_should_disregard_size_if_its_given_as_an_integer
300 | form_props(model: @post) do |f|
301 | f.text_area(:title, size: 20)
302 | end
303 | result = json.result!.strip
304 |
305 | expected = {
306 | "type" => "textarea",
307 | "name" => "post[title]",
308 | "id" => "post_title",
309 | "defaultValue" => "Hello World"
310 | }
311 |
312 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
313 | end
314 |
315 | def test_text_area_tag_unescaped_content
316 | @post.title = "hello world"
317 |
318 | form_props(model: @post) do |f|
319 | f.text_area(:title, size: "20x40")
320 | end
321 | result = json.result!.strip
322 |
323 | expected = {
324 | "type" => "textarea",
325 | "name" => "post[title]",
326 | "id" => "post_title",
327 | "cols" => "20",
328 | "rows" => "40",
329 | "defaultValue" => "hello world"
330 | }
331 |
332 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
333 | end
334 |
335 | def test_text_area_tag_unescaped_nil_content
336 | form_props(model: @post) do |f|
337 | f.text_area(:title, value: nil)
338 | end
339 | result = json.result!.strip
340 |
341 | expected = {
342 | "type" => "textarea",
343 | "name" => "post[title]",
344 | "id" => "post_title"
345 | }
346 |
347 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
348 | end
349 |
350 | def test_text_area_tag_options_symbolize_keys_side_effects
351 | options = {option: "random_option"}
352 |
353 | form_props(model: @post) do |f|
354 | f.text_area(:title, options)
355 | end
356 |
357 | assert_equal({option: "random_option"}, options)
358 | end
359 | end
360 |
--------------------------------------------------------------------------------
/test/inputs/text_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class TextFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_text_field_placeholder_without_locales
9 | @post = Post.new
10 | @post.body = "Back to the hill and over it again!"
11 |
12 | I18n.with_locale :placeholder do
13 | form_props(model: @post) do |f|
14 | f.text_field(:body, placeholder: true)
15 | end
16 | end
17 | result = json.result!.strip
18 |
19 | expected = {
20 | "type" => "text",
21 | "name" => "post[body]",
22 | "placeholder" => "Body",
23 | "id" => "post_body",
24 | "defaultValue" => "Back to the hill and over it again!"
25 | }
26 |
27 | assert_equal(JSON.parse(result)["inputs"]["body"], expected)
28 | end
29 |
30 | def test_text_field_placeholder_with_locales
31 | @post = Post.new
32 | @post.title = "Hello World"
33 |
34 | I18n.with_locale :placeholder do
35 | form_props(model: @post) do |f|
36 | f.text_field(:title, placeholder: true)
37 | end
38 | end
39 | result = json.result!.strip
40 |
41 | expected = {
42 | "type" => "text",
43 | "name" => "post[title]",
44 | "placeholder" => "What is this about?",
45 | "id" => "post_title",
46 | "defaultValue" => "Hello World"
47 | }
48 |
49 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
50 | end
51 |
52 | def test_text_field_placeholder_with_human_attribute_name
53 | @post = Post.new
54 |
55 | I18n.with_locale :placeholder do
56 | form_props(model: @post) do |f|
57 | f.text_field(:cost, placeholder: true)
58 | end
59 | end
60 | result = json.result!.strip
61 |
62 | expected = {
63 | "type" => "text",
64 | "name" => "post[cost]",
65 | "placeholder" => "Total cost",
66 | "id" => "post_cost"
67 | }
68 |
69 | assert_equal(JSON.parse(result)["inputs"]["cost"], expected)
70 | end
71 |
72 | def test_text_field_placeholder_with_string_value
73 | @post = Post.new
74 |
75 | I18n.with_locale :placeholder do
76 | form_props(model: @post) do |f|
77 | f.text_field(:cost, placeholder: "HOW MUCH?")
78 | end
79 | end
80 | result = json.result!.strip
81 |
82 | expected = {
83 | "type" => "text",
84 | "name" => "post[cost]",
85 | "placeholder" => "HOW MUCH?",
86 | "id" => "post_cost"
87 | }
88 |
89 | assert_equal(JSON.parse(result)["inputs"]["cost"], expected)
90 | end
91 |
92 | def test_text_field_placeholder_with_human_attribute_name_and_value
93 | @post = Post.new
94 |
95 | I18n.with_locale :placeholder do
96 | form_props(model: @post) do |f|
97 | f.text_field(:cost, placeholder: :uk)
98 | end
99 | end
100 | result = json.result!.strip
101 |
102 | expected = {
103 | "type" => "text",
104 | "name" => "post[cost]",
105 | "placeholder" => "Pounds",
106 | "id" => "post_cost"
107 | }
108 |
109 | assert_equal(JSON.parse(result)["inputs"]["cost"], expected)
110 | end
111 |
112 | def test_text_field_placeholder_with_locales_and_value
113 | @post = Post.new
114 |
115 | I18n.with_locale :placeholder do
116 | form_props(model: @post) do |f|
117 | f.text_field(:written_on, placeholder: :spanish)
118 | end
119 | end
120 | result = json.result!.strip
121 |
122 | expected = {
123 | "type" => "text",
124 | "name" => "post[written_on]",
125 | "placeholder" => "Escrito en",
126 | "id" => "post_written_on"
127 | }
128 |
129 | assert_equal(JSON.parse(result)["inputs"]["writtenOn"], expected)
130 | end
131 |
132 | def test_text_field_placeholder_with_locales_and_nested_attributes
133 | I18n.with_locale :placeholder do
134 | form_props(model: @post, method: :patch) do |f|
135 | f.fields_for(:comments) do |cf|
136 | cf.text_field(:body, placeholder: true)
137 | end
138 | end
139 | end
140 | result = json.result!.strip
141 |
142 | expected = {
143 | "inputs" => {
144 | "commentsAttributes" => [
145 | {
146 | "body" => {
147 | "placeholder" => "Write body here",
148 | "type" => "text",
149 | "name" => "post[comments_attributes][0][body]",
150 | "id" => "post_comments_attributes_0_body"
151 | }
152 | }
153 | ]
154 | },
155 | "extras" => {
156 | "utf8" => {"name" => "utf8", "type" => "hidden", "defaultValue" => "✓", "autoComplete" => "off"},
157 | "method" => {"name" => "_method", "type" => "hidden", "defaultValue" => "patch", "autoComplete" => "off"}
158 | },
159 | "form" => {"action" => "/posts/123", "acceptCharset" => "UTF-8", "method" => "post"}
160 | }
161 |
162 | assert_equal(JSON.parse(result), expected)
163 | end
164 |
165 | def test_text_field_placeholder_with_locales_fallback_and_nested_attributes
166 | I18n.with_locale :placeholder do
167 | form_props(model: @post, method: :patch) do |f|
168 | f.fields_for(:tags) do |cf|
169 | cf.text_field(:value, placeholder: true)
170 | end
171 | end
172 | end
173 | result = json.result!.strip
174 |
175 | expected = {
176 | "inputs" => {
177 | "tagsAttributes" => [
178 | {
179 | "value" => {
180 | "placeholder" => "Tag",
181 | "type" => "text",
182 | "defaultValue" => "new tag",
183 | "name" => "post[tags_attributes][0][value]",
184 | "id" => "post_tags_attributes_0_value"
185 | }
186 | }
187 | ]
188 | },
189 | "extras" => {
190 | "utf8" => {"name" => "utf8", "type" => "hidden", "defaultValue" => "✓", "autoComplete" => "off"},
191 | "method" => {"name" => "_method", "type" => "hidden", "defaultValue" => "patch", "autoComplete" => "off"}
192 | },
193 | "form" => {"action" => "/posts/123", "acceptCharset" => "UTF-8", "method" => "post"}
194 | }
195 |
196 | assert_equal(JSON.parse(result), expected)
197 | end
198 |
199 | def test_text_field
200 | form_props(model: @post) do |f|
201 | f.text_field(:title)
202 | end
203 | result = json.result!.strip
204 |
205 | expected = {
206 | "type" => "text",
207 | "name" => "post[title]",
208 | "id" => "post_title",
209 | "defaultValue" => "Hello World"
210 | }
211 |
212 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
213 | end
214 |
215 | def test_text_field_with_options
216 | form_props(model: @post) do |f|
217 | f.text_field(:title, size: 35)
218 | end
219 | result = json.result!.strip
220 |
221 | expected = {
222 | "type" => "text",
223 | "name" => "post[title]",
224 | "size" => 35,
225 | "id" => "post_title",
226 | "defaultValue" => "Hello World"
227 | }
228 |
229 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
230 |
231 | form_props(model: @post) do |f|
232 | f.text_field(:title, "size" => 35)
233 | end
234 | result = json.result!.strip
235 |
236 | expected = {
237 | "type" => "text",
238 | "name" => "post[title]",
239 | "size" => 35,
240 | "id" => "post_title",
241 | "defaultValue" => "Hello World"
242 | }
243 |
244 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
245 | end
246 |
247 | def test_text_field_assuming_size
248 | form_props(model: @post) do |f|
249 | f.text_field(:title, max_length: 35)
250 | end
251 | result = json.result!.strip
252 |
253 | expected = {
254 | "type" => "text",
255 | "name" => "post[title]",
256 | "maxLength" => 35,
257 | "size" => 35,
258 | "id" => "post_title",
259 | "defaultValue" => "Hello World"
260 | }
261 |
262 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
263 |
264 | form_props(model: @post) do |f|
265 | f.text_field(:title, "max_length" => 35)
266 | end
267 | result = json.result!.strip
268 |
269 | expected = {
270 | "type" => "text",
271 | "name" => "post[title]",
272 | "maxLength" => 35,
273 | "size" => 35,
274 | "id" => "post_title",
275 | "defaultValue" => "Hello World"
276 | }
277 |
278 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
279 | end
280 |
281 | def test_text_field_removing_size
282 | form_props(model: @post) do |f|
283 | f.text_field(:title, max_length: 35, size: nil)
284 | end
285 | result = json.result!.strip
286 |
287 | expected = {
288 | "type" => "text",
289 | "name" => "post[title]",
290 | "maxLength" => 35,
291 | "id" => "post_title",
292 | "defaultValue" => "Hello World"
293 | }
294 |
295 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
296 |
297 | form_props(model: @post) do |f|
298 | f.text_field(:title, "max_length" => 35, "size" => nil)
299 | end
300 | result = json.result!.strip
301 |
302 | expected = {
303 | "type" => "text",
304 | "name" => "post[title]",
305 | "maxLength" => 35,
306 | "id" => "post_title",
307 | "defaultValue" => "Hello World"
308 | }
309 |
310 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
311 | end
312 |
313 | def test_text_field_with_nil_value
314 | form_props(model: @post) do |f|
315 | f.text_field(:title, value: nil)
316 | end
317 | result = json.result!.strip
318 |
319 | expected = {
320 | "type" => "text",
321 | "name" => "post[title]",
322 | "id" => "post_title"
323 | }
324 |
325 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
326 | end
327 |
328 | def test_text_field_with_nil_name
329 | form_props(model: @post) do |f|
330 | f.text_field(:title, name: nil)
331 | end
332 | result = json.result!.strip
333 |
334 | expected = {
335 | "type" => "text",
336 | "id" => "post_title",
337 | "defaultValue" => "Hello World"
338 | }
339 |
340 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
341 | end
342 |
343 | def test_text_field_tag
344 | form_props(model: @post) do |f|
345 | f.text_field(:title, value: "Hello")
346 | end
347 | result = json.result!.strip
348 |
349 | expected = {
350 | "type" => "text",
351 | "id" => "post_title",
352 | "name" => "post[title]",
353 | "defaultValue" => "Hello"
354 | }
355 |
356 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
357 | end
358 |
359 | def test_text_field_tag_class_string
360 | form_props(model: @post) do |f|
361 | f.text_field(:title, value: "Hello", class_name: "admin")
362 | end
363 | result = json.result!.strip
364 |
365 | expected = {
366 | "type" => "text",
367 | "id" => "post_title",
368 | "name" => "post[title]",
369 | "className" => "admin",
370 | "defaultValue" => "Hello"
371 | }
372 |
373 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
374 | end
375 |
376 | def test_text_field_tag_with_ac_parameters
377 | form_props(model: @post) do |f|
378 | f.text_field(:title, value: ActionController::Parameters.new(key: "value"))
379 | end
380 | result = json.result!.strip
381 |
382 | expected = {
383 | "type" => "text",
384 | "id" => "post_title",
385 | "name" => "post[title]",
386 | "defaultValue" => "{\"key\"=>\"value\"}"
387 | }
388 |
389 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
390 | end
391 |
392 | def test_text_field_tag_disabled
393 | form_props(model: @post) do |f|
394 | f.text_field(:title, value: "Hello!", disabled: true)
395 | end
396 | result = json.result!.strip
397 |
398 | expected = {
399 | "type" => "text",
400 | "id" => "post_title",
401 | "name" => "post[title]",
402 | "disabled" => true,
403 | "defaultValue" => "Hello!"
404 | }
405 |
406 | assert_equal(JSON.parse(result)["inputs"]["title"], expected)
407 | end
408 | end
409 |
--------------------------------------------------------------------------------
/test/form_props_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "./test_helper"
2 |
3 | class FormPropsTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def url_for(options)
9 | if options.is_a?(Hash)
10 | "http://www.example.com"
11 | else
12 | super
13 | end
14 | end
15 |
16 | def test_form_with_multipart
17 | form_props(multipart: true)
18 |
19 | expected = {
20 | inputs: {},
21 | extras: {
22 | utf8: {
23 | name: "utf8",
24 | type: "hidden",
25 | defaultValue: "✓",
26 | autoComplete: "off"
27 | }
28 | },
29 | form: {
30 | encType: "multipart/form-data",
31 | action: "http://www.example.com",
32 | acceptCharset: "UTF-8",
33 | method: "post"
34 | }
35 | }.to_json
36 |
37 | result = json.result!.strip
38 |
39 | assert_equal(result, expected)
40 | end
41 |
42 | def test_form_with_with_method_patch
43 | form_props(method: :patch)
44 |
45 | expected = {
46 | inputs: {},
47 | extras: {
48 | method: {
49 | name: "_method",
50 | type: "hidden",
51 | defaultValue: "patch",
52 | autoComplete: "off"
53 | },
54 | utf8: {
55 | name: "utf8",
56 | type: "hidden",
57 | defaultValue: "✓",
58 | autoComplete: "off"
59 | }
60 | },
61 | form: {
62 | action: "http://www.example.com",
63 | acceptCharset: "UTF-8",
64 | method: "post"
65 | }
66 | }.to_json
67 |
68 | result = json.result!.strip
69 |
70 | assert_equal(result, expected)
71 | end
72 |
73 | def test_form_with_with_method_put
74 | form_props(method: :put)
75 |
76 | expected = {
77 | inputs: {},
78 | extras: {
79 | method: {
80 | name: "_method",
81 | type: "hidden",
82 | defaultValue: "put",
83 | autoComplete: "off"
84 | },
85 | utf8: {
86 | name: "utf8",
87 | type: "hidden",
88 | defaultValue: "✓",
89 | autoComplete: "off"
90 | }
91 | },
92 | form: {
93 | action: "http://www.example.com",
94 | acceptCharset: "UTF-8",
95 | method: "post"
96 | }
97 | }.to_json
98 |
99 | result = json.result!.strip
100 |
101 | assert_equal(result, expected)
102 | end
103 |
104 | def test_form_with_with_method_delete
105 | form_props(method: :delete)
106 |
107 | expected = {
108 | inputs: {},
109 | extras: {
110 | method: {
111 | name: "_method",
112 | type: "hidden",
113 | defaultValue: "delete",
114 | autoComplete: "off"
115 | },
116 | utf8: {
117 | name: "utf8",
118 | type: "hidden",
119 | defaultValue: "✓",
120 | autoComplete: "off"
121 | }
122 | },
123 | form: {
124 | action: "http://www.example.com",
125 | acceptCharset: "UTF-8",
126 | method: "post"
127 | }
128 | }.to_json
129 |
130 | result = json.result!.strip
131 | assert_equal(result, expected)
132 | end
133 |
134 | def test_form_with_false_url
135 | form_props(url: false)
136 | expected = {
137 | inputs: {},
138 | extras: {
139 | utf8: {
140 | name: "utf8",
141 | type: "hidden",
142 | defaultValue: "✓",
143 | autoComplete: "off"
144 | }
145 | },
146 | form: {
147 | acceptCharset: "UTF-8",
148 | method: "post"
149 | }
150 | }.to_json
151 |
152 | result = json.result!.strip
153 | assert_equal(result, expected)
154 | end
155 |
156 | def test_form_with_false_action
157 | form_props(html: {action: false})
158 |
159 | expected = {
160 | inputs: {},
161 | extras: {
162 | utf8: {
163 | name: "utf8",
164 | type: "hidden",
165 | defaultValue: "✓",
166 | autoComplete: "off"
167 | }
168 | },
169 | form: {
170 | acceptCharset: "UTF-8",
171 | method: "post"
172 | }
173 | }.to_json
174 |
175 | result = json.result!.strip
176 | assert_equal(result, expected)
177 | end
178 |
179 | def test_form_with_skip_enforcing_utf8_true
180 | form_props(skip_enforcing_utf8: true)
181 |
182 | expected = {
183 | inputs: {},
184 | extras: {},
185 | form: {
186 | action: "http://www.example.com",
187 | acceptCharset: "UTF-8",
188 | method: "post"
189 | }
190 | }.to_json
191 |
192 | result = json.result!.strip
193 | assert_equal(result, expected)
194 | end
195 |
196 | def test_form_with_default_enforce_utf8_false
197 | with_default_enforce_utf8 false do
198 | form_props
199 | expected = {
200 | inputs: {},
201 | extras: {},
202 | form: {
203 | action: "http://www.example.com",
204 | acceptCharset: "UTF-8",
205 | method: "post"
206 | }
207 | }.to_json
208 |
209 | result = json.result!.strip
210 | assert_equal(result, expected)
211 | end
212 | end
213 |
214 | def test_form_with_default_enforce_utf8_true
215 | with_default_enforce_utf8 true do
216 | form_props
217 |
218 | expected = {
219 | inputs: {},
220 | extras: {
221 | utf8: {
222 | name: "utf8",
223 | type: "hidden",
224 | defaultValue: "✓",
225 | autoComplete: "off"
226 | }
227 | },
228 | form: {
229 | action: "http://www.example.com",
230 | acceptCharset: "UTF-8",
231 | method: "post"
232 | }
233 | }.to_json
234 |
235 | result = json.result!.strip
236 | assert_equal(result, expected)
237 | end
238 | end
239 |
240 | def test_form_with
241 | form_props(model: @post, id: "create-post") do |f|
242 | f.text_field(:title)
243 | f.text_area(:body)
244 | f.check_box(:secret)
245 | f.select(:category, %w[animal economy sports])
246 | end
247 |
248 | result = json.result!.strip
249 |
250 | expected = {
251 | inputs: {
252 | title: {
253 | type: "text",
254 | defaultValue: "Hello World",
255 | name: "post[title]",
256 | id: "post_title"
257 | },
258 | body: {
259 | name: "post[body]",
260 | id: "post_body",
261 | type: "textarea",
262 | defaultValue: "Back to the hill and over it again!"
263 | },
264 | secret: {
265 | type: "checkbox",
266 | value: "1",
267 | defaultChecked: true,
268 | uncheckedValue: "0",
269 | includeHidden: true,
270 | name: "post[secret]",
271 | id: "post_secret"
272 | },
273 | category: {
274 | name: "post[category]",
275 | id: "post_category",
276 | type: "select",
277 | options: [
278 | {value: "animal", label: "animal"},
279 | {value: "economy", label: "economy"},
280 | {value: "sports", label: "sports"}
281 | ]
282 | }
283 | },
284 | extras: {
285 | method: {
286 | name: "_method",
287 | type: "hidden",
288 | defaultValue: "patch",
289 | autoComplete: "off"
290 | },
291 | utf8: {
292 | name: "utf8",
293 | type: "hidden",
294 | defaultValue: "\u0026#x2713;",
295 | autoComplete: "off"
296 | }
297 | },
298 | form: {
299 | id: "create-post",
300 | action: "/posts/123",
301 | acceptCharset: "UTF-8",
302 | method: "post"
303 | }
304 | }.to_json
305 | assert_equal(result, expected)
306 | end
307 |
308 | def test_form_with_using_controlled_option
309 | form_props(model: @post, id: "create-post", controlled: true) do |f|
310 | f.text_field(:title)
311 | f.text_area(:body)
312 | f.check_box(:secret)
313 | f.select(:category, %w[animal economy sports])
314 | f.radio_button(:admin, true)
315 | end
316 |
317 | result = json.result!.strip
318 |
319 | expected = {
320 | inputs: {
321 | title: {
322 | type: "text",
323 | value: "Hello World",
324 | name: "post[title]",
325 | id: "post_title"
326 | },
327 | body: {
328 | name: "post[body]",
329 | id: "post_body",
330 | type: "textarea",
331 | value: "Back to the hill and over it again!"
332 | },
333 | secret: {
334 | type: "checkbox",
335 | value: "1",
336 | checked: true,
337 | uncheckedValue: "0",
338 | includeHidden: true,
339 | name: "post[secret]",
340 | id: "post_secret"
341 | },
342 | category: {
343 | name: "post[category]",
344 | id: "post_category",
345 | type: "select",
346 | options: [
347 | {value: "animal", label: "animal"},
348 | {value: "economy", label: "economy"},
349 | {value: "sports", label: "sports"}
350 | ]
351 | },
352 | adminTrue: {
353 | type: "radio",
354 | value: "true",
355 | checked: true,
356 | name: "post[admin]",
357 | id: "post_admin_true"
358 | }
359 | },
360 | extras: {
361 | method: {
362 | name: "_method",
363 | type: "hidden",
364 | defaultValue: "patch",
365 | autoComplete: "off"
366 | },
367 | utf8: {
368 | name: "utf8",
369 | type: "hidden",
370 | defaultValue: "\u0026#x2713;",
371 | autoComplete: "off"
372 | }
373 | },
374 | form: {
375 | id: "create-post",
376 | action: "/posts/123",
377 | acceptCharset: "UTF-8",
378 | method: "post"
379 | }
380 | }.to_json
381 | assert_equal(result, expected)
382 | end
383 |
384 | def test_form_with_explicit_key
385 | form_props(model: @post, id: "create-post") do |f|
386 | f.text_field(:title, key: :aTitle)
387 | f.text_area(:body, key: :aBody)
388 | f.check_box(:secret, key: :aSecret)
389 | f.select(:category, %w[animal economy sports], key: :aCategory)
390 | end
391 |
392 | result = json.result!.strip
393 |
394 | expected = {
395 | inputs: {
396 | aTitle: {
397 | type: "text",
398 | defaultValue: "Hello World",
399 | name: "post[title]",
400 | id: "post_title"
401 | },
402 | aBody: {
403 | name: "post[body]",
404 | id: "post_body",
405 | type: "textarea",
406 | defaultValue: "Back to the hill and over it again!"
407 | },
408 | aSecret: {
409 | type: "checkbox",
410 | value: "1",
411 | defaultChecked: true,
412 | uncheckedValue: "0",
413 | includeHidden: true,
414 | name: "post[secret]",
415 | id: "post_secret"
416 | },
417 | aCategory: {
418 | name: "post[category]",
419 | id: "post_category",
420 | type: "select",
421 | options: [
422 | {value: "animal", label: "animal"},
423 | {value: "economy", label: "economy"},
424 | {value: "sports", label: "sports"}
425 | ]
426 | }
427 | },
428 | extras: {
429 | method: {
430 | name: "_method",
431 | type: "hidden",
432 | defaultValue: "patch",
433 | autoComplete: "off"
434 | },
435 | utf8: {
436 | name: "utf8",
437 | type: "hidden",
438 | defaultValue: "\u0026#x2713;",
439 | autoComplete: "off"
440 | }
441 | },
442 | form: {
443 | id: "create-post",
444 | action: "/posts/123",
445 | acceptCharset: "UTF-8",
446 | method: "post"
447 | }
448 | }.to_json
449 | assert_equal(result, expected)
450 | end
451 |
452 |
453 | def with_default_enforce_utf8(value)
454 | old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8
455 | ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value
456 |
457 | yield
458 | ensure
459 | ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
460 | end
461 | end
462 |
--------------------------------------------------------------------------------
/test/inputs/checkbox_field_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class CheckboxFieldTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | setup :setup_test_fixture
7 |
8 | def test_check_box
9 | @post.admin = false
10 | form_props(model: @post) do |f|
11 | f.check_box(:admin)
12 | end
13 | result = json.result!.strip
14 | expected = {
15 | "type" => "checkbox",
16 | "value" => "1",
17 | "uncheckedValue" => "0",
18 | "defaultChecked" => false,
19 | "includeHidden" => true,
20 | "name" => "post[admin]",
21 | "id" => "post_admin"
22 | }
23 | assert_equal(JSON.parse(result)["inputs"]["admin"], expected)
24 |
25 | output = render_js(<<-JS)
26 | let formProps = #{result}
27 | return
28 | JS
29 |
30 | assert_dom_equal(
31 | '',
32 | output
33 | )
34 | @post.admin = true
35 | end
36 |
37 | def test_check_box_disabled
38 | @post.admin = false
39 | form_props(model: @post) do |f|
40 | f.check_box(:admin, disabled: true)
41 | end
42 | result = json.result!.strip
43 | expected = {
44 | "type" => "checkbox",
45 | "value" => "1",
46 | "defaultChecked" => false,
47 | "uncheckedValue" => "0",
48 | "disabled" => true,
49 | "includeHidden" => true,
50 | "name" => "post[admin]",
51 | "id" => "post_admin"
52 | }
53 | assert_equal(JSON.parse(result)["inputs"]["admin"], expected)
54 | @post.admin = true
55 | end
56 |
57 | def test_check_box_default_checked
58 | @post.admin = true
59 | form_props(model: @post) do |f|
60 | f.check_box(:admin)
61 | end
62 | result = json.result!.strip
63 | expected = {
64 | "type" => "checkbox",
65 | "value" => "1",
66 | "uncheckedValue" => "0",
67 | "defaultChecked" => true,
68 | "includeHidden" => true,
69 | "name" => "post[admin]",
70 | "id" => "post_admin"
71 | }
72 | assert_equal(JSON.parse(result)["inputs"]["admin"], expected)
73 | @post.admin = true
74 | end
75 |
76 | def test_check_box_checked_if_object_value_is_same_that_check_value
77 | form_props(model: @post) do |f|
78 | f.check_box(:secret)
79 | end
80 | result = json.result!.strip
81 | expected = {
82 | "type" => "checkbox",
83 | "value" => "1",
84 | "defaultChecked" => true,
85 | "uncheckedValue" => "0",
86 | "includeHidden" => true,
87 | "name" => "post[secret]",
88 | "id" => "post_secret"
89 | }
90 | assert_equal(JSON.parse(result)["inputs"]["secret"], expected)
91 |
92 | output = render_js(<<-JS)
93 | let formProps = #{result}
94 | return
95 | JS
96 |
97 | assert_dom_equal(
98 | '',
99 | output
100 | )
101 | end
102 |
103 | def test_check_box_not_checked_if_object_value_is_same_that_unchecked_value
104 | @post.secret = 0
105 | form_props(model: @post) do |f|
106 | f.check_box(:secret)
107 | end
108 | result = json.result!.strip
109 | expected = {
110 | "type" => "checkbox",
111 | "value" => "1",
112 | "uncheckedValue" => "0",
113 | "defaultChecked" => false,
114 | "includeHidden" => true,
115 | "name" => "post[secret]",
116 | "id" => "post_secret"
117 | }
118 | assert_equal(JSON.parse(result)["inputs"]["secret"], expected)
119 |
120 | output = render_js(<<-JS)
121 | let formProps = #{result}
122 | return
123 | JS
124 |
125 | assert_dom_equal(
126 | '',
127 | output
128 | )
129 | end
130 |
131 | def test_check_box_checked_if_option_checked_is_present
132 | @post.admin = false
133 | form_props(model: @post) do |f|
134 | f.check_box(:admin, checked: "checked")
135 | end
136 | result = json.result!.strip
137 |
138 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], true)
139 | @post.admin = true
140 | end
141 |
142 | def test_check_box_checked_if_object_value_is_true
143 | @post.admin = true
144 | form_props(model: @post) do |f|
145 | f.check_box(:admin)
146 | end
147 | result = json.result!.strip
148 |
149 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], true)
150 |
151 | @post.instance_eval do
152 | def secret?
153 | true
154 | end
155 | end
156 |
157 | form_props(model: @post) do |f|
158 | f.check_box(:secret)
159 | end
160 | result = json.result!.strip
161 |
162 | assert_equal(JSON.parse(result)["inputs"]["secret"]["defaultChecked"], true)
163 | @post.admin = true
164 | end
165 |
166 | def test_check_box_checked_if_object_value_includes_checked_value
167 | @post.admin = ["0"]
168 | form_props(model: @post) do |f|
169 | f.check_box(:admin)
170 | end
171 | result = json.result!.strip
172 |
173 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], false)
174 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "1")
175 |
176 | @post.admin = ["1"]
177 | form_props(model: @post) do |f|
178 | f.check_box(:admin)
179 | end
180 | result = json.result!.strip
181 |
182 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], true)
183 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "1")
184 |
185 | @post.admin = Set.new(["1"])
186 | form_props(model: @post) do |f|
187 | f.check_box(:admin)
188 | end
189 | result = json.result!.strip
190 |
191 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], true)
192 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "1")
193 | end
194 |
195 | def test_check_box_with_include_hidden_false
196 | @post.secret = false
197 | form_props(model: @post) do |f|
198 | f.check_box(:secret, include_hidden: false)
199 | end
200 | result = json.result!.strip
201 | expected = {
202 | "type" => "checkbox",
203 | "value" => "1",
204 | "defaultChecked" => false,
205 | "includeHidden" => false,
206 | "uncheckedValue" => "0",
207 | "name" => "post[secret]",
208 | "id" => "post_secret"
209 | }
210 | assert_equal(JSON.parse(result)["inputs"]["secret"], expected)
211 |
212 | output = render_js(<<-JS)
213 | let formProps = #{result}
214 | return
215 | JS
216 |
217 | assert_dom_equal(
218 | '',
219 | output
220 | )
221 | end
222 |
223 | def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_string
224 | @post.admin = "on"
225 | form_props(model: @post) do |f|
226 | f.check_box(:admin, {}, "on", "off")
227 | end
228 | result = json.result!.strip
229 |
230 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], true)
231 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "on")
232 |
233 | @post.admin = "off"
234 | form_props(model: @post) do |f|
235 | f.check_box(:admin, {}, "on", "off")
236 | end
237 | result = json.result!.strip
238 |
239 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], false)
240 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "on")
241 | end
242 |
243 | def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_boolean
244 | @post.admin = false
245 | form_props(model: @post) do |f|
246 | f.check_box(:admin, {}, false, true)
247 | end
248 | result = json.result!.strip
249 |
250 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], true)
251 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "false")
252 |
253 | @post.admin = true
254 | form_props(model: @post) do |f|
255 | f.check_box(:admin, {}, false, true)
256 | end
257 | result = json.result!.strip
258 |
259 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], false)
260 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "false")
261 | end
262 |
263 | def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_integer
264 | @post.admin = 0
265 | form_props(model: @post) do |f|
266 | f.check_box(:admin, {}, 0, 1)
267 | end
268 | result = json.result!.strip
269 |
270 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], true)
271 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "0")
272 |
273 | @post.admin = 1
274 | form_props(model: @post) do |f|
275 | f.check_box(:admin, {}, 0, 1)
276 | end
277 | result = json.result!.strip
278 |
279 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], false)
280 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "0")
281 |
282 | @post.admin = 2
283 | form_props(model: @post) do |f|
284 | f.check_box(:admin, {}, 0, 1)
285 | end
286 | result = json.result!.strip
287 |
288 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], false)
289 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "0")
290 | end
291 |
292 | def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_float
293 | @post.admin = 0.0
294 | form_props(model: @post) do |f|
295 | f.check_box(:admin, {}, 0, 1)
296 | end
297 | result = json.result!.strip
298 |
299 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], true)
300 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "0")
301 |
302 | @post.admin = 1.1
303 | form_props(model: @post) do |f|
304 | f.check_box(:admin, {}, 0, 1)
305 | end
306 | result = json.result!.strip
307 |
308 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], false)
309 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "0")
310 |
311 | @post.admin = 2.2
312 | form_props(model: @post) do |f|
313 | f.check_box(:admin, {}, 0, 1)
314 | end
315 | result = json.result!.strip
316 |
317 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], false)
318 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "0")
319 | end
320 |
321 | def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_big_decimal
322 | @post.admin = BigDecimal("0")
323 | form_props(model: @post) do |f|
324 | f.check_box(:admin, {}, 0, 1)
325 | end
326 | result = json.result!.strip
327 |
328 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], true)
329 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "0")
330 |
331 | @post.admin = BigDecimal("1")
332 | form_props(model: @post) do |f|
333 | f.check_box(:admin, {}, 0, 1)
334 | end
335 | result = json.result!.strip
336 |
337 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], false)
338 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "0")
339 |
340 | @post.admin = BigDecimal("2.2", 1)
341 | form_props(model: @post) do |f|
342 | f.check_box(:admin, {}, 0, 1)
343 | end
344 | result = json.result!.strip
345 |
346 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], false)
347 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "0")
348 | end
349 |
350 | def test_check_box_with_nil_unchecked_value
351 | @post.admin = "on"
352 | form_props(model: @post) do |f|
353 | f.check_box(:admin, {}, "on", nil)
354 | end
355 | result = json.result!.strip
356 |
357 | assert_equal(JSON.parse(result)["inputs"]["admin"]["defaultChecked"], true)
358 | assert_equal(JSON.parse(result)["inputs"]["admin"]["value"], "on")
359 | end
360 |
361 | def test_check_box_with_multiple_behavior
362 | @post.comment_ids = [2, 3]
363 |
364 | form_props(model: @post) do |f|
365 | f.check_box(:comment_ids, {multiple: true}, 1)
366 | end
367 | result = json.result!.strip
368 |
369 | expected = {
370 | "type" => "checkbox",
371 | "value" => "1",
372 | "defaultChecked" => false,
373 | "uncheckedValue" => "0",
374 | "includeHidden" => true,
375 | "name" => "post[comment_ids][]",
376 | "id" => "post_comment_ids_1"
377 | }
378 |
379 | assert_equal(JSON.parse(result)["inputs"]["commentIds"], expected)
380 |
381 | form_props(model: @post) do |f|
382 | f.check_box(:comment_ids, {"multiple" => true}, 3)
383 | end
384 | result = json.result!.strip
385 |
386 | expected = {
387 | "type" => "checkbox",
388 | "value" => "3",
389 | "defaultChecked" => true,
390 | "uncheckedValue" => "0",
391 | "includeHidden" => true,
392 | "name" => "post[comment_ids][]",
393 | "id" => "post_comment_ids_3"
394 | }
395 |
396 | assert_equal(JSON.parse(result)["inputs"]["commentIds"], expected)
397 | end
398 |
399 | def test_checkbox_disabled_disables_hidden_field
400 | @post.admin = true
401 | form_props(model: @post) do |f|
402 | f.check_box(:admin, disabled: true)
403 | end
404 | result = json.result!.strip
405 |
406 | assert_equal(JSON.parse(result)["inputs"]["admin"]["disabled"], true)
407 | end
408 |
409 | def test_checkbox_form_html5_attribute
410 | @post.admin = true
411 | form_props(model: @post) do |f|
412 | f.check_box(:admin, form: "new_form")
413 | end
414 | result = json.result!.strip
415 |
416 | assert_equal(JSON.parse(result)["inputs"]["admin"]["form"], "new_form")
417 | end
418 | end
419 |
--------------------------------------------------------------------------------
/test/inputs/time_zone_select_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "../test_helper"
2 |
3 | class TimeZoneSelectTest < ActionView::TestCase
4 | include FormProps::ActionViewExtensions::FormHelper
5 |
6 | module FakeZones
7 | FakeZone = Struct.new(:name) do
8 | def to_s
9 | name
10 | end
11 |
12 | def =~(_re)
13 | end
14 |
15 | def match?(_re)
16 | end
17 | end
18 |
19 | module ClassMethods
20 | def [](id)
21 | fake_zones ? fake_zones[id] : super
22 | end
23 |
24 | def all
25 | fake_zones ? fake_zones.values : super
26 | end
27 |
28 | def dummy
29 | :test
30 | end
31 | end
32 |
33 | def self.prepended(base)
34 | base.mattr_accessor(:fake_zones)
35 | class << base
36 | prepend ClassMethods
37 | end
38 | end
39 | end
40 |
41 | ActiveSupport::TimeZone.prepend FakeZones
42 |
43 | setup do
44 | setup_test_fixture
45 |
46 | ActiveSupport::TimeZone.fake_zones = %w[A B C D E].index_with do |id|
47 | FakeZones::FakeZone.new(id)
48 | end
49 |
50 | @fake_timezones = ActiveSupport::TimeZone.all
51 | end
52 |
53 | teardown do
54 | ActiveSupport::TimeZone.fake_zones = nil
55 | end
56 |
57 | def test_time_zone_select
58 | @post = Post.new
59 | @post.time_zone = "D"
60 |
61 | form_props(model: @post) do |f|
62 | f.time_zone_select(:time_zone)
63 | end
64 | result = json.result!.strip
65 |
66 | expected = {
67 | "type" => "select",
68 | "name" => "post[time_zone]",
69 | "id" => "post_time_zone",
70 | "defaultValue" => "D",
71 | "options" => [
72 | {"value" => "A", "label" => "A"},
73 | {"value" => "B", "label" => "B"},
74 | {"value" => "C", "label" => "C"},
75 | {"value" => "D", "label" => "D"},
76 | {"value" => "E", "label" => "E"}
77 | ]
78 | }
79 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
80 | end
81 |
82 | def test_time_zone_select_under_fields_for
83 | @post = Post.new
84 | @post.time_zone = "D"
85 |
86 | json.output do
87 | fields_for :post, @post, builder: FormProps::FormBuilder do |f|
88 | f.time_zone_select(:time_zone)
89 | end
90 | end
91 | result = json.result!.strip
92 |
93 | expected = {
94 | "type" => "select",
95 | "name" => "post[time_zone]",
96 | "id" => "post_time_zone",
97 | "defaultValue" => "D",
98 | "options" => [
99 | {"value" => "A", "label" => "A"},
100 | {"value" => "B", "label" => "B"},
101 | {"value" => "C", "label" => "C"},
102 | {"value" => "D", "label" => "D"},
103 | {"value" => "E", "label" => "E"}
104 | ]
105 | }
106 | assert_equal(JSON.parse(result)["output"]["timeZone"], expected)
107 | end
108 |
109 | def test_time_zone_select_under_fields_for_with_index
110 | @post = Post.new
111 | @post.time_zone = "D"
112 |
113 | json.output do
114 | fields_for :post, @post, index: 305, builder: FormProps::FormBuilder do |f|
115 | f.time_zone_select(:time_zone)
116 | end
117 | end
118 | result = json.result!.strip
119 |
120 | expected = {
121 | "type" => "select",
122 | "name" => "post[305][time_zone]",
123 | "id" => "post_305_time_zone",
124 | "defaultValue" => "D",
125 | "options" => [
126 | {"value" => "A", "label" => "A"},
127 | {"value" => "B", "label" => "B"},
128 | {"value" => "C", "label" => "C"},
129 | {"value" => "D", "label" => "D"},
130 | {"value" => "E", "label" => "E"}
131 | ]
132 | }
133 | assert_equal(JSON.parse(result)["output"]["timeZone"], expected)
134 | end
135 |
136 | def test_time_zone_select_under_fields_for_with_auto_index
137 | @post = Post.new
138 | @post.time_zone = "D"
139 | @post.instance_eval do
140 | def to_param
141 | 305
142 | end
143 | end
144 |
145 | json.output do
146 | fields_for "post[]", @post, builder: FormProps::FormBuilder do |f|
147 | f.time_zone_select(:time_zone)
148 | end
149 | end
150 | result = json.result!.strip
151 |
152 | expected = {
153 | "type" => "select",
154 | "name" => "post[305][time_zone]",
155 | "id" => "post_305_time_zone",
156 | "defaultValue" => "D",
157 | "options" => [
158 | {"value" => "A", "label" => "A"},
159 | {"value" => "B", "label" => "B"},
160 | {"value" => "C", "label" => "C"},
161 | {"value" => "D", "label" => "D"},
162 | {"value" => "E", "label" => "E"}
163 | ]
164 | }
165 | assert_equal(JSON.parse(result)["output"]["timeZone"], expected)
166 | end
167 |
168 | def test_time_zone_select_with_blank
169 | @post = Post.new
170 | @post.time_zone = "D"
171 |
172 | form_props(model: @post) do |f|
173 | f.time_zone_select(:time_zone, nil, {include_blank: true})
174 | end
175 | result = json.result!.strip
176 |
177 | expected = {
178 | "type" => "select",
179 | "name" => "post[time_zone]",
180 | "id" => "post_time_zone",
181 | "defaultValue" => "D",
182 | "options" => [
183 | {"value" => "", "label" => " "},
184 | {"value" => "A", "label" => "A"},
185 | {"value" => "B", "label" => "B"},
186 | {"value" => "C", "label" => "C"},
187 | {"value" => "D", "label" => "D"},
188 | {"value" => "E", "label" => "E"}
189 | ]
190 | }
191 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
192 | end
193 |
194 | def test_time_zone_select_with_blank_as_string
195 | @post = Post.new
196 | @post.time_zone = "D"
197 |
198 | form_props(model: @post) do |f|
199 | f.time_zone_select(:time_zone, nil, {include_blank: "No Zone"})
200 | end
201 | result = json.result!.strip
202 |
203 | expected = {
204 | "type" => "select",
205 | "name" => "post[time_zone]",
206 | "id" => "post_time_zone",
207 | "defaultValue" => "D",
208 | "options" => [
209 | {"value" => "", "label" => "No Zone"},
210 | {"value" => "A", "label" => "A"},
211 | {"value" => "B", "label" => "B"},
212 | {"value" => "C", "label" => "C"},
213 | {"value" => "D", "label" => "D"},
214 | {"value" => "E", "label" => "E"}
215 | ]
216 | }
217 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
218 | end
219 |
220 | def test_time_zone_select_with_style
221 | @post = Post.new
222 | @post.time_zone = "D"
223 |
224 | form_props(model: @post) do |f|
225 | f.time_zone_select(:time_zone, nil, {}, {"style" => "color: red"})
226 | end
227 | result = json.result!.strip
228 |
229 | expected = {
230 | "type" => "select",
231 | "name" => "post[time_zone]",
232 | "id" => "post_time_zone",
233 | "style" => "color: red",
234 | "defaultValue" => "D",
235 | "options" => [
236 | {"value" => "A", "label" => "A"},
237 | {"value" => "B", "label" => "B"},
238 | {"value" => "C", "label" => "C"},
239 | {"value" => "D", "label" => "D"},
240 | {"value" => "E", "label" => "E"}
241 | ]
242 | }
243 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
244 | end
245 |
246 | def test_time_zone_select_with_blank_and_style
247 | @post = Post.new
248 | @post.time_zone = "D"
249 |
250 | form_props(model: @post) do |f|
251 | f.time_zone_select(:time_zone, nil, {include_blank: true}, {"style" => "color: red"})
252 | end
253 | result = json.result!.strip
254 |
255 | expected = {
256 | "type" => "select",
257 | "name" => "post[time_zone]",
258 | "id" => "post_time_zone",
259 | "style" => "color: red",
260 | "defaultValue" => "D",
261 | "options" => [
262 | {"value" => "", "label" => " "},
263 | {"value" => "A", "label" => "A"},
264 | {"value" => "B", "label" => "B"},
265 | {"value" => "C", "label" => "C"},
266 | {"value" => "D", "label" => "D"},
267 | {"value" => "E", "label" => "E"}
268 | ]
269 | }
270 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
271 |
272 | form_props(model: @post) do |f|
273 | f.time_zone_select(:time_zone, nil, {include_blank: true}, {style: "color: red"})
274 | end
275 | result = json.result!.strip
276 |
277 | expected = {
278 | "type" => "select",
279 | "name" => "post[time_zone]",
280 | "id" => "post_time_zone",
281 | "style" => "color: red",
282 | "defaultValue" => "D",
283 | "options" => [
284 | {"value" => "", "label" => " "},
285 | {"value" => "A", "label" => "A"},
286 | {"value" => "B", "label" => "B"},
287 | {"value" => "C", "label" => "C"},
288 | {"value" => "D", "label" => "D"},
289 | {"value" => "E", "label" => "E"}
290 | ]
291 | }
292 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
293 | end
294 |
295 | def test_time_zone_select_with_blank_as_string_and_style
296 | @post = Post.new
297 | @post.time_zone = "D"
298 |
299 | form_props(model: @post) do |f|
300 | f.time_zone_select(:time_zone, nil, {include_blank: "No Zone"}, {"style" => "color: red"})
301 | end
302 | result = json.result!.strip
303 |
304 | expected = {
305 | "type" => "select",
306 | "name" => "post[time_zone]",
307 | "id" => "post_time_zone",
308 | "style" => "color: red",
309 | "defaultValue" => "D",
310 | "options" => [
311 | {"value" => "", "label" => "No Zone"},
312 | {"value" => "A", "label" => "A"},
313 | {"value" => "B", "label" => "B"},
314 | {"value" => "C", "label" => "C"},
315 | {"value" => "D", "label" => "D"},
316 | {"value" => "E", "label" => "E"}
317 | ]
318 | }
319 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
320 |
321 | form_props(model: @post) do |f|
322 | f.time_zone_select(:time_zone, nil, {include_blank: "No Zone"}, {style: "color: red"})
323 | end
324 | result = json.result!.strip
325 |
326 | expected = {
327 | "type" => "select",
328 | "name" => "post[time_zone]",
329 | "id" => "post_time_zone",
330 | "style" => "color: red",
331 | "defaultValue" => "D",
332 | "options" => [
333 | {"value" => "", "label" => "No Zone"},
334 | {"value" => "A", "label" => "A"},
335 | {"value" => "B", "label" => "B"},
336 | {"value" => "C", "label" => "C"},
337 | {"value" => "D", "label" => "D"},
338 | {"value" => "E", "label" => "E"}
339 | ]
340 | }
341 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
342 | end
343 |
344 | def test_time_zone_select_with_priority_zones
345 | @post = Post.new
346 | @post.time_zone = "D"
347 | zones = [ActiveSupport::TimeZone.new("A"), ActiveSupport::TimeZone.new("D")]
348 |
349 | form_props(model: @post) do |f|
350 | f.time_zone_select(:time_zone, zones)
351 | end
352 | result = json.result!.strip
353 |
354 | expected = {
355 | "type" => "select",
356 | "name" => "post[time_zone]",
357 | "id" => "post_time_zone",
358 | "defaultValue" => "D",
359 | "options" => [
360 | {"value" => "A", "label" => "A"},
361 | {"value" => "D", "label" => "D"},
362 | {"label" => "-------------", "value" => "", "disabled" => true},
363 | {"value" => "B", "label" => "B"},
364 | {"value" => "C", "label" => "C"},
365 | {"value" => "E", "label" => "E"}
366 | ]
367 | }
368 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
369 | end
370 |
371 | def test_time_zone_select_with_priority_zones_as_regexp
372 | @post = Post.new
373 | @post.time_zone = "D"
374 | @fake_timezones.each do |tz|
375 | def tz.=~(re)
376 | %(A D).include?(name)
377 | end
378 |
379 | def tz.match?(re)
380 | %(A D).include?(name)
381 | end
382 | end
383 |
384 | form_props(model: @post) do |f|
385 | f.time_zone_select(:time_zone, /A|D/)
386 | end
387 | result = json.result!.strip
388 |
389 | expected = {
390 | "type" => "select",
391 | "name" => "post[time_zone]",
392 | "id" => "post_time_zone",
393 | "defaultValue" => "D",
394 | "options" => [
395 | {"value" => "A", "label" => "A"},
396 | {"value" => "D", "label" => "D"},
397 | {"label" => "-------------", "value" => "", "disabled" => true},
398 | {"value" => "B", "label" => "B"},
399 | {"value" => "C", "label" => "C"},
400 | {"value" => "E", "label" => "E"}
401 | ]
402 | }
403 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
404 | end
405 |
406 | def test_time_zone_select_with_priority_zones_is_not_implemented_with_grep
407 | @post = Post.new
408 | @post.time_zone = "D"
409 |
410 | # `time_zone_select` can't be written with `grep` because Active Support
411 | # time zones don't support implicit string coercion with `to_str`.
412 | @fake_timezones.each do |tz|
413 | def tz.===(zone)
414 | raise StandardError
415 | end
416 | end
417 |
418 | form_props(model: @post) do |f|
419 | f.time_zone_select(:time_zone, /A|D/)
420 | end
421 | result = json.result!.strip
422 |
423 | expected = {
424 | "type" => "select",
425 | "name" => "post[time_zone]",
426 | "id" => "post_time_zone",
427 | "defaultValue" => "D",
428 | "options" => [
429 | {"label" => "-------------", "value" => "", "disabled" => true},
430 | {"value" => "A", "label" => "A"},
431 | {"value" => "B", "label" => "B"},
432 | {"value" => "C", "label" => "C"},
433 | {"value" => "D", "label" => "D"},
434 | {"value" => "E", "label" => "E"}
435 | ]
436 | }
437 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
438 | end
439 |
440 | def test_time_zone_select_with_default_time_zone_and_nil_value
441 | @post = Post.new
442 | @post.time_zone = nil
443 |
444 | form_props(model: @post) do |f|
445 | f.time_zone_select(:time_zone, nil, default: "B")
446 | end
447 | result = json.result!.strip
448 |
449 | expected = {
450 | "type" => "select",
451 | "name" => "post[time_zone]",
452 | "id" => "post_time_zone",
453 | "defaultValue" => "B",
454 | "options" => [
455 | {"value" => "A", "label" => "A"},
456 | {"value" => "B", "label" => "B"},
457 | {"value" => "C", "label" => "C"},
458 | {"value" => "D", "label" => "D"},
459 | {"value" => "E", "label" => "E"}
460 | ]
461 | }
462 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
463 | end
464 |
465 | def test_time_zone_select_with_default_time_zone_and_value
466 | @post = Post.new
467 | @post.time_zone = "D"
468 |
469 | form_props(model: @post) do |f|
470 | f.time_zone_select(:time_zone, nil, default: "B")
471 | end
472 | result = json.result!.strip
473 |
474 | expected = {
475 | "type" => "select",
476 | "name" => "post[time_zone]",
477 | "id" => "post_time_zone",
478 | "defaultValue" => "D",
479 | "options" => [
480 | {"value" => "A", "label" => "A"},
481 | {"value" => "B", "label" => "B"},
482 | {"value" => "C", "label" => "C"},
483 | {"value" => "D", "label" => "D"},
484 | {"value" => "E", "label" => "E"}
485 | ]
486 | }
487 | assert_equal(JSON.parse(result)["inputs"]["timeZone"], expected)
488 | end
489 | end
490 |
--------------------------------------------------------------------------------