phlexi-form 0.3.0.rc1 → 0.4.0

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -8
  3. data/gemfiles/default.gemfile.lock +34 -32
  4. data/gemfiles/rails_7.gemfile.lock +16 -18
  5. data/lib/phlexi/form/base.rb +42 -41
  6. data/lib/phlexi/form/builder.rb +297 -0
  7. data/lib/phlexi/form/components/base.rb +3 -3
  8. data/lib/phlexi/form/components/collection_checkboxes.rb +1 -1
  9. data/lib/phlexi/form/components/collection_radio_buttons.rb +1 -1
  10. data/lib/phlexi/form/components/concerns/extracts_input.rb +53 -0
  11. data/lib/phlexi/form/components/concerns/handles_input.rb +4 -34
  12. data/lib/phlexi/form/components/concerns/submits_form.rb +8 -0
  13. data/lib/phlexi/form/components/error.rb +1 -1
  14. data/lib/phlexi/form/components/file_input.rb +1 -0
  15. data/lib/phlexi/form/components/hint.rb +1 -1
  16. data/lib/phlexi/form/components/input.rb +16 -6
  17. data/lib/phlexi/form/components/input_array.rb +1 -1
  18. data/lib/phlexi/form/components/select.rb +4 -3
  19. data/lib/phlexi/form/components/textarea.rb +2 -3
  20. data/lib/phlexi/form/html.rb +18 -0
  21. data/lib/phlexi/form/{field_options → options}/autofocus.rb +1 -1
  22. data/lib/phlexi/form/{field_options → options}/collection.rb +14 -10
  23. data/lib/phlexi/form/{field_options → options}/disabled.rb +1 -1
  24. data/lib/phlexi/form/{field_options → options}/errors.rb +18 -14
  25. data/lib/phlexi/form/options/hints.rb +13 -0
  26. data/lib/phlexi/form/options/inferred_types.rb +32 -0
  27. data/lib/phlexi/form/{field_options → options}/length.rb +3 -3
  28. data/lib/phlexi/form/{field_options → options}/limit.rb +2 -2
  29. data/lib/phlexi/form/options/max.rb +55 -0
  30. data/lib/phlexi/form/options/min.rb +55 -0
  31. data/lib/phlexi/form/{field_options → options}/pattern.rb +2 -2
  32. data/lib/phlexi/form/{field_options → options}/readonly.rb +1 -1
  33. data/lib/phlexi/form/{field_options → options}/required.rb +3 -3
  34. data/lib/phlexi/form/options/step.rb +39 -0
  35. data/lib/phlexi/form/options/validators.rb +24 -0
  36. data/lib/phlexi/form/structure/field_collection.rb +12 -27
  37. data/lib/phlexi/form/structure/namespace.rb +4 -111
  38. data/lib/phlexi/form/structure/namespace_collection.rb +1 -32
  39. data/lib/phlexi/form/theme.rb +160 -0
  40. data/lib/phlexi/form/version.rb +1 -1
  41. data/lib/phlexi/form.rb +3 -6
  42. metadata +36 -24
  43. data/lib/phlexi/form/field_options/associations.rb +0 -21
  44. data/lib/phlexi/form/field_options/hints.rb +0 -22
  45. data/lib/phlexi/form/field_options/inferred_types.rb +0 -155
  46. data/lib/phlexi/form/field_options/labels.rb +0 -28
  47. data/lib/phlexi/form/field_options/min_max.rb +0 -92
  48. data/lib/phlexi/form/field_options/multiple.rb +0 -65
  49. data/lib/phlexi/form/field_options/placeholder.rb +0 -18
  50. data/lib/phlexi/form/field_options/themes.rb +0 -207
  51. data/lib/phlexi/form/field_options/validators.rb +0 -48
  52. data/lib/phlexi/form/structure/dom.rb +0 -62
  53. data/lib/phlexi/form/structure/field_builder.rb +0 -236
  54. data/lib/phlexi/form/structure/node.rb +0 -18
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Phlexi
4
4
  module Form
5
- module FieldOptions
5
+ module Options
6
6
  module Errors
7
7
  def custom_error(error)
8
8
  options[:error] = error
@@ -21,14 +21,28 @@ 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
31
35
 
36
+ # Determines if the associated object is in a valid state
37
+ #
38
+ # An object is considered valid if it is persisted and has no errors.
39
+ #
40
+ # @return [Boolean] true if the object is persisted and has no errors, false otherwise
41
+ def object_valid?
42
+ object.respond_to?(:persisted?) && object.persisted? &&
43
+ object.respond_to?(:errors) && !object.errors.empty?
44
+ end
45
+
32
46
  protected
33
47
 
34
48
  def error_text
@@ -66,26 +80,16 @@ module Phlexi
66
80
  end
67
81
 
68
82
  def errors_on_association
69
- reflection ? object.errors[reflection.name] : []
83
+ association_reflection ? object.errors[association_reflection.name] : []
70
84
  end
71
85
 
72
86
  def full_errors_on_association
73
- reflection ? object.errors.full_messages_for(reflection.name) : []
87
+ association_reflection ? object.errors.full_messages_for(association_reflection.name) : []
74
88
  end
75
89
 
76
90
  def has_custom_error?
77
91
  options[:error].is_a?(String)
78
92
  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
89
93
  end
90
94
  end
91
95
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Options
6
+ module Hints
7
+ def show_hint?
8
+ has_hint? && !show_errors?
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Options
6
+ module InferredTypes
7
+ private
8
+
9
+ def infer_field_component
10
+ case inferred_field_type
11
+ when :string, :text
12
+ infer_string_field_type || inferred_field_type
13
+ when :integer, :float, :decimal
14
+ :number
15
+ when :json, :jsonb
16
+ :text
17
+ when :enum
18
+ :select
19
+ when :association
20
+ association_reflection.polymorphic? ? :"polymorphic_#{association_reflection.macro}" : association_reflection.macro
21
+ when :attachment, :binary
22
+ :file
23
+ when :date, :time, :datetime, :boolean, :hstore
24
+ inferred_field_type
25
+ else
26
+ :string
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Phlexi
4
4
  module Form
5
- module FieldOptions
5
+ module Options
6
6
  module Length
7
7
  def minlength(minlength = nil)
8
8
  if minlength.nil?
9
- options[:minlength] = options.fetch(:minlength) { calculate_minlength }
9
+ options.fetch(:minlength) { options[:minlength] = calculate_minlength }
10
10
  else
11
11
  options[:minlength] = minlength
12
12
  self
@@ -15,7 +15,7 @@ module Phlexi
15
15
 
16
16
  def maxlength(maxlength = nil)
17
17
  if maxlength.nil?
18
- options[:maxlength] = options.fetch(:maxlength) { calculate_maxlength }
18
+ options.fetch(:maxlength) { options[:maxlength] = calculate_maxlength }
19
19
  else
20
20
  options[:maxlength] = maxlength
21
21
  self
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Phlexi
4
4
  module Form
5
- module FieldOptions
5
+ module Options
6
6
  module Limit
7
7
  def limit(limit = nil)
8
8
  if limit.nil?
9
- options[:limit] = options.fetch(:limit) { calculate_limit }
9
+ options.fetch(:limit) { options[:limit] = calculate_limit }
10
10
  else
11
11
  options[:limit] = limit
12
12
  self
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Options
6
+ module Max
7
+ def max(max_value = nil)
8
+ if max_value.nil?
9
+ options.fetch(:max) { options[:max] = calculate_max }
10
+ else
11
+ options[:max] = max_value
12
+ self
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def calculate_max
19
+ if (numericality_validator = find_numericality_validator)
20
+ get_max_from_validator(numericality_validator)
21
+ elsif (max = get_max_from_attribute(key))
22
+ max
23
+ end
24
+ end
25
+
26
+ def get_max_from_validator(validator)
27
+ options = validator.options
28
+ max = if options.key?(:less_than)
29
+ {value: options[:less_than], exclusive: true}
30
+ elsif options.key?(:less_than_or_equal_to)
31
+ {value: options[:less_than_or_equal_to], exclusive: false}
32
+ end
33
+ evaluate_and_adjust_max(max)
34
+ end
35
+
36
+ def evaluate_and_adjust_max(max)
37
+ return nil unless max
38
+
39
+ value = evaluate_numericality_validator_option(max[:value])
40
+ max[:exclusive] ? value - 1 : value
41
+ end
42
+
43
+ def get_max_from_attribute(attribute)
44
+ if object.class.respond_to?(:attribute_types) && (attribute_type = object.class.attribute_types[attribute.to_s])
45
+ if (range = attribute_type.instance_variable_get(:@range))
46
+ range.max
47
+ elsif attribute_type.respond_to?(:precision) && (precision = attribute_type.precision)
48
+ (precision**8) - ((step && step != "any") ? step : 0.000001)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Options
6
+ module Min
7
+ def min(min_value = nil)
8
+ if min_value.nil?
9
+ options.fetch(:min) { options[:min] = calculate_min }
10
+ else
11
+ options[:min] = min_value
12
+ self
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def calculate_min
19
+ if (numericality_validator = find_numericality_validator)
20
+ get_min_from_validator(numericality_validator)
21
+ elsif (min = get_min_from_attribute(key))
22
+ min
23
+ end
24
+ end
25
+
26
+ def get_min_from_validator(validator)
27
+ options = validator.options
28
+ min = if options.key?(:greater_than)
29
+ {value: options[:greater_than], exclusive: true}
30
+ elsif options.key?(:greater_than_or_equal_to)
31
+ {value: options[:greater_than_or_equal_to], exclusive: false}
32
+ end
33
+ evaluate_and_adjust_min(min)
34
+ end
35
+
36
+ def evaluate_and_adjust_min(min)
37
+ return nil unless min
38
+
39
+ value = evaluate_numericality_validator_option(min[:value])
40
+ min[:exclusive] ? value + 1 : value
41
+ end
42
+
43
+ def get_min_from_attribute(attribute)
44
+ if object.class.respond_to?(:attribute_types) && (attribute_type = object.class.attribute_types[attribute.to_s])
45
+ if (range = attribute_type.instance_variable_get(:@range))
46
+ range.min
47
+ elsif attribute_type.respond_to?(:precision) && (precision = attribute_type.precision)
48
+ -((precision**8) - ((step && step != "any") ? step : 0.000001))
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Phlexi
4
4
  module Form
5
- module FieldOptions
5
+ module Options
6
6
  module Pattern
7
7
  def pattern(pattern = nil)
8
8
  if pattern.nil?
9
- options[:pattern] = options.fetch(:pattern) { calculate_pattern }
9
+ options.fetch(:pattern) { options[:pattern] = calculate_pattern }
10
10
  else
11
11
  options[:pattern] = pattern
12
12
  self
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Phlexi
4
4
  module Form
5
- module FieldOptions
5
+ module Options
6
6
  module Readonly
7
7
  def readonly?
8
8
  options[:readonly] == true
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Phlexi
4
4
  module Form
5
- module FieldOptions
5
+ module Options
6
6
  module Required
7
7
  def required?
8
- options[:required] = options.fetch(:required) { calculate_required }
8
+ options.fetch(:required) { options[:required] = calculate_required }
9
9
  end
10
10
 
11
11
  def required!(required = true)
@@ -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?
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Options
6
+ module Step
7
+ def step(value = nil)
8
+ if value.nil?
9
+ options.fetch(:step) { options[:step] = calculate_step }
10
+ else
11
+ options[:step] = value
12
+ self
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def calculate_step
19
+ if (scale = get_scale_from_attribute(key))
20
+ return 1.fdiv(10**scale)
21
+ end
22
+
23
+ case inferred_field_type
24
+ when :integer
25
+ 1
26
+ when :decimal, :float
27
+ "any"
28
+ end
29
+ end
30
+
31
+ def get_scale_from_attribute(attribute)
32
+ if object.class.respond_to?(:attribute_types) && (attribute_type = object.class.attribute_types[attribute.to_s])
33
+ attribute_type.scale if attribute_type.respond_to?(:scale)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Options
6
+ module Validators
7
+ private
8
+
9
+ def find_numericality_validator
10
+ find_validator(:numericality)
11
+ end
12
+
13
+ def evaluate_numericality_validator_option(option)
14
+ case option
15
+ when Proc
16
+ option.arity.zero? ? option.call : option.call(object)
17
+ else
18
+ option
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -3,20 +3,11 @@
3
3
  module Phlexi
4
4
  module Form
5
5
  module Structure
6
- class FieldCollection
7
- include Enumerable
8
-
9
- class Builder
10
- attr_reader :key, :index
11
-
12
- def initialize(key, field, index)
13
- @key = key.to_s
14
- @field = field
15
- @index = index
16
- end
17
-
18
- def field(**)
19
- @field.class.new(key, input_attributes: @field.input_attributes, **, parent: @field).tap do |field|
6
+ class FieldCollection < Phlexi::Field::Structure::FieldCollection
7
+ class Builder < Builder
8
+ def field(**options)
9
+ options = mix({input_attributes: @field.input_attributes}, options)
10
+ @field.class.new(key, **options, parent: @field).tap do |field|
20
11
  yield field if block_given?
21
12
  end
22
13
  end
@@ -30,22 +21,16 @@ module Phlexi
30
21
  end
31
22
  end
32
23
 
33
- def initialize(field:, range:, &)
34
- @field = field
35
- @range = case range
24
+ private
25
+
26
+ def build_collection(collection)
27
+ case collection
36
28
  when Range, Array
37
- range
29
+ collection
38
30
  when Integer
39
- 1..range
31
+ 1..collection
40
32
  else
41
- range.to_a
42
- end
43
- each(&) if block_given?
44
- end
45
-
46
- def each(&)
47
- @range.each.with_index do |key, index|
48
- yield Builder.new(key, @field, index)
33
+ collection.to_a
49
34
  end
50
35
  end
51
36
  end
@@ -3,74 +3,11 @@
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
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
- #
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.
13
- class Namespace < Structure::Node
14
- include Enumerable
6
+ class Namespace < Phlexi::Field::Structure::Namespace
7
+ class NamespaceCollection < Phlexi::Form::Structure::NamespaceCollection; end
15
8
 
16
- attr_reader :builder_klass, :object
17
-
18
- def initialize(key, parent:, builder_klass:, object: nil)
19
- super(key, parent: parent)
20
- @builder_klass = builder_klass
21
- @object = object
22
- @children = {}
23
- yield self if block_given?
24
- end
25
-
26
- def field(key, **attributes)
27
- create_child(key, attributes.delete(:builder_klass) || builder_klass, object: object, **attributes).tap do |field|
28
- yield field if block_given?
29
- end
30
- end
31
-
32
- def submit_button(key = nil, **attributes, &)
33
- field(key || SecureRandom.hex).submit_button_tag(**attributes, &)
34
- end
35
-
36
- # Creates a `Namespace` child instance with the parent set to the current instance, adds to
37
- # the `@children` Hash to ensure duplicate child namespaces aren't created, then calls the
38
- # method on the `@object` to get the child object to pass into that namespace.
39
- #
40
- # For example, if a `User#permission` returns a `Permission` object, we could map that to a
41
- # form like this:
42
- #
43
- # ```ruby
44
- # Superform :user, object: User.new do |form|
45
- # form.nest_one :permission do |permission|
46
- # form.field :role
47
- # end
48
- # end
49
- # ```
50
- def nest_one(key, object: nil, &)
51
- object ||= object_value_for(key: key)
52
- create_child(key, self.class, object:, builder_klass:, &)
53
- end
54
-
55
- # Wraps an array of objects in Namespace classes. For example, if `User#addresses` returns
56
- # an enumerable or array of `Address` classes:
57
- #
58
- # ```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|
63
- # render address.field(:street).input_tag
64
- # render address.field(:state).input_tag
65
- # render address.field(:zip).input_tag
66
- # end
67
- # end
68
- # ```
69
- # The object within the block is a `Namespace` object that maps each object within the enumerable
70
- # to another `Namespace` or `Field`.
71
- def nest_many(key, collection: nil, &)
72
- collection ||= Array(object_value_for(key: key))
73
- create_child(key, NamespaceCollection, collection:, &)
9
+ def submit_button(key = :submit_button, **, &)
10
+ field(key).submit_button_tag(**, &)
74
11
  end
75
12
 
76
13
  def extract_input(params)
@@ -85,50 +22,6 @@ module Phlexi
85
22
  {key => input}
86
23
  end
87
24
  end
88
-
89
- # Iterates through the children of the current namespace, which could be `Namespace` or `Field`
90
- # objects.
91
- def each(&)
92
- @children.values.each(&)
93
- end
94
-
95
- def dom_id
96
- @dom_id ||= begin
97
- id = if object.nil?
98
- nil
99
- elsif object.class.respond_to?(:primary_key)
100
- object.public_send(object.class.primary_key) || :new
101
- elsif object.respond_to?(:id)
102
- object.id || :new
103
- end
104
- [key, id].compact.join("_").underscore
105
- end
106
- end
107
-
108
- # Creates a root Namespace, which is essentially a form.
109
- def self.root(*, builder_klass:, **, &)
110
- new(*, parent: nil, builder_klass:, **, &)
111
- end
112
-
113
- protected
114
-
115
- # Calls the corresponding method on the object for the `key` name, if it exists. For example
116
- # if the `key` is `email` on `User`, this method would call `User#email` if the method is
117
- # present.
118
- #
119
- # This method could be overwritten if the mapping between the `@object` and `key` name is not
120
- # a method call. For example, a `Hash` would be accessed via `user[:email]` instead of `user.send(:email)`
121
- def object_value_for(key:)
122
- @object.send(key) if @object.respond_to? key
123
- end
124
-
125
- private
126
-
127
- # Checks if the child exists. If it does then it returns that. If it doesn't, it will
128
- # build the child.
129
- def create_child(key, child_class, **kwargs, &block)
130
- @children.fetch(key) { @children[key] = child_class.new(key, parent: self, **kwargs, &block) }
131
- end
132
25
  end
133
26
  end
134
27
  end
@@ -3,19 +3,7 @@
3
3
  module Phlexi
4
4
  module Form
5
5
  module Structure
6
- class NamespaceCollection < Node
7
- include Enumerable
8
-
9
- def initialize(key, parent:, collection: nil, &block)
10
- raise ArgumentError, "block is required" unless block.present?
11
-
12
- super(key, parent: parent)
13
-
14
- @collection = collection
15
- @block = block
16
- each(&block)
17
- end
18
-
6
+ class NamespaceCollection < Phlexi::Field::Structure::NamespaceCollection
19
7
  def extract_input(params)
20
8
  namespace = build_namespace(0)
21
9
  @block.call(namespace)
@@ -23,25 +11,6 @@ module Phlexi
23
11
  inputs = params[key].map { |param| namespace.extract_input([param]) }
24
12
  {key => inputs}
25
13
  end
26
-
27
- private
28
-
29
- def each(&)
30
- namespaces.each(&)
31
- end
32
-
33
- # Builds and memoizes namespaces for the collection.
34
- #
35
- # @return [Array<Hash>] An array of namespace hashes.
36
- def namespaces
37
- @namespaces ||= @collection.map.with_index do |object, key|
38
- build_namespace(key, object: object)
39
- end
40
- end
41
-
42
- def build_namespace(index, **)
43
- parent.class.new(index, parent: self, builder_klass: parent.builder_klass, **)
44
- end
45
14
  end
46
15
  end
47
16
  end