phlexi-form 0.3.0 → 0.4.0

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