phlexi-form 0.3.0.rc1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -8
  3. data/gemfiles/default.gemfile.lock +1 -1
  4. data/gemfiles/rails_7.gemfile.lock +1 -1
  5. data/lib/phlexi/form/base.rb +28 -36
  6. data/lib/phlexi/form/components/base.rb +2 -2
  7. data/lib/phlexi/form/components/collection_checkboxes.rb +1 -1
  8. data/lib/phlexi/form/components/collection_radio_buttons.rb +1 -1
  9. data/lib/phlexi/form/components/concerns/extracts_input.rb +53 -0
  10. data/lib/phlexi/form/components/concerns/handles_input.rb +4 -34
  11. data/lib/phlexi/form/components/concerns/submits_form.rb +8 -0
  12. data/lib/phlexi/form/components/error.rb +1 -1
  13. data/lib/phlexi/form/components/file_input.rb +1 -0
  14. data/lib/phlexi/form/components/hint.rb +1 -1
  15. data/lib/phlexi/form/components/input.rb +1 -5
  16. data/lib/phlexi/form/components/input_array.rb +1 -1
  17. data/lib/phlexi/form/components/select.rb +0 -3
  18. data/lib/phlexi/form/components/textarea.rb +2 -3
  19. data/lib/phlexi/form/field_options/associations.rb +2 -2
  20. data/lib/phlexi/form/field_options/collection.rb +8 -8
  21. data/lib/phlexi/form/field_options/errors.rb +7 -3
  22. data/lib/phlexi/form/field_options/hints.rb +5 -1
  23. data/lib/phlexi/form/field_options/inferred_types.rb +14 -10
  24. data/lib/phlexi/form/field_options/multiple.rb +1 -1
  25. data/lib/phlexi/form/field_options/required.rb +1 -1
  26. data/lib/phlexi/form/field_options/validators.rb +2 -2
  27. data/lib/phlexi/form/structure/dom.rb +2 -2
  28. data/lib/phlexi/form/structure/field_builder.rb +59 -52
  29. data/lib/phlexi/form/structure/field_collection.rb +7 -2
  30. data/lib/phlexi/form/structure/namespace.rb +19 -14
  31. data/lib/phlexi/form/structure/namespace_collection.rb +1 -1
  32. data/lib/phlexi/form/structure/node.rb +13 -3
  33. data/lib/phlexi/form/version.rb +1 -1
  34. metadata +4 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f0a5677bd380aa749f9de4f25243f6c7367839c66a3bfe3f59dfae0f1b32ab6
4
- data.tar.gz: 9107d443cb28c47c1c0a113af182af3b2b6c0a386451e913447a3b834676bbbe
3
+ metadata.gz: d20cf0d6ccfcdf3cbb3a05a518b25a9b6176ef827e32f9cab47fddb43391f470
4
+ data.tar.gz: '0788efe146100a6629f679d913a080ff7cc2039881477bc9ffda16dfe0f6dc9f'
5
5
  SHA512:
6
- metadata.gz: 0bedfa08fae8602d5cfc632d4907fb12a41c3ac7a64deaad4cc977c82fb3451f976c37e404e449388f6f88127921fe235cec2f42ffedeadc118865c341f924d8
7
- data.tar.gz: db75b28ad2166e117054c2f5eccd5e73e0efcfc7de330d17b02e46468cf46d0aa80934ab31fbd273910c9c80a66b1fdff4184ccf88dd3cbb814b758f36023c38
6
+ metadata.gz: 3e671e22b8daa6e5e243725f79302e26b9b1d5395d92a54013d0aed981bc9eb2dfd756d8214a69446e0328186b339dc21dd3e1853c5236270f048e974c4ae114
7
+ data.tar.gz: d7441ae010f74f188baf43b6ac90e5cdd49b88563a22261e223b11d3c20de6135e5f610c0f278eaae28d49bc3627df3ec0b3012fe49d5e41626751d15200fd26
data/README.md CHANGED
@@ -159,14 +159,16 @@ Phlexi::Form supports theming through a flexible theming system:
159
159
 
160
160
  ```ruby
161
161
  class ThemedForm < Phlexi::Form::Base
162
- private
163
-
164
- def default_theme
165
- {
166
- input: "border rounded px-2 py-1",
167
- label: "font-bold text-gray-700",
168
- # Add more theme options here
169
- }
162
+ class FieldBuilder < FieldBuilder
163
+ private
164
+
165
+ def default_theme
166
+ {
167
+ input: "border rounded px-2 py-1",
168
+ label: "font-bold text-gray-700",
169
+ # Add more theme options here
170
+ }
171
+ end
170
172
  end
171
173
  end
172
174
  ```
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- phlexi-form (0.2.0)
4
+ phlexi-form (0.3.0.rc1)
5
5
  activesupport
6
6
  phlex (~> 1.11)
7
7
  zeitwerk
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- phlexi-form (0.2.0)
4
+ phlexi-form (0.3.0.rc1)
5
5
  activesupport
6
6
  phlex (~> 1.11)
7
7
  zeitwerk
@@ -10,7 +10,7 @@ module Phlexi
10
10
  # A form component for building flexible and customizable forms.
11
11
  #
12
12
  # @example Basic usage
13
- # Phlexi::Form.new(user, action: '/users', method: 'post') do |f|
13
+ # Phlexi::Form(user, action: '/users', method: 'post') do
14
14
  # render field(:name).placeholder("Name").input_tag
15
15
  # render field(:email).placeholder("Email").input_tag
16
16
  # end
@@ -22,14 +22,6 @@ module Phlexi
22
22
 
23
23
  class FieldBuilder < Structure::FieldBuilder; end
24
24
 
25
- class << self
26
- def inherited(subclass)
27
- subclass.const_set(:Namespace, Class.new(self::Namespace)) unless subclass.const_defined?(:Namespace)
28
- subclass.const_set(:FieldBuilder, Class.new(self::FieldBuilder)) unless subclass.const_defined?(:FieldBuilder)
29
- super
30
- end
31
- end
32
-
33
25
  attr_reader :key, :object
34
26
 
35
27
  delegate :field, :submit_button, :nest_one, :nest_many, to: :@namespace
@@ -73,18 +65,6 @@ module Phlexi
73
65
  instance_exec(&@_content_block) if @_content_block
74
66
  end
75
67
 
76
- # Renders the form tag with its contents.
77
- #
78
- # @yield The form's content
79
- # @return [void]
80
- def form_tag(&)
81
- form(**form_attributes) do
82
- render_hidden_method_field
83
- render_authenticity_token if authenticity_token?
84
- yield
85
- end
86
- end
87
-
88
68
  def extract_input(params)
89
69
  call unless @_rendered
90
70
  @namespace.extract_input(params)
@@ -110,10 +90,11 @@ module Phlexi
110
90
  else
111
91
  @object = record
112
92
  if @key.nil?
113
- unless object.respond_to?(:model_name) && object.model_name.respond_to?(:param_key) && object.model_name.param_key.present?
114
- raise ArgumentError, "record must respond to #model_name.param_key with a non nil value or set `key` option e.g. Phlexi::Form(record, key: :record)"
93
+ @key = if object.respond_to?(:model_name) && object.model_name.respond_to?(:param_key) && object.model_name.param_key.present?
94
+ object.model_name.param_key
95
+ else
96
+ object.class.name.demodulize.underscore
115
97
  end
116
- @key = object.model_name.param_key
117
98
  end
118
99
  end
119
100
  @key = @key.to_sym
@@ -133,6 +114,23 @@ module Phlexi
133
114
  attributes.fetch(:accept_charset) { attributes[:accept_charset] = "UTF-8" }
134
115
  end
135
116
 
117
+ # Retrieves the form's CSS classes.
118
+ #
119
+ # @return [String] The form's CSS classes
120
+ attr_reader :form_class
121
+
122
+ # Renders the form tag with its contents.
123
+ #
124
+ # @yield The form's content
125
+ # @return [void]
126
+ def form_tag(&)
127
+ form(**form_attributes) do
128
+ render_hidden_method_field
129
+ render_authenticity_token if has_authenticity_token?
130
+ yield
131
+ end
132
+ end
133
+
136
134
  # Determines the form's action URL.
137
135
  #
138
136
  # @return [String, nil] The form's action URL
@@ -156,16 +154,11 @@ module Phlexi
156
154
  ActiveSupport::StringInquirer.new(@form_method)
157
155
  end
158
156
 
159
- # Retrieves the form's CSS classes.
160
- #
161
- # @return [String] The form's CSS classes
162
- attr_reader :form_class
163
-
164
157
  # Checks if the authenticity token should be included.
165
158
  #
166
159
  # @return [Boolean] True if the authenticity token should be included, false otherwise
167
- def authenticity_token?
168
- defined?(helpers) && options.fetch(:authenticity_token) { !form_method.get? }
160
+ def has_authenticity_token?
161
+ !form_method.get? && ((defined?(helpers) && helpers) || options[:authenticity_token])
169
162
  end
170
163
 
171
164
  # Retrieves the authenticity token.
@@ -221,13 +214,12 @@ module Phlexi
221
214
  #
222
215
  # @return [Hash] The form attributes
223
216
  def form_attributes
224
- {
217
+ mix({
225
218
  id: @namespace.dom_id,
226
- action: form_action,
227
- method: standardized_form_method,
228
219
  class: form_class,
229
- **attributes
230
- }
220
+ action: form_action,
221
+ method: standardized_form_method
222
+ }, attributes)
231
223
  end
232
224
 
233
225
  # Renders the authenticity token if required.
@@ -23,15 +23,15 @@ module Phlexi
23
23
  def append_attribute_classes
24
24
  return if attributes[:class] == false
25
25
 
26
- attributes[:class] = tokens(
26
+ default_classes = tokens(
27
27
  component_name,
28
- attributes[:class],
29
28
  -> { attributes[:required] } => "required",
30
29
  -> { !attributes[:required] } => "optional",
31
30
  -> { field.has_errors? } => "invalid",
32
31
  -> { attributes[:readonly] } => "readonly",
33
32
  -> { attributes[:disabled] } => "disabled"
34
33
  )
34
+ attributes[:class] = tokens(default_classes, attributes[:class])
35
35
  end
36
36
 
37
37
  def component_name
@@ -10,7 +10,7 @@ module Phlexi
10
10
 
11
11
  def view_template
12
12
  div(**attributes.slice(:id, :class)) do
13
- field.multi(option_mapper.values) do |builder|
13
+ field.repeated(option_mapper.values) do |builder|
14
14
  render builder.hidden_field_tag if builder.index == 0
15
15
 
16
16
  field = builder.field(
@@ -9,7 +9,7 @@ module Phlexi
9
9
 
10
10
  def view_template
11
11
  div(**attributes.slice(:id, :class)) do
12
- field.multi(option_mapper.values) do |builder|
12
+ field.repeated(option_mapper.values) do |builder|
13
13
  render builder.hidden_field_tag if builder.index == 0
14
14
 
15
15
  field = builder.field(
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ module Concerns
7
+ module ExtractsInput
8
+ # Collects parameters matching the input field's param key.
9
+ #
10
+ # @param params [Hash] the parameters to collect from.
11
+ # @return [Hash] the collected parameters.
12
+ def extract_input(params)
13
+ # # Handles multi parameter attributes
14
+ # # https://www.cookieshq.co.uk/posts/rails-spelunking-date-select
15
+ # # https://www.cookieshq.co.uk/posts/multiparameter-attributes
16
+
17
+ # # Matches
18
+ # # - parameter
19
+ # # - parameter(1)
20
+ # # - parameter(2)
21
+ # # - parameter(1i)
22
+ # # - parameter(2f)
23
+ # regex = /^#{param}(\(\d+[if]?\))?$/
24
+ # keys = params.select { |key, _| regex.match?(key) }.keys
25
+ # params.slice(*keys)
26
+
27
+ params ||= {}
28
+ {input_param => normalize_input(params[field.key])}
29
+ end
30
+
31
+ protected
32
+
33
+ def build_attributes
34
+ super
35
+ @input_param = attributes.delete(:input_param) || field.key
36
+ end
37
+
38
+ def input_param
39
+ @input_param
40
+ end
41
+
42
+ def normalize_input(input_value)
43
+ normalize_simple_input(input_value)
44
+ end
45
+
46
+ def normalize_simple_input(input_value)
47
+ input_value.to_s.presence
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -5,46 +5,16 @@ module Phlexi
5
5
  module Components
6
6
  module Concerns
7
7
  module HandlesInput
8
- # Collects parameters matching the input field's param key.
9
- #
10
- # @param params [Hash] the parameters to collect from.
11
- # @return [Hash] the collected parameters.
12
- def extract_input(params)
13
- # # Handles multi parameter attributes
14
- # # https://www.cookieshq.co.uk/posts/rails-spelunking-date-select
15
- # # https://www.cookieshq.co.uk/posts/multiparameter-attributes
16
-
17
- # # Matches
18
- # # - parameter
19
- # # - parameter(1)
20
- # # - parameter(2)
21
- # # - parameter(1i)
22
- # # - parameter(2f)
23
- # regex = /^#{param}(\(\d+[if]?\))?$/
24
- # keys = params.select { |key, _| regex.match?(key) }.keys
25
- # params.slice(*keys)
26
-
27
- params ||= {}
28
- {input_param => normalize_input(params[field.key])}
29
- end
8
+ include Phlexi::Form::Components::Concerns::ExtractsInput
30
9
 
31
10
  protected
32
11
 
33
12
  def build_attributes
34
13
  super
35
- @input_param = attributes.delete(:input_param) || field.key
36
- end
37
-
38
- def input_param
39
- @input_param
40
- end
41
-
42
- def normalize_input(input_value)
43
- normalize_simple_input(input_value)
44
- end
45
14
 
46
- def normalize_simple_input(input_value)
47
- input_value.to_s.presence
15
+ # only overwrite id if it was set in Base
16
+ attributes[:id] = field.dom.id if attributes[:id] == "#{field.dom.id}_#{component_name}"
17
+ attributes[:name] = field.dom.name
48
18
  end
49
19
  end
50
20
  end
@@ -5,6 +5,14 @@ module Phlexi
5
5
  module Components
6
6
  module Concerns
7
7
  module SubmitsForm
8
+ include Phlexi::Form::Components::Concerns::ExtractsInput
9
+
10
+ def extract_input(params)
11
+ {}
12
+ end
13
+
14
+ protected
15
+
8
16
  def submit_type_value
9
17
  if field.object.respond_to?(:persisted?)
10
18
  field.object.persisted? ? :update : :create
@@ -13,7 +13,7 @@ module Phlexi
13
13
  private
14
14
 
15
15
  def render?
16
- field.show_errors? && field.has_errors?
16
+ field.show_errors?
17
17
  end
18
18
  end
19
19
  end
@@ -14,6 +14,7 @@ module Phlexi
14
14
  def build_input_attributes
15
15
  attributes[:type] = :file
16
16
  super
17
+ # ensure we are always setting it to false
17
18
  attributes[:value] = false
18
19
  end
19
20
 
@@ -13,7 +13,7 @@ module Phlexi
13
13
  private
14
14
 
15
15
  def render?
16
- field.hint.present? && (!field.show_errors? || !field.has_errors?)
16
+ field.show_hint?
17
17
  end
18
18
  end
19
19
  end
@@ -15,17 +15,13 @@ module Phlexi
15
15
  def build_attributes
16
16
  super
17
17
 
18
- # only overwrite id if it was set in Base
19
- attributes[:id] = field.dom.id if attributes[:id] == "#{field.dom.id}_#{component_name}"
20
-
21
- attributes[:name] = field.dom.name
22
18
  attributes[:value] = field.dom.value
23
19
 
24
20
  build_input_attributes
25
21
  end
26
22
 
27
23
  def build_input_attributes
28
- attributes.fetch(:type) { attributes[:type] = field.inferred_input_type }
24
+ attributes.fetch(:type) { attributes[:type] = field.inferred_input_component_subtype }
29
25
  attributes.fetch(:disabled) { attributes[:disabled] = field.disabled? }
30
26
 
31
27
  case attributes[:type]
@@ -9,7 +9,7 @@ module Phlexi
9
9
 
10
10
  def view_template
11
11
  div(**attributes.slice(:id, :class)) do
12
- field.multi(values.length) do |builder|
12
+ field.repeated(values.length) do |builder|
13
13
  render builder.hidden_field_tag if builder.index == 0
14
14
 
15
15
  field = builder.field(
@@ -31,9 +31,6 @@ module Phlexi
31
31
  def build_attributes
32
32
  super
33
33
 
34
- attributes[:id] = field.dom.id
35
- attributes[:name] = field.dom.name
36
-
37
34
  build_select_attributes
38
35
  end
39
36
 
@@ -4,6 +4,8 @@ module Phlexi
4
4
  module Form
5
5
  module Components
6
6
  class Textarea < Base
7
+ include Concerns::HandlesInput
8
+
7
9
  def view_template
8
10
  textarea(**attributes) { field.dom.value }
9
11
  end
@@ -13,9 +15,6 @@ module Phlexi
13
15
  def build_attributes
14
16
  super
15
17
 
16
- attributes[:id] = field.dom.id
17
- attributes[:name] = field.dom.name
18
-
19
18
  build_textarea_attributes
20
19
  end
21
20
 
@@ -6,8 +6,8 @@ module Phlexi
6
6
  module Associations
7
7
  protected
8
8
 
9
- def reflection
10
- @reflection ||= find_association_reflection
9
+ def association_reflection
10
+ @association_reflection ||= find_association_reflection
11
11
  end
12
12
 
13
13
  def find_association_reflection
@@ -20,19 +20,19 @@ module Phlexi
20
20
  end
21
21
 
22
22
  def collection_value_from_association
23
- return unless reflection
23
+ return unless association_reflection
24
24
 
25
- relation = reflection.klass.all
25
+ relation = association_reflection.klass.all
26
26
 
27
- if reflection.respond_to?(:scope) && reflection.scope
28
- relation = if reflection.scope.parameters.any?
29
- reflection.klass.instance_exec(object, &reflection.scope)
27
+ if association_reflection.respond_to?(:scope) && association_reflection.scope
28
+ relation = if association_reflection.scope.parameters.any?
29
+ association_reflection.klass.instance_exec(object, &association_reflection.scope)
30
30
  else
31
- reflection.klass.instance_exec(&reflection.scope)
31
+ association_reflection.klass.instance_exec(&association_reflection.scope)
32
32
  end
33
33
  else
34
- order = reflection.options[:order]
35
- conditions = reflection.options[:conditions]
34
+ order = association_reflection.options[:order]
35
+ conditions = association_reflection.options[:conditions]
36
36
  conditions = object.instance_exec(&conditions) if conditions.respond_to?(:call)
37
37
 
38
38
  relation = relation.where(conditions) if relation.respond_to?(:where) && conditions.present?
@@ -21,10 +21,14 @@ module Phlexi
21
21
  object_with_errors? || !object && has_custom_error?
22
22
  end
23
23
 
24
- def show_errors?
24
+ def can_show_errors?
25
25
  options[:error] != false
26
26
  end
27
27
 
28
+ def show_errors?
29
+ can_show_errors? && has_errors?
30
+ end
31
+
28
32
  def valid?
29
33
  !has_errors? && has_value?
30
34
  end
@@ -66,11 +70,11 @@ module Phlexi
66
70
  end
67
71
 
68
72
  def errors_on_association
69
- reflection ? object.errors[reflection.name] : []
73
+ association_reflection ? object.errors[association_reflection.name] : []
70
74
  end
71
75
 
72
76
  def full_errors_on_association
73
- reflection ? object.errors.full_messages_for(reflection.name) : []
77
+ association_reflection ? object.errors.full_messages_for(association_reflection.name) : []
74
78
  end
75
79
 
76
80
  def has_custom_error?
@@ -14,7 +14,11 @@ module Phlexi
14
14
  end
15
15
 
16
16
  def has_hint?
17
- options[:hint] != false && hint.present?
17
+ hint.present?
18
+ end
19
+
20
+ def show_hint?
21
+ has_hint? && !show_errors?
18
22
  end
19
23
  end
20
24
  end
@@ -10,20 +10,22 @@ module Phlexi
10
10
  @inferred_db_type ||= infer_db_type
11
11
  end
12
12
 
13
- def inferred_input_component
14
- @inferred_input_component ||= infer_input_component
13
+ # This will give you the component type e.g. input, textarea, select
14
+ def inferred_component_type
15
+ @inferred_component_type ||= infer_component_type
15
16
  end
16
17
 
17
- def inferred_input_type
18
- @inferred_input_type ||= infer_input_type(inferred_input_component)
18
+ # This will give you the subtype of the input component e.g input type="text|date|checkbox" etc
19
+ def inferred_input_component_subtype
20
+ @inferred_input_component_subtype ||= infer_input_component_subtype(inferred_component_type)
19
21
  end
20
22
 
21
23
  private
22
24
 
23
25
  # this returns the element type
24
- # one of :input, :textarea, :select, :botton
25
- def infer_input_component
26
- return :select unless collection.blank?
26
+ # one of :input, :textarea, :select
27
+ def infer_component_type
28
+ return :select unless collection.nil?
27
29
 
28
30
  case inferred_db_type
29
31
  when :text, :json, :jsonb, :hstore
@@ -35,7 +37,7 @@ module Phlexi
35
37
 
36
38
  # this only applies when input_component is `:input`
37
39
  # resolves the type attribute of input components
38
- def infer_input_type(component)
40
+ def infer_input_component_subtype(component)
39
41
  case inferred_db_type
40
42
  when :string
41
43
  infer_string_input_type(key)
@@ -64,7 +66,7 @@ module Phlexi
64
66
  if object.class.respond_to?(:attribute_types)
65
67
  # ActiveModel::Attributes
66
68
  custom_type = object.class.attribute_types[key.to_s]
67
- return custom_type.type if custom_type
69
+ return custom_type.type if custom_type&.type
68
70
  end
69
71
 
70
72
  # Check if object responds to the key
@@ -81,8 +83,10 @@ module Phlexi
81
83
  case value
82
84
  when Integer
83
85
  :integer
84
- when Float, BigDecimal
86
+ when Float
85
87
  :float
88
+ when BigDecimal
89
+ :decimal
86
90
  when TrueClass, FalseClass
87
91
  :boolean
88
92
  when Date
@@ -16,7 +16,7 @@ module Phlexi
16
16
  private
17
17
 
18
18
  def calculate_multiple_field_value
19
- return true if reflection&.macro == :has_many
19
+ return true if association_reflection&.macro == :has_many
20
20
  return true if multiple_field_array_attribute?
21
21
 
22
22
  check_multiple_field_from_validators
@@ -24,7 +24,7 @@ module Phlexi
24
24
  end
25
25
 
26
26
  def required_by_validators?
27
- (attribute_validators + reflection_validators).any? { |v| v.kind == :presence && valid_validator?(v) }
27
+ (attribute_validators + association_reflection_validators).any? { |v| v.kind == :presence && valid_validator?(v) }
28
28
  end
29
29
 
30
30
  def required_by_default?
@@ -14,8 +14,8 @@ module Phlexi
14
14
  object.class.validators_on(key)
15
15
  end
16
16
 
17
- def reflection_validators
18
- reflection ? object.class.validators_on(reflection.name) : []
17
+ def association_reflection_validators
18
+ association_reflection ? object.class.validators_on(association_reflection.name) : []
19
19
  end
20
20
 
21
21
  def valid_validator?(validator)
@@ -5,7 +5,7 @@ module Phlexi
5
5
  module Structure
6
6
  # Generates DOM IDs, names, etc. for a Field, Namespace, or Node based on
7
7
  # norms that were established by Rails. These can be used outsidef or Rails in
8
- # other Ruby web frameworks since it has now dependencies on Rails.
8
+ # other Ruby web frameworks since it has no dependencies on Rails.
9
9
  class DOM
10
10
  def initialize(field:)
11
11
  @field = field
@@ -17,7 +17,7 @@ module Phlexi
17
17
  @field.value.to_s
18
18
  end
19
19
 
20
- # Walks from the current node to the parent node, grabs the names, and seperates
20
+ # Walks from the current node to the parent node, grabs the names, and separates
21
21
  # them with a `_` for a DOM ID.
22
22
  def id
23
23
  @id ||= begin
@@ -57,28 +57,28 @@ module Phlexi
57
57
  #
58
58
  # @param attributes [Hash] Additional attributes for the label.
59
59
  # @return [Components::Label] The label component.
60
- def label_tag(**attributes)
61
- create_component(Components::Label, :label, **attributes)
60
+ def label_tag(**, &)
61
+ create_component(Components::Label, :label, **, &)
62
62
  end
63
63
 
64
64
  # Creates an input tag for the field.
65
65
  #
66
66
  # @param attributes [Hash] Additional attributes for the input.
67
67
  # @return [Components::Input] The input component.
68
- def input_tag(**attributes)
69
- create_component(Components::Input, :input, **attributes)
68
+ def input_tag(**, &)
69
+ create_component(Components::Input, :input, **, &)
70
70
  end
71
71
 
72
- def file_input_tag(**attributes)
73
- create_component(Components::FileInput, :file, **attributes)
72
+ def file_input_tag(**, &)
73
+ create_component(Components::FileInput, :file, **, &)
74
74
  end
75
75
 
76
76
  # Creates a checkbox tag for the field.
77
77
  #
78
78
  # @param attributes [Hash] Additional attributes for the checkbox.
79
79
  # @return [Components::Checkbox] The checkbox component.
80
- def checkbox_tag(**attributes)
81
- create_component(Components::Checkbox, :checkbox, **attributes)
80
+ def checkbox_tag(**, &)
81
+ create_component(Components::Checkbox, :checkbox, **, &)
82
82
  end
83
83
 
84
84
  # Creates collection checkboxes for the field.
@@ -86,16 +86,16 @@ module Phlexi
86
86
  # @param attributes [Hash] Additional attributes for the collection checkboxes.
87
87
  # @yield [block] The block to be executed for each checkbox.
88
88
  # @return [Components::CollectionCheckboxes] The collection checkboxes component.
89
- def collection_checkboxes_tag(**attributes, &)
90
- create_component(Components::CollectionCheckboxes, :collection_checkboxes, **attributes, &)
89
+ def collection_checkboxes_tag(**, &)
90
+ create_component(Components::CollectionCheckboxes, :collection_checkboxes, **, &)
91
91
  end
92
92
 
93
93
  # Creates a radio button tag for the field.
94
94
  #
95
95
  # @param attributes [Hash] Additional attributes for the radio button.
96
96
  # @return [Components::RadioButton] The radio button component.
97
- def radio_button_tag(**attributes)
98
- create_component(Components::RadioButton, :radio, **attributes)
97
+ def radio_button_tag(**, &)
98
+ create_component(Components::RadioButton, :radio, **, &)
99
99
  end
100
100
 
101
101
  # Creates collection radio buttons for the field.
@@ -103,52 +103,52 @@ module Phlexi
103
103
  # @param attributes [Hash] Additional attributes for the collection radio buttons.
104
104
  # @yield [block] The block to be executed for each radio button.
105
105
  # @return [Components::CollectionRadioButtons] The collection radio buttons component.
106
- def collection_radio_buttons_tag(**attributes, &)
107
- create_component(Components::CollectionRadioButtons, :collection_radio_buttons, **attributes, &)
106
+ def collection_radio_buttons_tag(**, &)
107
+ create_component(Components::CollectionRadioButtons, :collection_radio_buttons, **, &)
108
108
  end
109
109
 
110
110
  # Creates a textarea tag for the field.
111
111
  #
112
112
  # @param attributes [Hash] Additional attributes for the textarea.
113
113
  # @return [Components::Textarea] The textarea component.
114
- def textarea_tag(**attributes)
115
- create_component(Components::Textarea, :textarea, **attributes)
114
+ def textarea_tag(**, &)
115
+ create_component(Components::Textarea, :textarea, **, &)
116
116
  end
117
117
 
118
118
  # Creates a select tag for the field.
119
119
  #
120
120
  # @param attributes [Hash] Additional attributes for the select.
121
121
  # @return [Components::Select] The select component.
122
- def select_tag(**attributes)
123
- create_component(Components::Select, :select, **attributes)
122
+ def select_tag(**, &)
123
+ create_component(Components::Select, :select, **, &)
124
124
  end
125
125
 
126
- def input_array_tag(**attributes)
127
- create_component(Components::InputArray, :array, **attributes)
126
+ def input_array_tag(**, &)
127
+ create_component(Components::InputArray, :array, **, &)
128
128
  end
129
129
 
130
130
  # Creates a hint tag for the field.
131
131
  #
132
132
  # @param attributes [Hash] Additional attributes for the hint.
133
133
  # @return [Components::Hint] The hint component.
134
- def hint_tag(**attributes)
135
- create_component(Components::Hint, :hint, **attributes)
134
+ def hint_tag(**, &)
135
+ create_component(Components::Hint, :hint, **, &)
136
136
  end
137
137
 
138
138
  # Creates an error tag for the field.
139
139
  #
140
140
  # @param attributes [Hash] Additional attributes for the error.
141
141
  # @return [Components::Error] The error component.
142
- def error_tag(**attributes)
143
- create_component(Components::Error, :error, **attributes)
142
+ def error_tag(**, &)
143
+ create_component(Components::Error, :error, **, &)
144
144
  end
145
145
 
146
146
  # Creates a full error tag for the field.
147
147
  #
148
148
  # @param attributes [Hash] Additional attributes for the full error.
149
149
  # @return [Components::FullError] The full error component.
150
- def full_error_tag(**attributes)
151
- create_component(Components::FullError, :full_error, **attributes)
150
+ def full_error_tag(**, &)
151
+ create_component(Components::FullError, :full_error, **, &)
152
152
  end
153
153
 
154
154
  # Wraps the field with additional markup.
@@ -158,17 +158,18 @@ module Phlexi
158
158
  # @yield [block] The block to be executed within the wrapper.
159
159
  # @return [Components::Wrapper] The wrapper component.
160
160
  def wrapped(inner: {}, **attributes, &)
161
- wrapper_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :wrapper)
162
- inner[:class] = inner.delete(:class) || themed(inner.delete(:theme) || :inner_wrapper)
163
- Components::Wrapper.new(self, class: wrapper_class, inner: inner, **attributes, &)
161
+ attributes = apply_component_theme(attributes, :wrapper)
162
+ inner = apply_component_theme(inner, :inner_wrapper)
163
+ Components::Wrapper.new(self, inner: inner, **attributes, &)
164
164
  end
165
165
 
166
- # Creates a multi-value field collection.
166
+ # Creates a repeated field collection.
167
167
  #
168
- # @param range [Integer, #to_a] The range of keys for each field. If an integer is passed, keys will begin from 1.
168
+ # @param range [Integer, #to_a] The range of keys for each field.
169
+ # If an integer (e.g. 6) is passed, it is converted to a range = 1..6
169
170
  # @yield [block] The block to be executed for each item in the collection.
170
171
  # @return [FieldCollection] The field collection.
171
- def multi(range = nil, &)
172
+ def repeated(range = nil, &)
172
173
  FieldCollection.new(field: self, range: range, &)
173
174
  end
174
175
 
@@ -176,31 +177,37 @@ module Phlexi
176
177
  #
177
178
  # @param attributes [Hash] Additional attributes for the submit.
178
179
  # @return [Components::SubmitButton] The submit button component.
179
- def submit_button_tag(**attributes, &)
180
- create_component(Components::SubmitButton, :submit_button, **attributes, &)
180
+ def submit_button_tag(**, &)
181
+ create_component(Components::SubmitButton, :submit_button, **, &)
181
182
  end
182
183
 
183
184
  def extract_input(params)
184
- raise "field##{dom.name} did not define an input component" unless @field_input_component
185
+ raise "field##{dom.name} did not define an input component" unless @field_input_extractor
185
186
 
186
- @field_input_component.extract_input(params)
187
+ @field_input_extractor.extract_input(params)
187
188
  end
188
189
 
189
190
  protected
190
191
 
191
- def create_component(component_class, theme_key, **attributes)
192
- if component_class.include?(Phlexi::Form::Components::Concerns::HandlesInput)
193
- raise "input component already defined: #{@field_input_component.inspect}" if @field_input_component
192
+ def create_component(component_class, theme_key, **attributes, &)
193
+ attributes = mix(input_attributes, attributes) if component_class.include?(Phlexi::Form::Components::Concerns::HandlesInput)
194
+ component = component_class.new(self, **apply_component_theme(attributes, theme_key), &)
195
+ if component_class.include?(Components::Concerns::ExtractsInput)
196
+ raise "input component already defined: #{@field_input_extractor.inspect}" if @field_input_extractor
194
197
 
195
- attributes = input_attributes.deep_merge(attributes)
196
- @field_input_component = component_class.new(self, class: component_class_for(theme_key, attributes), **attributes)
197
- else
198
- component_class.new(self, class: component_class_for(theme_key, attributes), **attributes)
198
+ @field_input_extractor = component
199
199
  end
200
+
201
+ component
200
202
  end
201
203
 
202
- def component_class_for(theme_key, attributes)
203
- attributes.delete(:class) || themed(attributes.key?(:theme) ? attributes.delete(:theme) : theme_key)
204
+ def apply_component_theme(attributes, theme_key)
205
+ theme_key = attributes.delete(:theme) || theme_key
206
+ if attributes.key?(:class!)
207
+ attributes
208
+ else
209
+ mix({class: themed(theme_key)}, attributes)
210
+ end
204
211
  end
205
212
 
206
213
  def has_value?
@@ -210,24 +217,24 @@ module Phlexi
210
217
  def determine_initial_value(value)
211
218
  return value unless value == NIL_VALUE
212
219
 
213
- determine_from_association || determine_value_from_object
220
+ determine_value_from_association || determine_value_from_object
214
221
  end
215
222
 
216
223
  def determine_value_from_object
217
224
  object.respond_to?(key) ? object.public_send(key) : nil
218
225
  end
219
226
 
220
- def determine_from_association
221
- return nil unless reflection.present?
227
+ def determine_value_from_association
228
+ return nil unless association_reflection.present?
222
229
 
223
230
  value = object.public_send(key)
224
- case reflection.macro
231
+ case association_reflection.macro
225
232
  when :has_many, :has_and_belongs_to_many
226
- value&.map { |v| v.public_send(reflection.klass.primary_key) }
233
+ value&.map { |v| v.public_send(association_reflection.klass.primary_key) }
227
234
  when :belongs_to, :has_one
228
- value&.public_send(reflection.klass.primary_key)
235
+ value&.public_send(association_reflection.klass.primary_key)
229
236
  else
230
- raise ArgumentError, "Unsupported association type: #{reflection.macro}"
237
+ raise ArgumentError, "Unsupported association type: #{association_reflection.macro}"
231
238
  end
232
239
  end
233
240
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "phlex"
4
+
3
5
  module Phlexi
4
6
  module Form
5
7
  module Structure
@@ -7,6 +9,8 @@ module Phlexi
7
9
  include Enumerable
8
10
 
9
11
  class Builder
12
+ include Phlex::Helpers
13
+
10
14
  attr_reader :key, :index
11
15
 
12
16
  def initialize(key, field, index)
@@ -15,8 +19,9 @@ module Phlexi
15
19
  @index = index
16
20
  end
17
21
 
18
- def field(**)
19
- @field.class.new(key, input_attributes: @field.input_attributes, **, parent: @field).tap do |field|
22
+ def field(**options)
23
+ options = mix({input_attributes: @field.input_attributes}, options)
24
+ @field.class.new(key, **options, parent: @field).tap do |field|
20
25
  yield field if block_given?
21
26
  end
22
27
  end
@@ -3,13 +3,18 @@
3
3
  module Phlexi
4
4
  module Form
5
5
  module Structure
6
- # A Namespace maps and object to values, but doesn't actually have a value itself. For
6
+ # A Namespace maps an object to values, but doesn't actually have a value itself. For
7
7
  # example, a `User` object or ActiveRecord model could be passed into the `:user` namespace.
8
- # To access the values on a Namespace, the `field` can be called for single values.
9
8
  #
10
- # Additionally, to access namespaces within a namespace, such as if a `User has_many :addresses` in
11
- # ActiveRecord, the `namespace` method can be called which will return another Namespace object and
12
- # set the current Namespace as the parent.
9
+ # To access single values on a Namespace, #field can be used.
10
+ #
11
+ # To access nested objects within a namespace, two methods are available:
12
+ #
13
+ # 1. #nest_one: Used for single nested objects, such as if a `User belongs_to :profile` in
14
+ # ActiveRecord. This method returns another Namespace object.
15
+ #
16
+ # 2. #nest_many: Used for collections of nested objects, such as if a `User has_many :addresses` in
17
+ # ActiveRecord. This method returns a NamespaceCollection object.
13
18
  class Namespace < Structure::Node
14
19
  include Enumerable
15
20
 
@@ -29,8 +34,8 @@ module Phlexi
29
34
  end
30
35
  end
31
36
 
32
- def submit_button(key = nil, **attributes, &)
33
- field(key || SecureRandom.hex).submit_button_tag(**attributes, &)
37
+ def submit_button(key = :submit_button, **, &)
38
+ field(key).submit_button_tag(**, &)
34
39
  end
35
40
 
36
41
  # Creates a `Namespace` child instance with the parent set to the current instance, adds to
@@ -41,9 +46,9 @@ module Phlexi
41
46
  # form like this:
42
47
  #
43
48
  # ```ruby
44
- # Superform :user, object: User.new do |form|
45
- # form.nest_one :permission do |permission|
46
- # form.field :role
49
+ # Phlexi::Form(User.new, as: :user) do
50
+ # nest_one :profile do |profile|
51
+ # render profile.field(:gender).input_tag
47
52
  # end
48
53
  # end
49
54
  # ```
@@ -56,10 +61,10 @@ module Phlexi
56
61
  # an enumerable or array of `Address` classes:
57
62
  #
58
63
  # ```ruby
59
- # Phlexi::Form.new User.new do |form|
60
- # render form.field(:email).input_tag
61
- # render form.field(:name).input_tag
62
- # form.nest_many :addresses do |address|
64
+ # Phlexi::Form(User.new) do
65
+ # render field(:email).input_tag
66
+ # render field(:name).input_tag
67
+ # nest_many :addresses do |address|
63
68
  # render address.field(:street).input_tag
64
69
  # render address.field(:state).input_tag
65
70
  # render address.field(:zip).input_tag
@@ -32,7 +32,7 @@ module Phlexi
32
32
 
33
33
  # Builds and memoizes namespaces for the collection.
34
34
  #
35
- # @return [Array<Hash>] An array of namespace hashes.
35
+ # @return [Array<Namespace>] An array of namespace objects.
36
36
  def namespaces
37
37
  @namespaces ||= @collection.map.with_index do |object, key|
38
38
  build_namespace(key, object: object)
@@ -3,15 +3,25 @@
3
3
  module Phlexi
4
4
  module Form
5
5
  module Structure
6
- # Superclass for Namespace and Field classes. Not much to it other than it has a `name`
7
- # and `parent` node attribute. Think of it as a tree.
6
+ # Superclass for Namespace and Field classes. Represents a node in the form tree structure.
7
+ #
8
+ # @attr_reader [Symbol] key The node's key
9
+ # @attr_reader [Node, nil] parent The node's parent in the tree structure
8
10
  class Node
9
11
  attr_reader :key, :parent
10
12
 
13
+ # Initializes a new Node instance.
14
+ #
15
+ # @param key [Symbol, String] The key for the node
16
+ # @param parent [Node, nil] The parent node
11
17
  def initialize(key, parent:)
12
- @key = key.to_s.to_sym
18
+ @key = :"#{key}"
13
19
  @parent = parent
14
20
  end
21
+
22
+ def inspect
23
+ "<#{self.class.name} key=#{key.inspect} parent=#{id.inspect} />"
24
+ end
15
25
  end
16
26
  end
17
27
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlexi
4
4
  module Form
5
- VERSION = "0.3.0.rc1"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlexi-form
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0.rc1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-08 00:00:00.000000000 Z
11
+ date: 2024-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: phlex
@@ -194,6 +194,7 @@ files:
194
194
  - lib/phlexi/form/components/checkbox.rb
195
195
  - lib/phlexi/form/components/collection_checkboxes.rb
196
196
  - lib/phlexi/form/components/collection_radio_buttons.rb
197
+ - lib/phlexi/form/components/concerns/extracts_input.rb
197
198
  - lib/phlexi/form/components/concerns/handles_array_input.rb
198
199
  - lib/phlexi/form/components/concerns/handles_input.rb
199
200
  - lib/phlexi/form/components/concerns/has_options.rb
@@ -260,7 +261,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
260
261
  - !ruby/object:Gem::Version
261
262
  version: '0'
262
263
  requirements: []
263
- rubygems_version: 3.5.11
264
+ rubygems_version: 3.4.10
264
265
  signing_key:
265
266
  specification_version: 4
266
267
  summary: Build forms in Rails