phlexi-form 0.2.0 → 0.3.0.rc1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +4 -9
  3. data/README.md +115 -316
  4. data/TODO +4 -0
  5. data/config.ru +0 -3
  6. data/gemfiles/default.gemfile.lock +22 -2
  7. data/gemfiles/rails_7.gemfile +8 -0
  8. data/gemfiles/rails_7.gemfile.lock +282 -0
  9. data/lib/phlexi/form/base.rb +52 -35
  10. data/lib/phlexi/form/components/base.rb +12 -6
  11. data/lib/phlexi/form/components/checkbox.rb +5 -0
  12. data/lib/phlexi/form/components/collection_checkboxes.rb +28 -14
  13. data/lib/phlexi/form/components/collection_radio_buttons.rb +19 -13
  14. data/lib/phlexi/form/components/concerns/handles_array_input.rb +21 -0
  15. data/lib/phlexi/form/components/concerns/handles_input.rb +53 -0
  16. data/lib/phlexi/form/components/concerns/has_options.rb +6 -2
  17. data/lib/phlexi/form/components/concerns/submits_form.rb +39 -0
  18. data/lib/phlexi/form/components/file_input.rb +32 -0
  19. data/lib/phlexi/form/components/input.rb +39 -33
  20. data/lib/phlexi/form/components/input_array.rb +45 -0
  21. data/lib/phlexi/form/components/label.rb +2 -1
  22. data/lib/phlexi/form/components/radio_button.rb +11 -1
  23. data/lib/phlexi/form/components/select.rb +21 -5
  24. data/lib/phlexi/form/components/submit_button.rb +41 -0
  25. data/lib/phlexi/form/field_options/associations.rb +21 -0
  26. data/lib/phlexi/form/field_options/autofocus.rb +1 -1
  27. data/lib/phlexi/form/field_options/collection.rb +26 -9
  28. data/lib/phlexi/form/field_options/errors.rb +10 -0
  29. data/lib/phlexi/form/field_options/{type.rb → inferred_types.rb} +12 -12
  30. data/lib/phlexi/form/field_options/multiple.rb +2 -0
  31. data/lib/phlexi/form/field_options/themes.rb +207 -0
  32. data/lib/phlexi/form/option_mapper.rb +2 -2
  33. data/lib/phlexi/form/structure/dom.rb +19 -14
  34. data/lib/phlexi/form/structure/field_builder.rb +145 -108
  35. data/lib/phlexi/form/structure/field_collection.rb +14 -5
  36. data/lib/phlexi/form/structure/namespace.rb +31 -19
  37. data/lib/phlexi/form/structure/namespace_collection.rb +20 -20
  38. data/lib/phlexi/form/structure/node.rb +1 -1
  39. data/lib/phlexi/form/version.rb +1 -1
  40. data/lib/phlexi/form.rb +4 -1
  41. metadata +30 -6
  42. data/CODE_OF_CONDUCT.md +0 -84
@@ -19,13 +19,17 @@ module Phlexi
19
19
  end
20
20
 
21
21
  def selected?(option)
22
- if field.multiple?
22
+ if attributes[:multiple]
23
23
  @options_list ||= Array(field.value)
24
24
  @options_list.any? { |item| item.to_s == option.to_s }
25
25
  else
26
- field.dom.value == option.to_s
26
+ field.value.to_s == option.to_s
27
27
  end
28
28
  end
29
+
30
+ def normalize_simple_input(input_value)
31
+ ([super] & option_mapper.values)[0]
32
+ end
29
33
  end
30
34
  end
31
35
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ module Concerns
7
+ module SubmitsForm
8
+ def submit_type_value
9
+ if field.object.respond_to?(:persisted?)
10
+ field.object.persisted? ? :update : :create
11
+ else
12
+ :submit
13
+ end
14
+ end
15
+
16
+ def submit_type_label
17
+ @submit_type_label ||= begin
18
+ key = submit_type_value
19
+
20
+ model_object = field.dom.lineage.first.key.to_s
21
+ model_name_human = if field.object.respond_to?(:model_name)
22
+ field.object.model_name.human
23
+ else
24
+ model_object.humanize
25
+ end
26
+
27
+ defaults = []
28
+ defaults << :"helpers.submit.#{model_object}.#{key}"
29
+ defaults << :"helpers.submit.#{key}"
30
+ defaults << "#{key.to_s.humanize} #{model_name_human}"
31
+
32
+ I18n.t(defaults.shift, model: model_name_human, default: defaults)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class FileInput < Input
7
+ def view_template
8
+ input(type: :hidden, name: attributes[:name], value: "", autocomplete: "off", hidden: true) if include_hidden?
9
+ input(**attributes)
10
+ end
11
+
12
+ protected
13
+
14
+ def build_input_attributes
15
+ attributes[:type] = :file
16
+ super
17
+ attributes[:value] = false
18
+ end
19
+
20
+ def include_hidden?
21
+ return false if @include_hidden == false
22
+
23
+ attributes[:multiple]
24
+ end
25
+
26
+ def normalize_input(input_value)
27
+ input_value
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -4,6 +4,8 @@ module Phlexi
4
4
  module Form
5
5
  module Components
6
6
  class Input < Base
7
+ include Concerns::HandlesInput
8
+
7
9
  def view_template
8
10
  input(**attributes)
9
11
  end
@@ -15,6 +17,7 @@ module Phlexi
15
17
 
16
18
  # only overwrite id if it was set in Base
17
19
  attributes[:id] = field.dom.id if attributes[:id] == "#{field.dom.id}_#{component_name}"
20
+
18
21
  attributes[:name] = field.dom.name
19
22
  attributes[:value] = field.dom.value
20
23
 
@@ -22,54 +25,57 @@ module Phlexi
22
25
  end
23
26
 
24
27
  def build_input_attributes
25
- attributes[:type] = attributes.fetch(:type, field.input_type)
26
- attributes[:disabled] = attributes.fetch(:disabled, field.disabled?)
28
+ attributes.fetch(:type) { attributes[:type] = field.inferred_input_type }
29
+ attributes.fetch(:disabled) { attributes[:disabled] = field.disabled? }
27
30
 
28
31
  case attributes[:type]
29
32
  when :text, :password, :email, :tel, :url, :search
30
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
31
- attributes[:placeholder] = attributes.fetch(:placeholder, field.placeholder)
32
- attributes[:minlength] = attributes.fetch(:minlength, field.minlength)
33
- attributes[:maxlength] = attributes.fetch(:maxlength, field.maxlength)
34
- attributes[:readonly] = attributes.fetch(:readonly, field.readonly?)
35
- attributes[:required] = attributes.fetch(:required, field.required?)
36
- attributes[:pattern] = attributes.fetch(:pattern, field.pattern)
33
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
34
+ attributes.fetch(:placeholder) { attributes[:placeholder] = field.placeholder }
35
+ attributes.fetch(:minlength) { attributes[:minlength] = field.minlength }
36
+ attributes.fetch(:maxlength) { attributes[:maxlength] = field.maxlength }
37
+ attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
38
+ attributes.fetch(:required) { attributes[:required] = field.required? }
39
+ attributes.fetch(:pattern) { attributes[:pattern] = field.pattern }
37
40
  when :number
38
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
39
- attributes[:placeholder] = attributes.fetch(:placeholder, field.placeholder)
40
- attributes[:readonly] = attributes.fetch(:readonly, field.readonly?)
41
- attributes[:required] = attributes.fetch(:required, field.required?)
42
- attributes[:min] = attributes.fetch(:min, field.min)
43
- attributes[:max] = attributes.fetch(:max, field.max)
44
- attributes[:step] = attributes.fetch(:step, field.step)
41
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
42
+ attributes.fetch(:placeholder) { attributes[:placeholder] = field.placeholder }
43
+ attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
44
+ attributes.fetch(:required) { attributes[:required] = field.required? }
45
+ attributes.fetch(:min) { attributes[:min] = field.min }
46
+ attributes.fetch(:max) { attributes[:max] = field.max }
47
+ attributes.fetch(:step) { attributes[:step] = field.step }
45
48
  when :checkbox, :radio
46
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
47
- attributes[:required] = attributes.fetch(:required, field.required?)
49
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
50
+ attributes.fetch(:required) { attributes[:required] = field.required? }
48
51
  when :file
49
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
50
- attributes[:required] = attributes.fetch(:required, field.required?)
51
- attributes[:multiple] = attributes.fetch(:multiple, field.multiple)
52
- attributes[:accept] = attributes.fetch(:accept, field.accept)
52
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
53
+ attributes.fetch(:required) { attributes[:required] = field.required? }
54
+ attributes.fetch(:multiple) { attributes[:multiple] = field.multiple? }
53
55
  when :date, :time, :datetime_local
54
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
55
- attributes[:readonly] = attributes.fetch(:readonly, field.readonly?)
56
- attributes[:required] = attributes.fetch(:required, field.required?)
57
- attributes[:min] = attributes.fetch(:min, field.min)
58
- attributes[:max] = attributes.fetch(:max, field.max)
56
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
57
+ attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
58
+ attributes.fetch(:required) { attributes[:required] = field.required? }
59
+ attributes.fetch(:min) { attributes[:min] = field.min }
60
+ attributes.fetch(:max) { attributes[:max] = field.max }
59
61
  when :color
60
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
62
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
61
63
  when :range
62
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
63
- attributes[:min] = attributes.fetch(:min, field.min)
64
- attributes[:max] = attributes.fetch(:max, field.max)
65
- attributes[:step] = attributes.fetch(:step, field.step)
64
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
65
+ attributes.fetch(:min) { attributes[:min] = field.min }
66
+ attributes.fetch(:max) { attributes[:max] = field.max }
67
+ attributes.fetch(:step) { attributes[:step] = field.step }
68
+ when :hidden
69
+ attributes[:class] = false
70
+ attributes[:hidden] = true
71
+ attributes[:autocomplete] = "off"
66
72
  else
67
73
  # Handle any unrecognized input types
68
74
  # Rails.logger.warn("Unhandled input type: #{attributes[:type]}")
69
75
  end
70
76
 
71
77
  if (attributes[:type] == :file) ? attributes[:multiple] : attributes.delete(:multiple)
72
- attributes[:name] = "#{attributes[:name].sub(/\[]$/, "")}[]"
78
+ attributes[:name] = "#{attributes[:name].sub(/\[\]$/, "")}[]"
73
79
  end
74
80
  end
75
81
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class InputArray < Base
7
+ include Concerns::HandlesInput
8
+ include Concerns::HandlesArrayInput
9
+
10
+ def view_template
11
+ div(**attributes.slice(:id, :class)) do
12
+ field.multi(values.length) do |builder|
13
+ render builder.hidden_field_tag if builder.index == 0
14
+
15
+ field = builder.field(
16
+ label: builder.key,
17
+ # we expect key to be an integer string starting from "1"
18
+ value: values[builder.index]
19
+ )
20
+ if block_given?
21
+ yield field
22
+ else
23
+ render field.input_tag
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ protected
30
+
31
+ def build_attributes
32
+ super
33
+
34
+ attributes[:multiple] = true
35
+ end
36
+
37
+ private
38
+
39
+ def values
40
+ @values ||= Array(field.value)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -18,7 +18,8 @@ module Phlexi
18
18
 
19
19
  def build_attributes
20
20
  super
21
- attributes[:for] ||= field.dom.id
21
+
22
+ attributes.fetch(:for) { attributes[:for] = field.dom.id }
22
23
  end
23
24
  end
24
25
  end
@@ -8,6 +8,11 @@ module Phlexi
8
8
  input(**attributes, value: @checked_value)
9
9
  end
10
10
 
11
+ def extract_input(...)
12
+ # when a radio is not submitted, nothing is returned
13
+ super.compact
14
+ end
15
+
11
16
  protected
12
17
 
13
18
  def build_input_attributes
@@ -17,7 +22,7 @@ module Phlexi
17
22
  @checked_value = (attributes.key?(:checked_value) ? attributes.delete(:checked_value) : "1").to_s
18
23
 
19
24
  # this is a hack to workaround the fact that radio cannot be indexed/multiple
20
- attributes[:name] = attributes[:name].sub(/\[]$/, "")
25
+ attributes[:name] = attributes[:name].sub(/\[\]$/, "")
21
26
  attributes[:value] = @checked_value
22
27
  attributes[:checked] = attributes.fetch(:checked) { checked? }
23
28
  end
@@ -25,6 +30,11 @@ module Phlexi
25
30
  def checked?
26
31
  field.dom.value == @checked_value
27
32
  end
33
+
34
+ def normalize_input(...)
35
+ input_value = super
36
+ (input_value == @checked_value) ? input_value : nil
37
+ end
28
38
  end
29
39
  end
30
40
  end
@@ -4,11 +4,14 @@ module Phlexi
4
4
  module Form
5
5
  module Components
6
6
  class Select < Base
7
+ include Concerns::HandlesInput
8
+ include Concerns::HandlesArrayInput
7
9
  include Concerns::HasOptions
8
10
 
9
- def view_template(&block)
11
+ def view_template
12
+ input(type: :hidden, name: attributes[:name], value: "", autocomplete: "off", hidden: true) if include_hidden?
10
13
  select(**attributes) do
11
- blank_option { blank_option_text } unless skip_blank_option?
14
+ blank_option { blank_option_text } if include_blank?
12
15
  options
13
16
  end
14
17
  end
@@ -35,7 +38,8 @@ module Phlexi
35
38
  end
36
39
 
37
40
  def build_select_attributes
38
- @include_blank_option = attributes.delete(:include_blank_option)
41
+ @include_blank = attributes.delete(:include_blank)
42
+ @include_hidden = attributes.delete(:include_hidden)
39
43
 
40
44
  attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
41
45
  attributes[:required] = attributes.fetch(:required, field.required?)
@@ -48,8 +52,20 @@ module Phlexi
48
52
  field.placeholder
49
53
  end
50
54
 
51
- def skip_blank_option?
52
- @include_blank_option == false
55
+ def include_blank?
56
+ return true if @include_blank == true
57
+
58
+ @include_blank != false && !attributes[:multiple]
59
+ end
60
+
61
+ def include_hidden?
62
+ return false if @include_hidden == false
63
+
64
+ attributes[:multiple]
65
+ end
66
+
67
+ def normalize_input(input_value)
68
+ attributes[:multiple] ? normalize_array_input(input_value) : normalize_simple_input(input_value)
53
69
  end
54
70
  end
55
71
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class SubmitButton < Base
7
+ include Concerns::SubmitsForm
8
+
9
+ def view_template(&content)
10
+ content ||= proc { submit_type_label }
11
+ button(**attributes, &content)
12
+ end
13
+
14
+ protected
15
+
16
+ def build_attributes
17
+ root_key = field.dom.lineage.first.respond_to?(:dom_id) ? field.dom.lineage.first.dom_id : field.dom.lineage.first.key
18
+ attributes.fetch(:id) { attributes[:id] = "#{root_key}_submit_button" }
19
+ attributes[:class] = tokens(
20
+ component_name,
21
+ submit_type_value,
22
+ attributes[:class]
23
+ )
24
+
25
+ build_button_attributes
26
+ end
27
+
28
+ def build_button_attributes
29
+ formmethod = attributes[:formmethod]
30
+ if formmethod.present? && !/post|get/i.match?(formmethod) && !attributes.key?(:name) && !attributes.key?(:value)
31
+ attributes.merge! formmethod: :post, name: "_method", value: formmethod
32
+ end
33
+
34
+ attributes.fetch(:name) { attributes[:name] = "commit" }
35
+ attributes.fetch(:value) { attributes[:value] = submit_type_label }
36
+ attributes.fetch(:type) { attributes[:type] = :submit }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module FieldOptions
6
+ module Associations
7
+ protected
8
+
9
+ def reflection
10
+ @reflection ||= find_association_reflection
11
+ end
12
+
13
+ def find_association_reflection
14
+ if object.class.respond_to?(:reflect_on_association)
15
+ object.class.reflect_on_association(key)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -8,7 +8,7 @@ module Phlexi
8
8
  options[:autofocus] == true
9
9
  end
10
10
 
11
- def focus(autofocus = true)
11
+ def focused!(autofocus = true)
12
12
  options[:autofocus] = autofocus
13
13
  self
14
14
  end
@@ -16,20 +16,37 @@ module Phlexi
16
16
  private
17
17
 
18
18
  def infer_collection
19
- if has_validators?
20
- inclusion_validator = find_inclusion_validator
21
- collection_value_from(inclusion_validator)
22
- end
19
+ collection_value_from_association || collection_value_from_validator
23
20
  end
24
21
 
25
- def collection_value_from(inclusion_validator)
26
- if inclusion_validator
27
- inclusion_validator.options[:in] || inclusion_validator.options[:within]
22
+ def collection_value_from_association
23
+ return unless reflection
24
+
25
+ relation = reflection.klass.all
26
+
27
+ if reflection.respond_to?(:scope) && reflection.scope
28
+ relation = if reflection.scope.parameters.any?
29
+ reflection.klass.instance_exec(object, &reflection.scope)
30
+ else
31
+ reflection.klass.instance_exec(&reflection.scope)
32
+ end
33
+ else
34
+ order = reflection.options[:order]
35
+ conditions = reflection.options[:conditions]
36
+ conditions = object.instance_exec(&conditions) if conditions.respond_to?(:call)
37
+
38
+ relation = relation.where(conditions) if relation.respond_to?(:where) && conditions.present?
39
+ relation = relation.order(order) if relation.respond_to?(:order)
28
40
  end
41
+
42
+ relation
29
43
  end
30
44
 
31
- def find_inclusion_validator
32
- find_validator(:inclusion)
45
+ def collection_value_from_validator
46
+ return unless has_validators?
47
+
48
+ inclusion_validator = find_validator(:inclusion)
49
+ inclusion_validator.options[:in] || inclusion_validator.options[:within] if inclusion_validator
33
50
  end
34
51
  end
35
52
  end
@@ -76,6 +76,16 @@ module Phlexi
76
76
  def has_custom_error?
77
77
  options[:error].is_a?(String)
78
78
  end
79
+
80
+ # Determines if the associated object is in a valid state
81
+ #
82
+ # An object is considered valid if it is persisted and has no errors.
83
+ #
84
+ # @return [Boolean] true if the object is persisted and has no errors, false otherwise
85
+ def object_valid?
86
+ object.respond_to?(:persisted?) && object.persisted? &&
87
+ object.respond_to?(:errors) && !object.errors.empty?
88
+ end
79
89
  end
80
90
  end
81
91
  end
@@ -1,19 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bigdecimal"
4
+
3
5
  module Phlexi
4
6
  module Form
5
7
  module FieldOptions
6
- module Type
7
- def db_type
8
- @db_type ||= infer_db_type
8
+ module InferredTypes
9
+ def inferred_db_type
10
+ @inferred_db_type ||= infer_db_type
9
11
  end
10
12
 
11
- def input_component
12
- @input_component ||= infer_input_component
13
+ def inferred_input_component
14
+ @inferred_input_component ||= infer_input_component
13
15
  end
14
16
 
15
- def input_type
16
- @input_type ||= infer_input_type
17
+ def inferred_input_type
18
+ @inferred_input_type ||= infer_input_type(inferred_input_component)
17
19
  end
18
20
 
19
21
  private
@@ -23,7 +25,7 @@ module Phlexi
23
25
  def infer_input_component
24
26
  return :select unless collection.blank?
25
27
 
26
- case db_type
28
+ case inferred_db_type
27
29
  when :text, :json, :jsonb, :hstore
28
30
  :textarea
29
31
  else
@@ -33,10 +35,8 @@ module Phlexi
33
35
 
34
36
  # this only applies when input_component is `:input`
35
37
  # resolves the type attribute of input components
36
- def infer_input_type
37
- return nil unless input_component == :input
38
-
39
- case db_type
38
+ def infer_input_type(component)
39
+ case inferred_db_type
40
40
  when :string
41
41
  infer_string_input_type(key)
42
42
  when :integer, :float, :decimal
@@ -16,7 +16,9 @@ module Phlexi
16
16
  private
17
17
 
18
18
  def calculate_multiple_field_value
19
+ return true if reflection&.macro == :has_many
19
20
  return true if multiple_field_array_attribute?
21
+
20
22
  check_multiple_field_from_validators
21
23
  end
22
24