phlexi-form 0.2.0

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.ruby-version +1 -0
  4. data/Appraisals +13 -0
  5. data/CHANGELOG.md +5 -0
  6. data/CODE_OF_CONDUCT.md +84 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +395 -0
  9. data/Rakefile +14 -0
  10. data/config.ru +9 -0
  11. data/gemfiles/default.gemfile +5 -0
  12. data/gemfiles/default.gemfile.lock +174 -0
  13. data/lib/generators/superform/install/USAGE +8 -0
  14. data/lib/generators/superform/install/install_generator.rb +34 -0
  15. data/lib/generators/superform/install/templates/application_form.rb +31 -0
  16. data/lib/phlexi/form/base.rb +234 -0
  17. data/lib/phlexi/form/components/base.rb +37 -0
  18. data/lib/phlexi/form/components/checkbox.rb +43 -0
  19. data/lib/phlexi/form/components/collection_checkboxes.rb +30 -0
  20. data/lib/phlexi/form/components/collection_radio_buttons.rb +29 -0
  21. data/lib/phlexi/form/components/concerns/has_options.rb +33 -0
  22. data/lib/phlexi/form/components/error.rb +21 -0
  23. data/lib/phlexi/form/components/full_error.rb +21 -0
  24. data/lib/phlexi/form/components/hint.rb +21 -0
  25. data/lib/phlexi/form/components/input.rb +78 -0
  26. data/lib/phlexi/form/components/label.rb +26 -0
  27. data/lib/phlexi/form/components/radio_button.rb +31 -0
  28. data/lib/phlexi/form/components/select.rb +57 -0
  29. data/lib/phlexi/form/components/textarea.rb +34 -0
  30. data/lib/phlexi/form/components/wrapper.rb +31 -0
  31. data/lib/phlexi/form/field_options/autofocus.rb +18 -0
  32. data/lib/phlexi/form/field_options/collection.rb +37 -0
  33. data/lib/phlexi/form/field_options/disabled.rb +18 -0
  34. data/lib/phlexi/form/field_options/errors.rb +82 -0
  35. data/lib/phlexi/form/field_options/hints.rb +22 -0
  36. data/lib/phlexi/form/field_options/labels.rb +28 -0
  37. data/lib/phlexi/form/field_options/length.rb +53 -0
  38. data/lib/phlexi/form/field_options/limit.rb +66 -0
  39. data/lib/phlexi/form/field_options/min_max.rb +92 -0
  40. data/lib/phlexi/form/field_options/multiple.rb +63 -0
  41. data/lib/phlexi/form/field_options/pattern.rb +38 -0
  42. data/lib/phlexi/form/field_options/placeholder.rb +18 -0
  43. data/lib/phlexi/form/field_options/readonly.rb +18 -0
  44. data/lib/phlexi/form/field_options/required.rb +37 -0
  45. data/lib/phlexi/form/field_options/type.rb +155 -0
  46. data/lib/phlexi/form/field_options/validators.rb +48 -0
  47. data/lib/phlexi/form/option_mapper.rb +154 -0
  48. data/lib/phlexi/form/structure/dom.rb +57 -0
  49. data/lib/phlexi/form/structure/field_builder.rb +199 -0
  50. data/lib/phlexi/form/structure/field_collection.rb +45 -0
  51. data/lib/phlexi/form/structure/namespace.rb +123 -0
  52. data/lib/phlexi/form/structure/namespace_collection.rb +48 -0
  53. data/lib/phlexi/form/structure/node.rb +18 -0
  54. data/lib/phlexi/form/version.rb +7 -0
  55. data/lib/phlexi/form.rb +28 -0
  56. data/lib/phlexi-form.rb +3 -0
  57. data/sig/phlexi/form.rbs +6 -0
  58. metadata +243 -0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class Hint < Base
7
+ def view_template
8
+ p(**attributes) do
9
+ field.hint
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def render?
16
+ field.hint.present? && (!field.show_errors? || !field.has_errors?)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class Input < Base
7
+ def view_template
8
+ input(**attributes)
9
+ end
10
+
11
+ protected
12
+
13
+ def build_attributes
14
+ super
15
+
16
+ # only overwrite id if it was set in Base
17
+ attributes[:id] = field.dom.id if attributes[:id] == "#{field.dom.id}_#{component_name}"
18
+ attributes[:name] = field.dom.name
19
+ attributes[:value] = field.dom.value
20
+
21
+ build_input_attributes
22
+ end
23
+
24
+ def build_input_attributes
25
+ attributes[:type] = attributes.fetch(:type, field.input_type)
26
+ attributes[:disabled] = attributes.fetch(:disabled, field.disabled?)
27
+
28
+ case attributes[:type]
29
+ when :text, :password, :email, :tel, :url, :search
30
+ attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
31
+ attributes[:placeholder] = attributes.fetch(:placeholder, field.placeholder)
32
+ attributes[:minlength] = attributes.fetch(:minlength, field.minlength)
33
+ attributes[:maxlength] = attributes.fetch(:maxlength, field.maxlength)
34
+ attributes[:readonly] = attributes.fetch(:readonly, field.readonly?)
35
+ attributes[:required] = attributes.fetch(:required, field.required?)
36
+ attributes[:pattern] = attributes.fetch(:pattern, field.pattern)
37
+ when :number
38
+ attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
39
+ attributes[:placeholder] = attributes.fetch(:placeholder, field.placeholder)
40
+ attributes[:readonly] = attributes.fetch(:readonly, field.readonly?)
41
+ attributes[:required] = attributes.fetch(:required, field.required?)
42
+ attributes[:min] = attributes.fetch(:min, field.min)
43
+ attributes[:max] = attributes.fetch(:max, field.max)
44
+ attributes[:step] = attributes.fetch(:step, field.step)
45
+ when :checkbox, :radio
46
+ attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
47
+ attributes[:required] = attributes.fetch(:required, field.required?)
48
+ when :file
49
+ attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
50
+ attributes[:required] = attributes.fetch(:required, field.required?)
51
+ attributes[:multiple] = attributes.fetch(:multiple, field.multiple)
52
+ attributes[:accept] = attributes.fetch(:accept, field.accept)
53
+ when :date, :time, :datetime_local
54
+ attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
55
+ attributes[:readonly] = attributes.fetch(:readonly, field.readonly?)
56
+ attributes[:required] = attributes.fetch(:required, field.required?)
57
+ attributes[:min] = attributes.fetch(:min, field.min)
58
+ attributes[:max] = attributes.fetch(:max, field.max)
59
+ when :color
60
+ attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
61
+ when :range
62
+ attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
63
+ attributes[:min] = attributes.fetch(:min, field.min)
64
+ attributes[:max] = attributes.fetch(:max, field.max)
65
+ attributes[:step] = attributes.fetch(:step, field.step)
66
+ else
67
+ # Handle any unrecognized input types
68
+ # Rails.logger.warn("Unhandled input type: #{attributes[:type]}")
69
+ end
70
+
71
+ if (attributes[:type] == :file) ? attributes[:multiple] : attributes.delete(:multiple)
72
+ attributes[:name] = "#{attributes[:name].sub(/\[]$/, "")}[]"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class Label < Base
7
+ def view_template
8
+ label(**attributes) do
9
+ if field.required?
10
+ abbr(title: "required") { "*" }
11
+ whitespace
12
+ end
13
+ plain field.label
14
+ end
15
+ end
16
+
17
+ protected
18
+
19
+ def build_attributes
20
+ super
21
+ attributes[:for] ||= field.dom.id
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class RadioButton < Input
7
+ def view_template
8
+ input(**attributes, value: @checked_value)
9
+ end
10
+
11
+ protected
12
+
13
+ def build_input_attributes
14
+ attributes[:type] = :radio
15
+ super
16
+
17
+ @checked_value = (attributes.key?(:checked_value) ? attributes.delete(:checked_value) : "1").to_s
18
+
19
+ # this is a hack to workaround the fact that radio cannot be indexed/multiple
20
+ attributes[:name] = attributes[:name].sub(/\[]$/, "")
21
+ attributes[:value] = @checked_value
22
+ attributes[:checked] = attributes.fetch(:checked) { checked? }
23
+ end
24
+
25
+ def checked?
26
+ field.dom.value == @checked_value
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class Select < Base
7
+ include Concerns::HasOptions
8
+
9
+ def view_template(&block)
10
+ select(**attributes) do
11
+ blank_option { blank_option_text } unless skip_blank_option?
12
+ options
13
+ end
14
+ end
15
+
16
+ protected
17
+
18
+ def options
19
+ option_mapper.each do |value, label|
20
+ option(selected: selected?(value), value: value) { label }
21
+ end
22
+ end
23
+
24
+ def blank_option(&)
25
+ option(selected: field.value.nil?, &)
26
+ end
27
+
28
+ def build_attributes
29
+ super
30
+
31
+ attributes[:id] = field.dom.id
32
+ attributes[:name] = field.dom.name
33
+
34
+ build_select_attributes
35
+ end
36
+
37
+ def build_select_attributes
38
+ @include_blank_option = attributes.delete(:include_blank_option)
39
+
40
+ attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
41
+ attributes[:required] = attributes.fetch(:required, field.required?)
42
+ attributes[:disabled] = attributes.fetch(:disabled, field.disabled?)
43
+ attributes[:multiple] = attributes.fetch(:multiple, field.multiple?)
44
+ attributes[:size] = attributes.fetch(:size, field.limit)
45
+ end
46
+
47
+ def blank_option_text
48
+ field.placeholder
49
+ end
50
+
51
+ def skip_blank_option?
52
+ @include_blank_option == false
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class Textarea < Base
7
+ def view_template
8
+ textarea(**attributes) { field.dom.value }
9
+ end
10
+
11
+ protected
12
+
13
+ def build_attributes
14
+ super
15
+
16
+ attributes[:id] = field.dom.id
17
+ attributes[:name] = field.dom.name
18
+
19
+ build_textarea_attributes
20
+ end
21
+
22
+ def build_textarea_attributes
23
+ attributes[:placeholder] = attributes.fetch(:placeholder, field.placeholder)
24
+ attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
25
+ attributes[:minlength] = attributes.fetch(:minlength, field.minlength)
26
+ attributes[:maxlength] = attributes.fetch(:maxlength, field.maxlength)
27
+ attributes[:readonly] = attributes.fetch(:readonly, field.readonly?)
28
+ attributes[:required] = attributes.fetch(:required, field.required?)
29
+ attributes[:disabled] = attributes.fetch(:disabled, field.disabled?)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class Wrapper < Base
7
+ attr_reader :inner_attributes
8
+
9
+ def view_template
10
+ div(**attributes) do
11
+ render field.label_tag
12
+ div(**inner_attributes) do
13
+ yield field if block_given?
14
+ render field.full_error_tag
15
+ render field.hint_tag
16
+ end
17
+ end
18
+ end
19
+
20
+ protected
21
+
22
+ def build_attributes
23
+ super
24
+
25
+ @inner_attributes = attributes.delete(:inner) || {}
26
+ inner_attributes[:class] = tokens("inner-wrapper", inner_attributes[:class])
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module FieldOptions
6
+ module Autofocus
7
+ def focused?
8
+ options[:autofocus] == true
9
+ end
10
+
11
+ def focus(autofocus = true)
12
+ options[:autofocus] = autofocus
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 Form
5
+ module FieldOptions
6
+ module Collection
7
+ def collection(collection = nil)
8
+ if collection.nil?
9
+ options[:collection] = options.fetch(:collection) { infer_collection }
10
+ else
11
+ options[:collection] = collection
12
+ self
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def infer_collection
19
+ if has_validators?
20
+ inclusion_validator = find_inclusion_validator
21
+ collection_value_from(inclusion_validator)
22
+ end
23
+ end
24
+
25
+ def collection_value_from(inclusion_validator)
26
+ if inclusion_validator
27
+ inclusion_validator.options[:in] || inclusion_validator.options[:within]
28
+ end
29
+ end
30
+
31
+ def find_inclusion_validator
32
+ find_validator(:inclusion)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module FieldOptions
6
+ module Disabled
7
+ def disabled?
8
+ options[:disabled] == true
9
+ end
10
+
11
+ def disabled!(disabled = true)
12
+ options[:disabled] = disabled
13
+ self
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module FieldOptions
6
+ module Errors
7
+ def custom_error(error)
8
+ options[:error] = error
9
+ self
10
+ end
11
+
12
+ def error
13
+ error_text if has_errors?
14
+ end
15
+
16
+ def full_error
17
+ full_error_text if has_errors?
18
+ end
19
+
20
+ def has_errors?
21
+ object_with_errors? || !object && has_custom_error?
22
+ end
23
+
24
+ def show_errors?
25
+ options[:error] != false
26
+ end
27
+
28
+ def valid?
29
+ !has_errors? && has_value?
30
+ end
31
+
32
+ protected
33
+
34
+ def error_text
35
+ text = has_custom_error? ? options[:error] : errors.send(error_method)
36
+
37
+ "#{options[:error_prefix]} #{text}".lstrip
38
+ end
39
+
40
+ def full_error_text
41
+ has_custom_error? ? options[:error] : full_errors.send(error_method)
42
+ end
43
+
44
+ def object_with_errors?
45
+ object&.respond_to?(:errors) && errors.present?
46
+ end
47
+
48
+ def error_method
49
+ options[:error_method] || :first
50
+ end
51
+
52
+ def errors
53
+ @errors ||= (errors_on_attribute + errors_on_association).compact
54
+ end
55
+
56
+ def full_errors
57
+ @full_errors ||= (full_errors_on_attribute + full_errors_on_association).compact
58
+ end
59
+
60
+ def errors_on_attribute
61
+ object.errors[key] || []
62
+ end
63
+
64
+ def full_errors_on_attribute
65
+ object.errors.full_messages_for(key)
66
+ end
67
+
68
+ def errors_on_association
69
+ reflection ? object.errors[reflection.name] : []
70
+ end
71
+
72
+ def full_errors_on_association
73
+ reflection ? object.errors.full_messages_for(reflection.name) : []
74
+ end
75
+
76
+ def has_custom_error?
77
+ options[:error].is_a?(String)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
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,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
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 Form
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 Form
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