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,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module Components
6
+ class Error < Base
7
+ def view_template
8
+ p(**attributes) do
9
+ field.error
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def render?
16
+ field.show_errors? && field.has_errors?
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module Components
6
+ class FileInput < Input
7
+ def view_template
8
+ input(type: :hidden, name: attributes[:name], value: "", autocomplete: "off", hidden: true) if include_hidden?
9
+ input(**attributes)
10
+ end
11
+
12
+ protected
13
+
14
+ def build_input_attributes
15
+ attributes[:type] = :file
16
+ super
17
+ attributes[:value] = false
18
+ end
19
+
20
+ def include_hidden?
21
+ return false if @include_hidden == false
22
+
23
+ attributes[:multiple]
24
+ end
25
+
26
+ def normalize_input(input_value)
27
+ input_value
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module Components
6
+ class FullError < Base
7
+ def view_template
8
+ p(**attributes) do
9
+ field.full_error
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def render?
16
+ field.show_errors? && field.has_errors?
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
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,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module Components
6
+ class Input < Base
7
+ include Concerns::HandlesInput
8
+
9
+ def view_template
10
+ input(**attributes)
11
+ end
12
+
13
+ protected
14
+
15
+ def build_attributes
16
+ super
17
+
18
+ # only overwrite id if it was set in Base
19
+ attributes[:id] = field.dom.id if attributes[:id] == "#{field.dom.id}_#{component_name}"
20
+
21
+ attributes[:name] = field.dom.name
22
+ attributes[:value] = field.dom.value
23
+
24
+ build_input_attributes
25
+ end
26
+
27
+ def build_input_attributes
28
+ attributes.fetch(:type) { attributes[:type] = field.inferred_input_type }
29
+ attributes.fetch(:disabled) { attributes[:disabled] = field.disabled? }
30
+
31
+ case attributes[:type]
32
+ when :text, :password, :email, :tel, :url, :search
33
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
34
+ attributes.fetch(:placeholder) { attributes[:placeholder] = field.placeholder }
35
+ attributes.fetch(:minlength) { attributes[:minlength] = field.minlength }
36
+ attributes.fetch(:maxlength) { attributes[:maxlength] = field.maxlength }
37
+ attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
38
+ attributes.fetch(:required) { attributes[:required] = field.required? }
39
+ attributes.fetch(:pattern) { attributes[:pattern] = field.pattern }
40
+ when :number
41
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
42
+ attributes.fetch(:placeholder) { attributes[:placeholder] = field.placeholder }
43
+ attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
44
+ attributes.fetch(:required) { attributes[:required] = field.required? }
45
+ attributes.fetch(:min) { attributes[:min] = field.min }
46
+ attributes.fetch(:max) { attributes[:max] = field.max }
47
+ attributes.fetch(:step) { attributes[:step] = field.step }
48
+ when :checkbox, :radio
49
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
50
+ attributes.fetch(:required) { attributes[:required] = field.required? }
51
+ when :file
52
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
53
+ attributes.fetch(:required) { attributes[:required] = field.required? }
54
+ attributes.fetch(:multiple) { attributes[:multiple] = field.multiple? }
55
+ when :date, :time, :datetime_local
56
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
57
+ attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
58
+ attributes.fetch(:required) { attributes[:required] = field.required? }
59
+ attributes.fetch(:min) { attributes[:min] = field.min }
60
+ attributes.fetch(:max) { attributes[:max] = field.max }
61
+ when :color
62
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
63
+ when :range
64
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
65
+ attributes.fetch(:min) { attributes[:min] = field.min }
66
+ attributes.fetch(:max) { attributes[:max] = field.max }
67
+ attributes.fetch(:step) { attributes[:step] = field.step }
68
+ when :hidden
69
+ attributes[:class] = false
70
+ attributes[:hidden] = true
71
+ attributes[:autocomplete] = "off"
72
+ else
73
+ # Handle any unrecognized input types
74
+ # Rails.logger.warn("Unhandled input type: #{attributes[:type]}")
75
+ end
76
+
77
+ if (attributes[:type] == :file) ? attributes[:multiple] : attributes.delete(:multiple)
78
+ attributes[:name] = "#{attributes[:name].sub(/\[\]$/, "")}[]"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module Components
6
+ class InputArray < Base
7
+ include Concerns::HandlesInput
8
+ include Concerns::HandlesArrayInput
9
+
10
+ def view_template
11
+ div(**attributes.slice(:id, :class)) do
12
+ field.multi(values.length) do |builder|
13
+ render builder.hidden_field_tag if builder.index == 0
14
+
15
+ field = builder.field(
16
+ label: builder.key,
17
+ # we expect key to be an integer string starting from "1"
18
+ value: values[builder.index]
19
+ )
20
+ if block_given?
21
+ yield field
22
+ else
23
+ render field.input_tag
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ protected
30
+
31
+ def build_attributes
32
+ super
33
+
34
+ attributes[:multiple] = true
35
+ end
36
+
37
+ private
38
+
39
+ def values
40
+ @values ||= Array(field.value)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
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
+
22
+ attributes.fetch(:for) { attributes[:for] = field.dom.id }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module Components
6
+ class RadioButton < Input
7
+ def view_template
8
+ input(**attributes, value: @checked_value)
9
+ end
10
+
11
+ def extract_input(...)
12
+ # when a radio is not submitted, nothing is returned
13
+ super.compact
14
+ end
15
+
16
+ protected
17
+
18
+ def build_input_attributes
19
+ attributes[:type] = :radio
20
+ super
21
+
22
+ @checked_value = (attributes.key?(:checked_value) ? attributes.delete(:checked_value) : "1").to_s
23
+
24
+ # this is a hack to workaround the fact that radio cannot be indexed/multiple
25
+ attributes[:name] = attributes[:name].sub(/\[\]$/, "")
26
+ attributes[:value] = @checked_value
27
+ attributes[:checked] = attributes.fetch(:checked) { checked? }
28
+ end
29
+
30
+ def checked?
31
+ field.dom.value == @checked_value
32
+ end
33
+
34
+ def normalize_input(...)
35
+ input_value = super
36
+ (input_value == @checked_value) ? input_value : nil
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module Components
6
+ class Select < Base
7
+ include Concerns::HandlesInput
8
+ include Concerns::HandlesArrayInput
9
+ include Concerns::HasOptions
10
+
11
+ def view_template
12
+ PUI::Select.new
13
+ end
14
+
15
+ protected
16
+
17
+ def options
18
+ option_mapper.each do |value, label|
19
+ option(selected: selected?(value), value: value) { label }
20
+ end
21
+ end
22
+
23
+ def blank_option(&)
24
+ option(selected: field.value.nil?, &)
25
+ end
26
+
27
+ def build_attributes
28
+ super
29
+
30
+ attributes[:id] = field.dom.id
31
+ attributes[:name] = field.dom.name
32
+
33
+ build_select_attributes
34
+ end
35
+
36
+ def build_select_attributes
37
+ @include_blank = attributes.delete(:include_blank)
38
+ @include_hidden = attributes.delete(:include_hidden)
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 include_blank?
52
+ return true if @include_blank == true
53
+
54
+ @include_blank != false && !attributes[:multiple]
55
+ end
56
+
57
+ def include_hidden?
58
+ return false if @include_hidden == false
59
+
60
+ attributes[:multiple]
61
+ end
62
+
63
+ def normalize_input(input_value)
64
+ attributes[:multiple] ? normalize_array_input(input_value) : normalize_simple_input(input_value)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module Components
6
+ class SubmitButton < Base
7
+ include Concerns::SubmitsDisplay
8
+
9
+ def view_template(&content)
10
+ content ||= proc { submit_type_label }
11
+ button(**attributes, &content)
12
+ end
13
+
14
+ protected
15
+
16
+ def build_attributes
17
+ root_key = field.dom.lineage.first.respond_to?(:dom_id) ? field.dom.lineage.first.dom_id : field.dom.lineage.first.key
18
+ attributes.fetch(:id) { attributes[:id] = "#{root_key}_submit_button" }
19
+ attributes[:class] = tokens(
20
+ component_name,
21
+ submit_type_value,
22
+ attributes[:class]
23
+ )
24
+
25
+ build_button_attributes
26
+ end
27
+
28
+ def build_button_attributes
29
+ formmethod = attributes[:formmethod]
30
+ if formmethod.present? && !/post|get/i.match?(formmethod) && !attributes.key?(:name) && !attributes.key?(:value)
31
+ attributes.merge! formmethod: :post, name: "_method", value: formmethod
32
+ end
33
+
34
+ attributes.fetch(:name) { attributes[:name] = "commit" }
35
+ attributes.fetch(:value) { attributes[:value] = submit_type_label }
36
+ attributes.fetch(:type) { attributes[:type] = :submit }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
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 Display
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,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module FieldOptions
6
+ module Associations
7
+ protected
8
+
9
+ def reflection
10
+ @reflection ||= find_association_reflection
11
+ end
12
+
13
+ def find_association_reflection
14
+ if object.class.respond_to?(:reflect_on_association)
15
+ object.class.reflect_on_association(key)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
5
+ module FieldOptions
6
+ module Autofocus
7
+ def focused?
8
+ options[:autofocus] == true
9
+ end
10
+
11
+ def focused!(autofocus = true)
12
+ options[:autofocus] = autofocus
13
+ self
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
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
+ collection_value_from_association || collection_value_from_validator
20
+ end
21
+
22
+ def collection_value_from_association
23
+ return unless reflection
24
+
25
+ relation = reflection.klass.all
26
+
27
+ if reflection.respond_to?(:scope) && reflection.scope
28
+ relation = if reflection.scope.parameters.any?
29
+ reflection.klass.instance_exec(object, &reflection.scope)
30
+ else
31
+ reflection.klass.instance_exec(&reflection.scope)
32
+ end
33
+ else
34
+ order = reflection.options[:order]
35
+ conditions = reflection.options[:conditions]
36
+ conditions = object.instance_exec(&conditions) if conditions.respond_to?(:call)
37
+
38
+ relation = relation.where(conditions) if relation.respond_to?(:where) && conditions.present?
39
+ relation = relation.order(order) if relation.respond_to?(:order)
40
+ end
41
+
42
+ relation
43
+ end
44
+
45
+ def collection_value_from_validator
46
+ return unless has_validators?
47
+
48
+ inclusion_validator = find_validator(:inclusion)
49
+ inclusion_validator.options[:in] || inclusion_validator.options[:within] if inclusion_validator
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
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,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Display
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
+
80
+ # Determines if the associated object is in a valid state
81
+ #
82
+ # An object is considered valid if it is persisted and has no errors.
83
+ #
84
+ # @return [Boolean] true if the object is persisted and has no errors, false otherwise
85
+ def object_valid?
86
+ object.respond_to?(:persisted?) && object.persisted? &&
87
+ object.respond_to?(:errors) && !object.errors.empty?
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end