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,174 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ phlexi-form (0.1.0)
5
+ activesupport
6
+ phlex (~> 1.10)
7
+ zeitwerk
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ actionpack (7.1.3.4)
13
+ actionview (= 7.1.3.4)
14
+ activesupport (= 7.1.3.4)
15
+ nokogiri (>= 1.8.5)
16
+ racc
17
+ rack (>= 2.2.4)
18
+ rack-session (>= 1.0.1)
19
+ rack-test (>= 0.6.3)
20
+ rails-dom-testing (~> 2.2)
21
+ rails-html-sanitizer (~> 1.6)
22
+ actionview (7.1.3.4)
23
+ activesupport (= 7.1.3.4)
24
+ builder (~> 3.1)
25
+ erubi (~> 1.11)
26
+ rails-dom-testing (~> 2.2)
27
+ rails-html-sanitizer (~> 1.6)
28
+ activesupport (7.1.3.4)
29
+ base64
30
+ bigdecimal
31
+ concurrent-ruby (~> 1.0, >= 1.0.2)
32
+ connection_pool (>= 2.2.5)
33
+ drb
34
+ i18n (>= 1.6, < 2)
35
+ minitest (>= 5.1)
36
+ mutex_m
37
+ tzinfo (~> 2.0)
38
+ ansi (1.5.0)
39
+ appraisal (2.5.0)
40
+ bundler
41
+ rake
42
+ thor (>= 0.14.0)
43
+ ast (2.4.2)
44
+ base64 (0.2.0)
45
+ bigdecimal (3.1.8)
46
+ builder (3.3.0)
47
+ bundle-audit (0.1.0)
48
+ bundler-audit
49
+ bundler-audit (0.9.1)
50
+ bundler (>= 1.2.0, < 3)
51
+ thor (~> 1.0)
52
+ combustion (1.5.0)
53
+ activesupport (>= 3.0.0)
54
+ railties (>= 3.0.0)
55
+ thor (>= 0.14.6)
56
+ concurrent-ruby (1.3.3)
57
+ connection_pool (2.4.1)
58
+ crass (1.0.6)
59
+ drb (2.2.1)
60
+ erubi (1.13.0)
61
+ i18n (1.14.5)
62
+ concurrent-ruby (~> 1.0)
63
+ io-console (0.7.2)
64
+ irb (1.14.0)
65
+ rdoc (>= 4.0.0)
66
+ reline (>= 0.4.2)
67
+ json (2.7.2)
68
+ language_server-protocol (3.17.0.3)
69
+ lint_roller (1.1.0)
70
+ loofah (2.22.0)
71
+ crass (~> 1.0.2)
72
+ nokogiri (>= 1.12.0)
73
+ minitest (5.24.1)
74
+ minitest-reporters (1.7.1)
75
+ ansi
76
+ builder
77
+ minitest (>= 5.0)
78
+ ruby-progressbar
79
+ mutex_m (0.2.0)
80
+ nokogiri (1.16.7-x86_64-darwin)
81
+ racc (~> 1.4)
82
+ parallel (1.25.1)
83
+ parser (3.3.4.0)
84
+ ast (~> 2.4.1)
85
+ racc
86
+ phlex (1.11.0)
87
+ psych (5.1.2)
88
+ stringio
89
+ racc (1.8.1)
90
+ rack (3.1.7)
91
+ rack-session (2.0.0)
92
+ rack (>= 3.0.0)
93
+ rack-test (2.1.0)
94
+ rack (>= 1.3)
95
+ rackup (2.1.0)
96
+ rack (>= 3)
97
+ webrick (~> 1.8)
98
+ rails-dom-testing (2.2.0)
99
+ activesupport (>= 5.0.0)
100
+ minitest
101
+ nokogiri (>= 1.6)
102
+ rails-html-sanitizer (1.6.0)
103
+ loofah (~> 2.21)
104
+ nokogiri (~> 1.14)
105
+ railties (7.1.3.4)
106
+ actionpack (= 7.1.3.4)
107
+ activesupport (= 7.1.3.4)
108
+ irb
109
+ rackup (>= 1.0.0)
110
+ rake (>= 12.2)
111
+ thor (~> 1.0, >= 1.2.2)
112
+ zeitwerk (~> 2.6)
113
+ rainbow (3.1.1)
114
+ rake (13.2.1)
115
+ rdoc (6.7.0)
116
+ psych (>= 4.0.0)
117
+ regexp_parser (2.9.2)
118
+ reline (0.5.9)
119
+ io-console (~> 0.5)
120
+ rexml (3.3.2)
121
+ strscan
122
+ rubocop (1.64.1)
123
+ json (~> 2.3)
124
+ language_server-protocol (>= 3.17.0)
125
+ parallel (~> 1.10)
126
+ parser (>= 3.3.0.2)
127
+ rainbow (>= 2.2.2, < 4.0)
128
+ regexp_parser (>= 1.8, < 3.0)
129
+ rexml (>= 3.2.5, < 4.0)
130
+ rubocop-ast (>= 1.31.1, < 2.0)
131
+ ruby-progressbar (~> 1.7)
132
+ unicode-display_width (>= 2.4.0, < 3.0)
133
+ rubocop-ast (1.31.3)
134
+ parser (>= 3.3.1.0)
135
+ rubocop-performance (1.21.1)
136
+ rubocop (>= 1.48.1, < 2.0)
137
+ rubocop-ast (>= 1.31.1, < 2.0)
138
+ ruby-progressbar (1.13.0)
139
+ standard (1.39.2)
140
+ language_server-protocol (~> 3.17.0.2)
141
+ lint_roller (~> 1.0)
142
+ rubocop (~> 1.64.0)
143
+ standard-custom (~> 1.0.0)
144
+ standard-performance (~> 1.4)
145
+ standard-custom (1.0.2)
146
+ lint_roller (~> 1.0)
147
+ rubocop (~> 1.50)
148
+ standard-performance (1.4.0)
149
+ lint_roller (~> 1.1)
150
+ rubocop-performance (~> 1.21.0)
151
+ stringio (3.1.1)
152
+ strscan (3.1.0)
153
+ thor (1.3.1)
154
+ tzinfo (2.0.6)
155
+ concurrent-ruby (~> 1.0)
156
+ unicode-display_width (2.5.0)
157
+ webrick (1.8.1)
158
+ zeitwerk (2.6.17)
159
+
160
+ PLATFORMS
161
+ x86_64-darwin
162
+
163
+ DEPENDENCIES
164
+ appraisal
165
+ bundle-audit
166
+ combustion
167
+ minitest
168
+ minitest-reporters
169
+ phlexi-form!
170
+ rake
171
+ standard
172
+
173
+ BUNDLED WITH
174
+ 2.5.16
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Installs Phlex Rails and Superform
3
+
4
+ Example:
5
+ bin/rails generate superform:install
6
+
7
+ This will create:
8
+ app/views/forms/application_form.rb
@@ -0,0 +1,34 @@
1
+ require "bundler"
2
+
3
+ class Superform::InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ APPLICATION_CONFIGURATION_PATH = Rails.root.join("config/application.rb")
7
+
8
+ def install_phlex_rails
9
+ return if gem_in_bundle? "phlex-rails"
10
+
11
+ gem "phlex-rails"
12
+ generate "phlex:install"
13
+ end
14
+
15
+ def autoload_components
16
+ return unless APPLICATION_CONFIGURATION_PATH.exist?
17
+
18
+ inject_into_class(
19
+ APPLICATION_CONFIGURATION_PATH,
20
+ "Application",
21
+ %( config.autoload_paths << "\#{root}/app/views/forms"\n)
22
+ )
23
+ end
24
+
25
+ def create_application_form
26
+ template "application_form.rb", Rails.root.join("app/views/forms/application_form.rb")
27
+ end
28
+
29
+ private
30
+
31
+ def gem_in_bundle?(gem_name)
32
+ Bundler.load.specs.any? { |spec| spec.name == gem_name }
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ class ApplicationForm < Superform::Rails::Form
2
+ include Phlex::Rails::Helpers::Pluralize
3
+
4
+ def row(component)
5
+ div do
6
+ render component.field.label(style: "display: block;")
7
+ render component
8
+ end
9
+ end
10
+
11
+ def around_template(&)
12
+ super do
13
+ error_messages
14
+ yield
15
+ submit
16
+ end
17
+ end
18
+
19
+ def error_messages
20
+ if model.errors.any?
21
+ div(style: "color: red;") do
22
+ h2 { "#{pluralize model.errors.count, "error"} prohibited this post from being saved:" }
23
+ ul do
24
+ model.errors.each do |error|
25
+ li { error.full_message }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/model_naming"
4
+ require "active_support/core_ext/module/delegation"
5
+ require "active_support/string_inquirer"
6
+ require "active_support/core_ext/object/blank"
7
+
8
+ module Phlexi
9
+ module Form
10
+ # A form component for building flexible and customizable forms.
11
+ #
12
+ # @example Basic usage
13
+ # Form.new(user, action: '/users', method: 'post') do |f|
14
+ # f.field :name
15
+ # f.field :email
16
+ # end
17
+ #
18
+ # @attr_reader [Symbol] key The form's key, derived from the record or explicitly set
19
+ # @attr_reader [ActiveModel::Model, nil] object The form's associated object
20
+ class Base < BaseComponent
21
+ include ActionView::ModelNaming
22
+
23
+ attr_reader :key, :object
24
+
25
+ delegate :field, :nest_one, :nest_many, to: :@namespace
26
+
27
+ # Initializes a new Form instance.
28
+ #
29
+ # @param record [ActiveModel::Model, Symbol, String] The form's associated record or key
30
+ # @param action [String, nil] The form's action URL
31
+ # @param method [String, nil] The form's HTTP method
32
+ # @param attributes [Hash] Additional HTML attributes for the form tag
33
+ # @param options [Hash] Additional options for form configuration
34
+ # @option options [String] :class CSS classes for the form
35
+ # @option options [Class] :namespace_klass Custom namespace class
36
+ # @option options [Class] :builder_klass Custom field builder class
37
+ def initialize(record, action: nil, method: nil, attributes: {}, **options)
38
+ @form_action = action
39
+ @form_method = method
40
+ @form_class = options.delete(:class)
41
+ @attributes = attributes
42
+ @options = options
43
+
44
+ initialize_object_and_key(record)
45
+ initialize_namespace
46
+ initialize_attributes
47
+ end
48
+
49
+ # Renders the form template.
50
+ #
51
+ # @return [void]
52
+ def view_template
53
+ form_tag { form_template }
54
+ end
55
+
56
+ # Executes the form's content block.
57
+ # Override this in subclasses to defie a static form.
58
+ #
59
+ # @return [void]
60
+ def form_template
61
+ instance_exec(&@_content_block) if @_content_block
62
+ end
63
+
64
+ # Renders the form tag with its contents.
65
+ #
66
+ # @yield The form's content
67
+ # @return [void]
68
+ def form_tag(&block)
69
+ form(**form_attributes) do
70
+ render_hidden_method_field
71
+ render_authenticity_token if authenticity_token?
72
+ yield
73
+ end
74
+ end
75
+
76
+ protected
77
+
78
+ attr_reader :options, :attributes
79
+
80
+ # Initializes the object and key based on the given record.
81
+ #
82
+ # @param record [ActiveModel::Model, Symbol, String] The form's associated record or key
83
+ # @return [void]
84
+ def initialize_object_and_key(record)
85
+ case record
86
+ when String, Symbol
87
+ @object = nil
88
+ @key = record
89
+ else
90
+ @object = convert_to_model(record)
91
+ @key = options.delete(:as)
92
+
93
+ if @key.nil?
94
+ unless object.respond_to?(:model_name) && object.model_name.respond_to?(:param_key) && object.model_name.param_key
95
+ raise ArgumentError, "record must respond to #model_name.param_key with a non nil value"
96
+ end
97
+ @key = object.model_name.param_key
98
+ end
99
+ end
100
+ @key = @key.to_sym
101
+ end
102
+
103
+ # Initializes the namespace for the form.
104
+ #
105
+ # @return [void]
106
+ def initialize_namespace
107
+ @namespace = namespace_klass.root(key, object: object, builder_klass: builder_klass)
108
+ end
109
+
110
+ # Initializes form attributes.
111
+ #
112
+ # @return [void]
113
+ def initialize_attributes
114
+ attributes[:accept_charset] ||= "UTF-8"
115
+ end
116
+
117
+ # Determines the form's action URL.
118
+ #
119
+ # @return [String, nil] The form's action URL
120
+ def form_action
121
+ puts ""
122
+ # if @form_action != false
123
+ # @form_action ||= if options[:format].nil?
124
+ # polymorphic_path(object, {})
125
+ # else
126
+ # polymorphic_path(object, format: options[:format])
127
+ # end
128
+ # end
129
+ @form_action
130
+ end
131
+
132
+ # Determines the form's HTTP method.
133
+ #
134
+ # @return [ActiveSupport::StringInquirer] The form's HTTP method
135
+ def form_method
136
+ @form_method ||= (object_form_method || "get").to_s.downcase
137
+ ActiveSupport::StringInquirer.new(@form_method)
138
+ end
139
+
140
+ # Retrieves the form's CSS classes.
141
+ #
142
+ # @return [String] The form's CSS classes
143
+ def form_class
144
+ @form_class || "flex flex-col space-y-6 px-4 py-2"
145
+ end
146
+
147
+ # Checks if the authenticity token should be included.
148
+ #
149
+ # @return [Boolean] True if the authenticity token should be included, false otherwise
150
+ def authenticity_token?
151
+ defined?(helpers) && options.fetch(:authenticity_token) { !form_method.get? }
152
+ end
153
+
154
+ # Retrieves the authenticity token.
155
+ #
156
+ # @return [String] The authenticity token
157
+ def authenticity_token
158
+ options.fetch(:authenticity_token) { helpers.form_authenticity_token }
159
+ end
160
+
161
+ # Renders the authenticity token field.
162
+ #
163
+ # @param name [String] The name attribute for the authenticity token field
164
+ # @param value [String] The value for the authenticity token field
165
+ # @return [void]
166
+ def authenticity_token_field(name = "authenticity_token", value = authenticity_token)
167
+ input(name: name, value: value, type: "hidden", hidden: true)
168
+ end
169
+
170
+ # Determines the appropriate form method based on the object's state.
171
+ #
172
+ # @return [String, nil] The appropriate form method
173
+ def object_form_method
174
+ return unless object
175
+ object.persisted? ? "patch" : "post"
176
+ end
177
+
178
+ # Retrieves the namespace class.
179
+ #
180
+ # @return [Class] The namespace class
181
+ def namespace_klass
182
+ @namespace_klass ||= options.delete(:namespace_klass) || Structure::Namespace
183
+ end
184
+
185
+ # Retrieves the builder class.
186
+ #
187
+ # @return [Class] The builder class
188
+ def builder_klass
189
+ @builder_klass ||= options.delete(:builder_klass) || Structure::FieldBuilder
190
+ end
191
+
192
+ # Renders the hidden method field for non-standard HTTP methods.
193
+ #
194
+ # @return [void]
195
+ def render_hidden_method_field
196
+ return if standard_form_method?
197
+ input(name: "_method", value: form_method, type: "hidden", hidden: true)
198
+ end
199
+
200
+ # Checks if the form method is standard (GET or POST).
201
+ #
202
+ # @return [Boolean] True if the form method is standard, false otherwise
203
+ def standard_form_method?
204
+ form_method.get? || form_method.post?
205
+ end
206
+
207
+ # Returns the standardized form method for the HTML form tag.
208
+ #
209
+ # @return [String] The standardized form method
210
+ def standardized_form_method
211
+ standard_form_method? ? form_method : "post"
212
+ end
213
+
214
+ # Generates the form attributes hash.
215
+ #
216
+ # @return [Hash] The form attributes
217
+ def form_attributes
218
+ {
219
+ action: form_action,
220
+ method: standardized_form_method,
221
+ class: form_class,
222
+ **attributes
223
+ }
224
+ end
225
+
226
+ # Renders the authenticity token if required.
227
+ #
228
+ # @return [void]
229
+ def render_authenticity_token
230
+ authenticity_token_field
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class Base < BaseComponent
7
+ attr_reader :field, :attributes
8
+
9
+ def initialize(field, **attributes)
10
+ @field = field
11
+ @attributes = attributes
12
+
13
+ build_attributes
14
+ end
15
+
16
+ protected
17
+
18
+ def build_attributes
19
+ attributes[:id] ||= "#{field.dom.id}_#{component_name}"
20
+ attributes[:class] = tokens(
21
+ component_name,
22
+ attributes[:class],
23
+ -> { field.required? } => "required",
24
+ -> { !field.required? } => "optional",
25
+ -> { field.has_errors? } => "invalid",
26
+ -> { field.readonly? } => "readonly",
27
+ -> { field.disabled? } => "disabled"
28
+ )
29
+ end
30
+
31
+ def component_name
32
+ @component_name ||= self.class.name.demodulize.underscore
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class Checkbox < Input
7
+ def view_template
8
+ input(type: :hidden, name: attributes[:name], value: @unchecked_value, autocomplete: "off", hidden: true) if include_hidden?
9
+ input(**attributes, value: @checked_value)
10
+ end
11
+
12
+ protected
13
+
14
+ def build_input_attributes
15
+ attributes[:type] = :checkbox
16
+ super
17
+
18
+ @include_hidden = attributes.delete(:include_hidden)
19
+ @checked_value = (attributes.key?(:checked_value) ? attributes.delete(:checked_value) : "1").to_s
20
+ @unchecked_value = (attributes.key?(:unchecked_value) ? attributes.delete(:unchecked_value) : "0").to_s
21
+
22
+ attributes[:value] = @checked_value
23
+ attributes[:checked] = attributes.fetch(:checked) { checked? }
24
+ end
25
+
26
+ def include_hidden?
27
+ @include_hidden != false
28
+ end
29
+
30
+ def checked?
31
+ return false if field.dom.value == @unchecked_value
32
+
33
+ if @checked_value == "1" # using default values
34
+ # handle nils, numbers and booleans
35
+ !["", "0", "false"].include?(field.dom.value)
36
+ else # custom value, so explicit match
37
+ field.dom.value == @checked_value
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class CollectionCheckboxes < Base
7
+ include Concerns::HasOptions
8
+
9
+ def view_template
10
+ render field.input_tag(type: :hidden, value: "", theme: false, hidden: true, autocomplete: "off", multiple: true)
11
+ field.multi(option_mapper.values) do |builder|
12
+ field = builder.field(
13
+ label: option_mapper[builder.key],
14
+ attributes: {
15
+ checked_value: builder.key,
16
+ include_hidden: false
17
+ }
18
+ )
19
+ if block_given?
20
+ yield field
21
+ else
22
+ render field.checkbox_tag
23
+ render field.label_tag
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ class CollectionRadioButtons < Base
7
+ include Concerns::HasOptions
8
+
9
+ def view_template
10
+ render field.input_tag(type: :hidden, value: "", theme: false, hidden: true, autocomplete: "off")
11
+ field.multi(option_mapper.values) do |builder|
12
+ field = builder.field(
13
+ label: option_mapper[builder.key],
14
+ attributes: {
15
+ checked_value: builder.key
16
+ }
17
+ )
18
+ if block_given?
19
+ yield field
20
+ else
21
+ render field.radio_button_tag
22
+ render field.label_tag
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ module Concerns
7
+ module HasOptions
8
+ protected
9
+
10
+ def build_attributes
11
+ super
12
+ @collection = attributes.delete(:collection) || field.collection
13
+ @label_method = attributes.delete(:label_method)
14
+ @value_method = attributes.delete(:value_method)
15
+ end
16
+
17
+ def option_mapper
18
+ @option_mapper ||= OptionMapper.new(@collection, label_method: @label_method, value_method: @value_method)
19
+ end
20
+
21
+ def selected?(option)
22
+ if field.multiple?
23
+ @options_list ||= Array(field.value)
24
+ @options_list.any? { |item| item.to_s == option.to_s }
25
+ else
26
+ field.dom.value == option.to_s
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
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,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
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