phlexi-form 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/gemfiles/default.gemfile.lock +34 -32
  4. data/gemfiles/rails_7.gemfile.lock +16 -18
  5. data/lib/phlexi/form/base.rb +18 -9
  6. data/lib/phlexi/form/builder.rb +297 -0
  7. data/lib/phlexi/form/components/base.rb +1 -1
  8. data/lib/phlexi/form/components/input.rb +16 -2
  9. data/lib/phlexi/form/components/select.rb +4 -0
  10. data/lib/phlexi/form/html.rb +18 -0
  11. data/lib/phlexi/form/{field_options → options}/autofocus.rb +1 -1
  12. data/lib/phlexi/form/{field_options → options}/collection.rb +6 -2
  13. data/lib/phlexi/form/{field_options → options}/disabled.rb +1 -1
  14. data/lib/phlexi/form/{field_options → options}/errors.rb +11 -11
  15. data/lib/phlexi/form/options/hints.rb +13 -0
  16. data/lib/phlexi/form/options/inferred_types.rb +32 -0
  17. data/lib/phlexi/form/{field_options → options}/length.rb +3 -3
  18. data/lib/phlexi/form/{field_options → options}/limit.rb +2 -2
  19. data/lib/phlexi/form/options/max.rb +55 -0
  20. data/lib/phlexi/form/options/min.rb +55 -0
  21. data/lib/phlexi/form/{field_options → options}/pattern.rb +2 -2
  22. data/lib/phlexi/form/{field_options → options}/readonly.rb +1 -1
  23. data/lib/phlexi/form/{field_options → options}/required.rb +2 -2
  24. data/lib/phlexi/form/options/step.rb +39 -0
  25. data/lib/phlexi/form/options/validators.rb +24 -0
  26. data/lib/phlexi/form/structure/field_collection.rb +9 -29
  27. data/lib/phlexi/form/structure/namespace.rb +2 -114
  28. data/lib/phlexi/form/structure/namespace_collection.rb +1 -32
  29. data/lib/phlexi/form/theme.rb +160 -0
  30. data/lib/phlexi/form/version.rb +1 -1
  31. data/lib/phlexi/form.rb +3 -6
  32. metadata +34 -23
  33. data/lib/phlexi/form/field_options/associations.rb +0 -21
  34. data/lib/phlexi/form/field_options/hints.rb +0 -26
  35. data/lib/phlexi/form/field_options/inferred_types.rb +0 -159
  36. data/lib/phlexi/form/field_options/labels.rb +0 -28
  37. data/lib/phlexi/form/field_options/min_max.rb +0 -92
  38. data/lib/phlexi/form/field_options/multiple.rb +0 -65
  39. data/lib/phlexi/form/field_options/placeholder.rb +0 -18
  40. data/lib/phlexi/form/field_options/themes.rb +0 -207
  41. data/lib/phlexi/form/field_options/validators.rb +0 -48
  42. data/lib/phlexi/form/structure/dom.rb +0 -62
  43. data/lib/phlexi/form/structure/field_builder.rb +0 -243
  44. data/lib/phlexi/form/structure/node.rb +0 -28
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "time"
4
+
3
5
  module Phlexi
4
6
  module Form
5
7
  module Components
@@ -21,7 +23,7 @@ module Phlexi
21
23
  end
22
24
 
23
25
  def build_input_attributes
24
- attributes.fetch(:type) { attributes[:type] = field.inferred_input_component_subtype }
26
+ attributes.fetch(:type) { attributes[:type] = field.inferred_string_field_type }
25
27
  attributes.fetch(:disabled) { attributes[:disabled] = field.disabled? }
26
28
 
27
29
  case attributes[:type]
@@ -48,12 +50,24 @@ module Phlexi
48
50
  attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
49
51
  attributes.fetch(:required) { attributes[:required] = field.required? }
50
52
  attributes.fetch(:multiple) { attributes[:multiple] = field.multiple? }
51
- when :date, :time, :datetime_local
53
+ when :date, :time, :"datetime-local"
52
54
  attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
53
55
  attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
54
56
  attributes.fetch(:required) { attributes[:required] = field.required? }
55
57
  attributes.fetch(:min) { attributes[:min] = field.min }
56
58
  attributes.fetch(:max) { attributes[:max] = field.max }
59
+
60
+ # TODO: Investigate if this is Timezone complaint
61
+ if field.value.respond_to?(:strftime)
62
+ attributes[:value] = case attributes[:type]
63
+ when :date
64
+ field.value.strftime("%Y-%m-%d")
65
+ when :time
66
+ field.value.strftime("%H:%M:%S")
67
+ when :"datetime-local"
68
+ field.value.strftime("%Y-%m-%dT%H:%M:%S")
69
+ end
70
+ end
57
71
  when :color
58
72
  attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
59
73
  when :range
@@ -43,6 +43,10 @@ module Phlexi
43
43
  attributes[:disabled] = attributes.fetch(:disabled, field.disabled?)
44
44
  attributes[:multiple] = attributes.fetch(:multiple, field.multiple?)
45
45
  attributes[:size] = attributes.fetch(:size, field.limit)
46
+
47
+ if attributes[:multiple]
48
+ attributes[:name] = "#{attributes[:name].sub(/\[\]$/, "")}[]"
49
+ end
46
50
  end
47
51
 
48
52
  def blank_option_text
@@ -0,0 +1,18 @@
1
+ module Phlexi
2
+ module Form
3
+ class HTML < (defined?(::ApplicationComponent) ? ::ApplicationComponent : Phlex::HTML)
4
+ module Behaviour
5
+ protected
6
+
7
+ def themed(component, field)
8
+ base_theme = Phlexi::Form::Theme.instance.resolve_theme(component)
9
+ validity_theme = Phlexi::Form::Theme.instance.resolve_validity_theme(component, field)
10
+
11
+ tokens(base_theme, validity_theme)
12
+ end
13
+ end
14
+
15
+ include Behaviour
16
+ end
17
+ end
18
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Phlexi
4
4
  module Form
5
- module FieldOptions
5
+ module Options
6
6
  module Autofocus
7
7
  def focused?
8
8
  options[:autofocus] == true
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Phlexi
4
4
  module Form
5
- module FieldOptions
5
+ module Options
6
6
  module Collection
7
7
  def collection(collection = nil)
8
8
  if collection.nil?
9
- options[:collection] = options.fetch(:collection) { infer_collection }
9
+ options.fetch(:collection) { options[:collection] = infer_collection }
10
10
  else
11
11
  options[:collection] = collection
12
12
  self
@@ -16,6 +16,10 @@ module Phlexi
16
16
  private
17
17
 
18
18
  def infer_collection
19
+ if object.class.respond_to?(:defined_enums)
20
+ return object.class.defined_enums.fetch(key.to_s).keys if object.class.defined_enums.key?(key.to_s)
21
+ end
22
+
19
23
  collection_value_from_association || collection_value_from_validator
20
24
  end
21
25
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Phlexi
4
4
  module Form
5
- module FieldOptions
5
+ module Options
6
6
  module Disabled
7
7
  def disabled?
8
8
  options[:disabled] == true
@@ -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
@@ -33,6 +33,16 @@ module Phlexi
33
33
  !has_errors? && has_value?
34
34
  end
35
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
+
36
46
  protected
37
47
 
38
48
  def error_text
@@ -80,16 +90,6 @@ module Phlexi
80
90
  def has_custom_error?
81
91
  options[:error].is_a?(String)
82
92
  end
83
-
84
- # Determines if the associated object is in a valid state
85
- #
86
- # An object is considered valid if it is persisted and has no errors.
87
- #
88
- # @return [Boolean] true if the object is persisted and has no errors, false otherwise
89
- def object_valid?
90
- object.respond_to?(:persisted?) && object.persisted? &&
91
- object.respond_to?(:errors) && !object.errors.empty?
92
- end
93
93
  end
94
94
  end
95
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)
@@ -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
@@ -1,24 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "phlex"
4
-
5
3
  module Phlexi
6
4
  module Form
7
5
  module Structure
8
- class FieldCollection
9
- include Enumerable
10
-
11
- class Builder
12
- include Phlex::Helpers
13
-
14
- attr_reader :key, :index
15
-
16
- def initialize(key, field, index)
17
- @key = key.to_s
18
- @field = field
19
- @index = index
20
- end
21
-
6
+ class FieldCollection < Phlexi::Field::Structure::FieldCollection
7
+ class Builder < Phlexi::Field::Structure::FieldCollection::Builder
22
8
  def field(**options)
23
9
  options = mix({input_attributes: @field.input_attributes}, options)
24
10
  @field.class.new(key, **options, parent: @field).tap do |field|
@@ -35,22 +21,16 @@ module Phlexi
35
21
  end
36
22
  end
37
23
 
38
- def initialize(field:, range:, &)
39
- @field = field
40
- @range = case range
24
+ private
25
+
26
+ def build_collection(collection)
27
+ case collection
41
28
  when Range, Array
42
- range
29
+ collection
43
30
  when Integer
44
- 1..range
31
+ 1..collection
45
32
  else
46
- range.to_a
47
- end
48
- each(&) if block_given?
49
- end
50
-
51
- def each(&)
52
- @range.each.with_index do |key, index|
53
- yield Builder.new(key, @field, index)
33
+ collection.to_a
54
34
  end
55
35
  end
56
36
  end
@@ -3,81 +3,13 @@
3
3
  module Phlexi
4
4
  module Form
5
5
  module Structure
6
- # A Namespace maps an 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
- #
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.
18
- class Namespace < Structure::Node
19
- include Enumerable
20
-
21
- attr_reader :builder_klass, :object
22
-
23
- def initialize(key, parent:, builder_klass:, object: nil)
24
- super(key, parent: parent)
25
- @builder_klass = builder_klass
26
- @object = object
27
- @children = {}
28
- yield self if block_given?
29
- end
30
-
31
- def field(key, **attributes)
32
- create_child(key, attributes.delete(:builder_klass) || builder_klass, object: object, **attributes).tap do |field|
33
- yield field if block_given?
34
- end
35
- end
6
+ class Namespace < Phlexi::Field::Structure::Namespace
7
+ class NamespaceCollection < Phlexi::Form::Structure::NamespaceCollection; end
36
8
 
37
9
  def submit_button(key = :submit_button, **, &)
38
10
  field(key).submit_button_tag(**, &)
39
11
  end
40
12
 
41
- # Creates a `Namespace` child instance with the parent set to the current instance, adds to
42
- # the `@children` Hash to ensure duplicate child namespaces aren't created, then calls the
43
- # method on the `@object` to get the child object to pass into that namespace.
44
- #
45
- # For example, if a `User#permission` returns a `Permission` object, we could map that to a
46
- # form like this:
47
- #
48
- # ```ruby
49
- # Phlexi::Form(User.new, as: :user) do
50
- # nest_one :profile do |profile|
51
- # render profile.field(:gender).input_tag
52
- # end
53
- # end
54
- # ```
55
- def nest_one(key, object: nil, &)
56
- object ||= object_value_for(key: key)
57
- create_child(key, self.class, object:, builder_klass:, &)
58
- end
59
-
60
- # Wraps an array of objects in Namespace classes. For example, if `User#addresses` returns
61
- # an enumerable or array of `Address` classes:
62
- #
63
- # ```ruby
64
- # Phlexi::Form(User.new) do
65
- # render field(:email).input_tag
66
- # render field(:name).input_tag
67
- # nest_many :addresses do |address|
68
- # render address.field(:street).input_tag
69
- # render address.field(:state).input_tag
70
- # render address.field(:zip).input_tag
71
- # end
72
- # end
73
- # ```
74
- # The object within the block is a `Namespace` object that maps each object within the enumerable
75
- # to another `Namespace` or `Field`.
76
- def nest_many(key, collection: nil, &)
77
- collection ||= Array(object_value_for(key: key))
78
- create_child(key, NamespaceCollection, collection:, &)
79
- end
80
-
81
13
  def extract_input(params)
82
14
  if params.is_a?(Array)
83
15
  each_with_object({}) do |child, hash|
@@ -90,50 +22,6 @@ module Phlexi
90
22
  {key => input}
91
23
  end
92
24
  end
93
-
94
- # Iterates through the children of the current namespace, which could be `Namespace` or `Field`
95
- # objects.
96
- def each(&)
97
- @children.values.each(&)
98
- end
99
-
100
- def dom_id
101
- @dom_id ||= begin
102
- id = if object.nil?
103
- nil
104
- elsif object.class.respond_to?(:primary_key)
105
- object.public_send(object.class.primary_key) || :new
106
- elsif object.respond_to?(:id)
107
- object.id || :new
108
- end
109
- [key, id].compact.join("_").underscore
110
- end
111
- end
112
-
113
- # Creates a root Namespace, which is essentially a form.
114
- def self.root(*, builder_klass:, **, &)
115
- new(*, parent: nil, builder_klass:, **, &)
116
- end
117
-
118
- protected
119
-
120
- # Calls the corresponding method on the object for the `key` name, if it exists. For example
121
- # if the `key` is `email` on `User`, this method would call `User#email` if the method is
122
- # present.
123
- #
124
- # This method could be overwritten if the mapping between the `@object` and `key` name is not
125
- # a method call. For example, a `Hash` would be accessed via `user[:email]` instead of `user.send(:email)`
126
- def object_value_for(key:)
127
- @object.send(key) if @object.respond_to? key
128
- end
129
-
130
- private
131
-
132
- # Checks if the child exists. If it does then it returns that. If it doesn't, it will
133
- # build the child.
134
- def create_child(key, child_class, **kwargs, &block)
135
- @children.fetch(key) { @children[key] = child_class.new(key, parent: self, **kwargs, &block) }
136
- end
137
25
  end
138
26
  end
139
27
  end