phlexi-display 0.0.1

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