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,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