phlexi-form 0.3.0.rc1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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