phlexi-display 0.0.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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.ruby-version +1 -0
  4. data/Appraisals +8 -0
  5. data/CHANGELOG.md +5 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +13 -0
  8. data/Rakefile +14 -0
  9. data/TODO +0 -0
  10. data/config.ru +6 -0
  11. data/gemfiles/default.gemfile +5 -0
  12. data/gemfiles/rails_7.gemfile +8 -0
  13. data/lib/phlexi/display/base.rb +243 -0
  14. data/lib/phlexi/display/components/base.rb +51 -0
  15. data/lib/phlexi/display/components/checkbox.rb +48 -0
  16. data/lib/phlexi/display/components/collection_checkboxes.rb +44 -0
  17. data/lib/phlexi/display/components/collection_radio_buttons.rb +35 -0
  18. data/lib/phlexi/display/components/concerns/handles_array_input.rb +21 -0
  19. data/lib/phlexi/display/components/concerns/handles_input.rb +53 -0
  20. data/lib/phlexi/display/components/concerns/has_options.rb +37 -0
  21. data/lib/phlexi/display/components/concerns/submits_form.rb +39 -0
  22. data/lib/phlexi/display/components/error.rb +21 -0
  23. data/lib/phlexi/display/components/file_input.rb +32 -0
  24. data/lib/phlexi/display/components/full_error.rb +21 -0
  25. data/lib/phlexi/display/components/hint.rb +21 -0
  26. data/lib/phlexi/display/components/input.rb +84 -0
  27. data/lib/phlexi/display/components/input_array.rb +45 -0
  28. data/lib/phlexi/display/components/label.rb +27 -0
  29. data/lib/phlexi/display/components/radio_button.rb +41 -0
  30. data/lib/phlexi/display/components/select.rb +69 -0
  31. data/lib/phlexi/display/components/submit_button.rb +41 -0
  32. data/lib/phlexi/display/components/textarea.rb +34 -0
  33. data/lib/phlexi/display/components/wrapper.rb +31 -0
  34. data/lib/phlexi/display/field_options/associations.rb +21 -0
  35. data/lib/phlexi/display/field_options/autofocus.rb +18 -0
  36. data/lib/phlexi/display/field_options/collection.rb +54 -0
  37. data/lib/phlexi/display/field_options/disabled.rb +18 -0
  38. data/lib/phlexi/display/field_options/errors.rb +92 -0
  39. data/lib/phlexi/display/field_options/hints.rb +22 -0
  40. data/lib/phlexi/display/field_options/inferred_types.rb +155 -0
  41. data/lib/phlexi/display/field_options/labels.rb +28 -0
  42. data/lib/phlexi/display/field_options/length.rb +53 -0
  43. data/lib/phlexi/display/field_options/limit.rb +66 -0
  44. data/lib/phlexi/display/field_options/min_max.rb +92 -0
  45. data/lib/phlexi/display/field_options/multiple.rb +65 -0
  46. data/lib/phlexi/display/field_options/pattern.rb +38 -0
  47. data/lib/phlexi/display/field_options/placeholder.rb +18 -0
  48. data/lib/phlexi/display/field_options/readonly.rb +18 -0
  49. data/lib/phlexi/display/field_options/required.rb +37 -0
  50. data/lib/phlexi/display/field_options/themes.rb +207 -0
  51. data/lib/phlexi/display/field_options/validators.rb +48 -0
  52. data/lib/phlexi/display/option_mapper.rb +154 -0
  53. data/lib/phlexi/display/structure/dom.rb +62 -0
  54. data/lib/phlexi/display/structure/field_builder.rb +236 -0
  55. data/lib/phlexi/display/structure/field_collection.rb +54 -0
  56. data/lib/phlexi/display/structure/namespace.rb +135 -0
  57. data/lib/phlexi/display/structure/namespace_collection.rb +48 -0
  58. data/lib/phlexi/display/structure/node.rb +18 -0
  59. data/lib/phlexi/display/version.rb +7 -0
  60. data/lib/phlexi/display.rb +31 -0
  61. data/lib/phlexi-display.rb +3 -0
  62. data/sig/phlexi/display.rbs +6 -0
  63. metadata +262 -0
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module FieldOptions
6
+ module Hints
7
+ def hint(hint = nil)
8
+ if hint.nil?
9
+ options[:hint]
10
+ else
11
+ options[:hint] = hint
12
+ self
13
+ end
14
+ end
15
+
16
+ def has_hint?
17
+ options[:hint] != false && hint.present?
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+
5
+ module Phlexi
6
+ module Display
7
+ module FieldOptions
8
+ module InferredTypes
9
+ def inferred_db_type
10
+ @inferred_db_type ||= infer_db_type
11
+ end
12
+
13
+ def inferred_input_component
14
+ @inferred_input_component ||= infer_input_component
15
+ end
16
+
17
+ def inferred_input_type
18
+ @inferred_input_type ||= infer_input_type(inferred_input_component)
19
+ end
20
+
21
+ private
22
+
23
+ # this returns the element type
24
+ # one of :input, :textarea, :select, :botton
25
+ def infer_input_component
26
+ return :select unless collection.blank?
27
+
28
+ case inferred_db_type
29
+ when :text, :json, :jsonb, :hstore
30
+ :textarea
31
+ else
32
+ :input
33
+ end
34
+ end
35
+
36
+ # this only applies when input_component is `:input`
37
+ # resolves the type attribute of input components
38
+ def infer_input_type(component)
39
+ case inferred_db_type
40
+ when :string
41
+ infer_string_input_type(key)
42
+ when :integer, :float, :decimal
43
+ :number
44
+ when :date
45
+ :date
46
+ when :datetime
47
+ :datetime
48
+ when :time
49
+ :time
50
+ when :boolean
51
+ :checkbox
52
+ else
53
+ :text
54
+ end
55
+ end
56
+
57
+ def infer_db_type
58
+ if object.class.respond_to?(:columns_hash)
59
+ # ActiveRecord object
60
+ column = object.class.columns_hash[key.to_s]
61
+ return column.type if column
62
+ end
63
+
64
+ if object.class.respond_to?(:attribute_types)
65
+ # ActiveModel::Attributes
66
+ custom_type = object.class.attribute_types[key.to_s]
67
+ return custom_type.type if custom_type
68
+ end
69
+
70
+ # Check if object responds to the key
71
+ if object.respond_to?(key)
72
+ # Fallback to inferring type from the value
73
+ return infer_db_type_from_value(object.send(key))
74
+ end
75
+
76
+ # Default to string if we can't determine the type
77
+ :string
78
+ end
79
+
80
+ def infer_db_type_from_value(value)
81
+ case value
82
+ when Integer
83
+ :integer
84
+ when Float, BigDecimal
85
+ :float
86
+ when TrueClass, FalseClass
87
+ :boolean
88
+ when Date
89
+ :date
90
+ when Time, DateTime
91
+ :datetime
92
+ else
93
+ :string
94
+ end
95
+ end
96
+
97
+ def infer_string_input_type(key)
98
+ key = key.to_s.downcase
99
+
100
+ return :password if is_password_field?
101
+
102
+ custom_type = custom_string_input_type(key)
103
+ return custom_type if custom_type
104
+
105
+ if has_validators?
106
+ infer_string_input_type_from_validations
107
+ else
108
+ :text
109
+ end
110
+ end
111
+
112
+ def custom_string_input_type(key)
113
+ custom_mappings = {
114
+ /url$|^link|^site/ => :url,
115
+ /^email/ => :email,
116
+ /^search/ => :search,
117
+ /phone|tel(ephone)?/ => :tel,
118
+ /^time/ => :time,
119
+ /^date/ => :date,
120
+ /^number|_count$|_amount$/ => :number,
121
+ /^color/ => :color
122
+ }
123
+
124
+ custom_mappings.each do |pattern, type|
125
+ return type if key.match?(pattern)
126
+ end
127
+
128
+ nil
129
+ end
130
+
131
+ def infer_string_input_type_from_validations
132
+ if attribute_validators.find { |v| v.kind == :numericality }
133
+ :number
134
+ elsif attribute_validators.find { |v| v.kind == :format && v.options[:with] == URI::MailTo::EMAIL_REGEXP }
135
+ :email
136
+ else
137
+ :text
138
+ end
139
+ end
140
+
141
+ def is_password_field?
142
+ key = self.key.to_s.downcase
143
+
144
+ exact_matches = ["password"]
145
+ prefixes = ["encrypted_"]
146
+ suffixes = ["_password", "_digest", "_hash"]
147
+
148
+ exact_matches.include?(key) ||
149
+ prefixes.any? { |prefix| key.start_with?(prefix) } ||
150
+ suffixes.any? { |suffix| key.end_with?(suffix) }
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module FieldOptions
6
+ module Labels
7
+ def label(label = nil)
8
+ if label.nil?
9
+ options[:label] = options.fetch(:label) { calculate_label }
10
+ else
11
+ options[:label] = label
12
+ self
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def calculate_label
19
+ if object.class.respond_to?(:human_attribute_name)
20
+ object.class.human_attribute_name(key.to_s, {base: object})
21
+ else
22
+ key.to_s.humanize
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module FieldOptions
6
+ module Length
7
+ def minlength(minlength = nil)
8
+ if minlength.nil?
9
+ options[:minlength] = options.fetch(:minlength) { calculate_minlength }
10
+ else
11
+ options[:minlength] = minlength
12
+ self
13
+ end
14
+ end
15
+
16
+ def maxlength(maxlength = nil)
17
+ if maxlength.nil?
18
+ options[:maxlength] = options.fetch(:maxlength) { calculate_maxlength }
19
+ else
20
+ options[:maxlength] = maxlength
21
+ self
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def calculate_minlength
28
+ minimum_length_value_from(find_length_validator)
29
+ end
30
+
31
+ def minimum_length_value_from(length_validator)
32
+ if length_validator
33
+ length_validator.options[:is] || length_validator.options[:minimum]
34
+ end
35
+ end
36
+
37
+ def calculate_maxlength
38
+ maximum_length_value_from(find_length_validator)
39
+ end
40
+
41
+ def maximum_length_value_from(length_validator)
42
+ if length_validator
43
+ length_validator.options[:is] || length_validator.options[:maximum]
44
+ end
45
+ end
46
+
47
+ def find_length_validator
48
+ find_validator(:length)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module FieldOptions
6
+ module Limit
7
+ def limit(limit = nil)
8
+ if limit.nil?
9
+ options[:limit] = options.fetch(:limit) { calculate_limit }
10
+ else
11
+ options[:limit] = limit
12
+ self
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def calculate_limit
19
+ return unless multiple?
20
+
21
+ limit_from_validators = [
22
+ limit_from_length_validator,
23
+ limit_from_inclusion_validator
24
+ ].compact.min
25
+
26
+ limit_from_validators || limit_from_db_column
27
+ end
28
+
29
+ def limit_from_length_validator
30
+ length_validator = find_validator(:length)
31
+ return unless length_validator
32
+
33
+ length_validator.options[:maximum]
34
+ end
35
+
36
+ def limit_from_inclusion_validator
37
+ return unless has_validators?
38
+
39
+ inclusion_validator = find_validator(:inclusion)
40
+ return unless inclusion_validator
41
+
42
+ in_option = inclusion_validator.options[:in]
43
+ in_option.is_a?(Array) ? in_option.size : nil
44
+ end
45
+
46
+ def limit_from_db_column
47
+ return unless object.class.respond_to?(:columns_hash)
48
+
49
+ column = object.class.columns_hash[key.to_s]
50
+ return unless column
51
+
52
+ case object.class.connection.adapter_name.downcase
53
+ when "postgresql"
54
+ if column.array?
55
+ # Check if there's a limit on the array size
56
+ column.limit
57
+ elsif column.type == :string && column.sql_type.include?("[]")
58
+ # For string arrays, extract the limit if specified
59
+ column.sql_type.match(/\[(\d+)\]/)&.captures&.first&.to_i
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module FieldOptions
6
+ module MinMax
7
+ def min(min_value = nil)
8
+ if min_value.nil?
9
+ options[:min] = options.fetch(:min) { calculate_min }
10
+ else
11
+ options[:min] = min_value
12
+ self
13
+ end
14
+ end
15
+
16
+ def max(max_value = nil)
17
+ if max_value.nil?
18
+ options[:max] = options.fetch(:max) { calculate_max }
19
+ else
20
+ options[:max] = max_value
21
+ self
22
+ end
23
+ end
24
+
25
+ def step
26
+ 1 if min || max
27
+ end
28
+
29
+ private
30
+
31
+ def calculate_min
32
+ if (numericality_validator = find_numericality_validator)
33
+ get_min_from_validator(numericality_validator)
34
+ end
35
+ end
36
+
37
+ def calculate_max
38
+ if (numericality_validator = find_numericality_validator)
39
+ get_max_from_validator(numericality_validator)
40
+ end
41
+ end
42
+
43
+ def find_numericality_validator
44
+ find_validator(:numericality)
45
+ end
46
+
47
+ def get_min_from_validator(validator)
48
+ options = validator.options
49
+ min = if options.key?(:greater_than)
50
+ {value: options[:greater_than], exclusive: true}
51
+ elsif options.key?(:greater_than_or_equal_to)
52
+ {value: options[:greater_than_or_equal_to], exclusive: false}
53
+ end
54
+ evaluate_and_adjust_min(min)
55
+ end
56
+
57
+ def get_max_from_validator(validator)
58
+ options = validator.options
59
+ max = if options.key?(:less_than)
60
+ {value: options[:less_than], exclusive: true}
61
+ elsif options.key?(:less_than_or_equal_to)
62
+ {value: options[:less_than_or_equal_to], exclusive: false}
63
+ end
64
+ evaluate_and_adjust_max(max)
65
+ end
66
+
67
+ def evaluate_and_adjust_min(min)
68
+ return nil unless min
69
+
70
+ value = evaluate_numericality_validator_option(min[:value])
71
+ min[:exclusive] ? value + 1 : value
72
+ end
73
+
74
+ def evaluate_and_adjust_max(max)
75
+ return nil unless max
76
+
77
+ value = evaluate_numericality_validator_option(max[:value])
78
+ max[:exclusive] ? value - 1 : value
79
+ end
80
+
81
+ def evaluate_numericality_validator_option(option)
82
+ case option
83
+ when Proc
84
+ option.arity.zero? ? option.call : option.call(object)
85
+ else
86
+ option
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module FieldOptions
6
+ module Multiple
7
+ def multiple?
8
+ options[:multiple] = options.fetch(:multiple) { calculate_multiple_field_value }
9
+ end
10
+
11
+ def multiple!(multiple = true)
12
+ options[:multiple] = multiple
13
+ self
14
+ end
15
+
16
+ private
17
+
18
+ def calculate_multiple_field_value
19
+ return true if reflection&.macro == :has_many
20
+ return true if multiple_field_array_attribute?
21
+
22
+ check_multiple_field_from_validators
23
+ end
24
+
25
+ def multiple_field_array_attribute?
26
+ return false unless object.class.respond_to?(:columns_hash)
27
+
28
+ column = object.class.columns_hash[key.to_s]
29
+ return false unless column
30
+
31
+ case object.class.connection.adapter_name.downcase
32
+ when "postgresql"
33
+ column.array? || (column.type == :string && column.sql_type.include?("[]"))
34
+ end # || object.class.attribute_types[key.to_s].is_a?(ActiveRecord::Type::Serialized)
35
+ rescue
36
+ # Rails.logger.warn("Error checking multiple field array attribute: #{e.message}")
37
+ false
38
+ end
39
+
40
+ def check_multiple_field_from_validators
41
+ inclusion_validator = find_validator(:inclusion)
42
+ length_validator = find_validator(:length)
43
+
44
+ return false unless inclusion_validator || length_validator
45
+
46
+ check_multiple_field_inclusion_validator(inclusion_validator) ||
47
+ check_multiple_field_length_validator(length_validator)
48
+ end
49
+
50
+ def check_multiple_field_inclusion_validator(validator)
51
+ return false unless validator
52
+ in_option = validator.options[:in]
53
+ return false unless in_option.is_a?(Array)
54
+
55
+ validator.options[:multiple] == true || (multiple_field_array_attribute? && in_option.size > 1)
56
+ end
57
+
58
+ def check_multiple_field_length_validator(validator)
59
+ return false unless validator
60
+ validator.options[:maximum].to_i > 1 if validator.options[:maximum]
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module FieldOptions
6
+ module Pattern
7
+ def pattern(pattern = nil)
8
+ if pattern.nil?
9
+ options[:pattern] = options.fetch(:pattern) { calculate_pattern }
10
+ else
11
+ options[:pattern] = pattern
12
+ self
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def calculate_pattern
19
+ if (pattern_validator = find_pattern_validator) && (with = pattern_validator.options[:with])
20
+ evaluate_format_validator_option(with).source
21
+ end
22
+ end
23
+
24
+ def find_pattern_validator
25
+ find_validator(:format)
26
+ end
27
+
28
+ def evaluate_format_validator_option(option)
29
+ if option.respond_to?(:call)
30
+ option.call(object)
31
+ else
32
+ option
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module FieldOptions
6
+ module Placeholder
7
+ def placeholder(placeholder = nil)
8
+ if placeholder.nil?
9
+ options[:placeholder]
10
+ else
11
+ options[:placeholder] = placeholder
12
+ self
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module FieldOptions
6
+ module Readonly
7
+ def readonly?
8
+ options[:readonly] == true
9
+ end
10
+
11
+ def readonly!(readonly = true)
12
+ options[:readonly] = readonly
13
+ self
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module FieldOptions
6
+ module Required
7
+ def required?
8
+ options[:required] = options.fetch(:required) { calculate_required }
9
+ end
10
+
11
+ def required!(required = true)
12
+ options[:required] = required
13
+ self
14
+ end
15
+
16
+ private
17
+
18
+ def calculate_required
19
+ if has_validators?
20
+ required_by_validators?
21
+ else
22
+ required_by_default?
23
+ end
24
+ end
25
+
26
+ def required_by_validators?
27
+ (attribute_validators + reflection_validators).any? { |v| v.kind == :presence && valid_validator?(v) }
28
+ end
29
+
30
+ def required_by_default?
31
+ # TODO: get this from configuration
32
+ false
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end