phlexi-form 0.2.0

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