form_props 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +714 -0
  3. data/lib/form_props/action_view_extensions/form_helper.rb +120 -0
  4. data/lib/form_props/form_builder.rb +223 -0
  5. data/lib/form_props/form_options_helper.rb +158 -0
  6. data/lib/form_props/inputs/base.rb +164 -0
  7. data/lib/form_props/inputs/check_box.rb +69 -0
  8. data/lib/form_props/inputs/collection_check_boxes.rb +36 -0
  9. data/lib/form_props/inputs/collection_helpers.rb +53 -0
  10. data/lib/form_props/inputs/collection_radio_buttons.rb +35 -0
  11. data/lib/form_props/inputs/collection_select.rb +30 -0
  12. data/lib/form_props/inputs/color_field.rb +27 -0
  13. data/lib/form_props/inputs/date_field.rb +17 -0
  14. data/lib/form_props/inputs/datetime_field.rb +32 -0
  15. data/lib/form_props/inputs/datetime_local_field.rb +17 -0
  16. data/lib/form_props/inputs/email_field.rb +13 -0
  17. data/lib/form_props/inputs/file_field.rb +13 -0
  18. data/lib/form_props/inputs/grouped_collection_select.rb +31 -0
  19. data/lib/form_props/inputs/hidden_field.rb +16 -0
  20. data/lib/form_props/inputs/month_field.rb +17 -0
  21. data/lib/form_props/inputs/number_field.rb +21 -0
  22. data/lib/form_props/inputs/password_field.rb +18 -0
  23. data/lib/form_props/inputs/radio_button.rb +48 -0
  24. data/lib/form_props/inputs/range_field.rb +13 -0
  25. data/lib/form_props/inputs/search_field.rb +28 -0
  26. data/lib/form_props/inputs/select.rb +42 -0
  27. data/lib/form_props/inputs/submit.rb +26 -0
  28. data/lib/form_props/inputs/tel_field.rb +13 -0
  29. data/lib/form_props/inputs/text_area.rb +37 -0
  30. data/lib/form_props/inputs/text_field.rb +28 -0
  31. data/lib/form_props/inputs/time_field.rb +17 -0
  32. data/lib/form_props/inputs/time_zone_select.rb +22 -0
  33. data/lib/form_props/inputs/url_field.rb +13 -0
  34. data/lib/form_props/inputs/week_field.rb +17 -0
  35. data/lib/form_props/inputs/weekday_select.rb +28 -0
  36. data/lib/form_props/version.rb +5 -0
  37. data/lib/form_props.rb +45 -0
  38. metadata +120 -0
@@ -0,0 +1,69 @@
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] = "checkbox"
24
+ options[:value] = @checked_value
25
+ options[:checked] = true if 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_id_for_value(@checked_value, options)
32
+ options.delete(:multiple)
33
+ else
34
+ add_default_name_and_id(options)
35
+ end
36
+
37
+ input_props(options)
38
+ }
39
+
40
+ if flatten
41
+ body_block.call
42
+ else
43
+ json.set!(sanitized_method_name) do
44
+ body_block.call
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def checked?(value)
52
+ case value
53
+ when TrueClass, FalseClass
54
+ value == !!@checked_value
55
+ when NilClass
56
+ false
57
+ when String
58
+ value == @checked_value
59
+ else
60
+ if value.respond_to?(:include?)
61
+ value.include?(@checked_value)
62
+ else
63
+ value.to_i == @checked_value.to_i
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,36 @@
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 CollectionHelpers
10
+
11
+ class CheckBoxBuilder < Builder
12
+ def render(extra_html_options = {})
13
+ html_options = extra_html_options.merge(@input_html_options)
14
+ html_options[:multiple] = true
15
+ html_options[:skip_default_ids] = false
16
+
17
+ checkbox = CheckBox.new(@object_name, @method_name, @template_object, @value, nil, html_options)
18
+ checkbox.render(true)
19
+ checkbox.json.label @text
20
+ end
21
+ end
22
+
23
+ def render
24
+ json.set!(sanitized_method_name) do
25
+ json.collection do
26
+ render_collection_for(CheckBoxBuilder)
27
+ end
28
+
29
+ json.include_hidden(@options.fetch(:include_hidden) { true })
30
+
31
+ input_props(@html_options)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,53 @@
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
@@ -0,0 +1,35 @@
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 CollectionHelpers
10
+
11
+ class RadioButtonBuilder < Builder
12
+ def render(extra_html_options = {})
13
+ html_options = extra_html_options.merge(@input_html_options)
14
+ html_options[:skip_default_ids] = false
15
+
16
+ checkbox = RadioButton.new(@object_name, @method_name, @template_object, @value, html_options)
17
+ checkbox.render(true)
18
+ checkbox.json.label @text
19
+ end
20
+ end
21
+
22
+ def render
23
+ json.set!(sanitized_method_name) do
24
+ json.collection do
25
+ render_collection_for(RadioButtonBuilder)
26
+ end
27
+
28
+ json.include_hidden(@options.fetch(:include_hidden) { true })
29
+
30
+ input_props(@html_options)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FormProps
4
+ module Inputs
5
+ class CollectionSelect < Base # :nodoc:
6
+ include FormOptionsHelper
7
+
8
+ def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
9
+ @collection = collection
10
+ @value_method = value_method
11
+ @text_method = text_method
12
+ @html_options = html_options
13
+
14
+ super(object_name, method_name, template_object, options)
15
+ end
16
+
17
+ def render
18
+ option_tags_options = {
19
+ selected: @options.fetch(:selected) { value },
20
+ disabled: @options[:disabled]
21
+ }
22
+
23
+ select_content_props(
24
+ options_from_collection_for_select(@collection, @value_method, @text_method, option_tags_options),
25
+ @options, @html_options
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,17 @@
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&.strftime("%Y-%m-%d")
10
+ end
11
+
12
+ def field_type
13
+ "date"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
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
@@ -0,0 +1,17 @@
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
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FormProps
4
+ module Inputs
5
+ class GroupedCollectionSelect < Base
6
+ include FormOptionsHelper
7
+
8
+ def initialize(object_name, method_name, template_object, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
9
+ @collection = collection
10
+ @group_method = group_method
11
+ @group_label_method = group_label_method
12
+ @option_key_method = option_key_method
13
+ @option_value_method = option_value_method
14
+ @html_options = html_options
15
+
16
+ super(object_name, method_name, template_object, options)
17
+ end
18
+
19
+ def render
20
+ option_tags_options = {
21
+ selected: @options.fetch(:selected) { value },
22
+ disabled: @options[:disabled]
23
+ }
24
+
25
+ select_content_props(
26
+ option_groups_from_collection_for_select(@collection, @group_method, @group_label_method, @option_key_method, @option_value_method, option_tags_options), @options, @html_options
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FormProps
4
+ module Inputs
5
+ class HiddenField < TextField
6
+ def render
7
+ @options[:autocomplete] = "off"
8
+ super
9
+ end
10
+
11
+ def field_type
12
+ "hidden"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
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
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,48 @@
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] = "radio"
22
+ @options[:value] = @tag_value
23
+ @options[:checked] = true if input_checked?(@options)
24
+
25
+ name_for_key = sanitized_method_name + "_#{sanitized_value(@tag_value)}"
26
+
27
+ body_block = -> {
28
+ add_default_name_and_id_for_value(@tag_value, @options)
29
+ input_props(@options)
30
+ }
31
+
32
+ if flatten
33
+ body_block.call
34
+ else
35
+ json.set!(name_for_key) do
36
+ body_block.call
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def checked?(value)
44
+ value.to_s == @tag_value.to_s
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FormProps
4
+ module Inputs
5
+ class Select < Base
6
+ include FormOptionsHelper
7
+
8
+ def initialize(object_name, method_name, template_object, choices, options, html_options)
9
+ @choices = choices
10
+ @choices = @choices.to_a if @choices.is_a?(Range)
11
+
12
+ @html_options = html_options
13
+ super(object_name, method_name, template_object, options)
14
+ end
15
+
16
+ def render
17
+ option_tags_options = {
18
+ selected: @options.fetch(:selected) { value.nil? ? "" : value },
19
+ disabled: @options[:disabled]
20
+ }
21
+
22
+ option_tags = if grouped_choices?
23
+ grouped_options_for_select(@choices, option_tags_options)
24
+ else
25
+ options_for_select(@choices, option_tags_options)
26
+ end
27
+
28
+ select_content_props(option_tags, @options, @html_options)
29
+ end
30
+
31
+ private
32
+
33
+ # Grouped choices look like this:
34
+ #
35
+ # [nil, []]
36
+ # { nil => [] }
37
+ def grouped_choices?
38
+ !@choices.blank? && @choices.first.respond_to?(:last) && Array === @choices.first.last
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,26 @@
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
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,37 @@
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_method_name) do
12
+ add_default_name_and_id(@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