view_component-form 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 869789c616058368805d6dba5564a79c4e2ddb9fe85919721a052da761d34e91
4
- data.tar.gz: 58b02a2e260e61a97386e41d0c891d5aee3344ca4b5c6aebe3fb1f00514b75f4
3
+ metadata.gz: 72d1e925f6501a2af4458ca669c87bd9a947173fc4766ec4e72c466f794c649a
4
+ data.tar.gz: 6c8be1d09936f2ba0de85be506419472bbe84d9901628cbcbb4ea3f90c21b21f
5
5
  SHA512:
6
- metadata.gz: 82b77433712fbe3b1453d6dceebaa02bb01be4deba4ddcab28e97b1fe60667feacfaf50cc0d8cf2dcecc18db83738ae5a07441255db4728fcfe8420ef4340d86
7
- data.tar.gz: 2c08a19fa894929aaeb2a03afa0bd666fa05e9f63bed5e85671f74d32190d2a3d45f58c761a7da1bad2eb20744c7e498da3a9a6c2dcb40f9ba3c15e5afdcb967
6
+ metadata.gz: 524c3595df4a9891f036584ab79590a8bf2547c32cf74220fe6276ff3c3b6027e14b148ba558151513592a372e29c6ae0f9562c9ae23715500567a34e73c7b05
7
+ data.tar.gz: 20c3af9cacc78f509a3d10371ab117c6ec6e766ffd1fa418efce9fc0ac4eb87b5a751b4ec0d24a6aab4a0f2f0bc741a97b5064956ee34bfa822ab219e3a46964
data/CHANGELOG.md CHANGED
@@ -5,6 +5,47 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
+ Nothing yet
9
+
10
+ ## [0.2.0] - 2022-03-02
11
+ ### Added
12
+ - Test BuilderGenerator with generator\_spec (#64)
13
+ - Add HintComponent and ErrorMessageComponent (#98)
14
+ - Add `validation_context` option to `Form::Builder` (#101)
15
+ - Add `validators` helper to `FieldComponent` (#101)
16
+ - Add `optional?` and `required?` helpers to `FieldComponent` (#101)
17
+ - Add `label_text` helper (#103)
18
+ - Add `field_id` helper, backported from Rails 7.0 (#104)
19
+ - Add `weekday_select` helper (#105)
20
+ - Add README section about supported helpers (#106)
21
+ - Setup zeitwerk (#107)
22
+ - Add documentation for tests (#108)
23
+
24
+ ## [0.1.3] - 2022-01-11
25
+ ### Fixed
26
+ - Update dependencies for Rails 7.0.0 (#96)
27
+ - Bump to Rails 7 in Gemfile.lock (#99)
28
+
29
+ ## [0.1.2] - 2021-12-07
30
+ ### Added
31
+ - Add missing component specs (#75)
32
+ - Add missing builder specs for return values (#76)
33
+ - Add accurate test cases for all helpers from ActionView::Helpers::FormBuilder
34
+ documentation (#85)
35
+
36
+ ### Changed
37
+ - Cross-documented Rails form helpers (#84)
38
+ - Made tag_klass optional when inheriting from a component class (#87)
39
+ - Improve README: generator, html_class example (#88)
40
+ - Make rails version condition used the same way (#92)
41
+ - Add rails 7.0 and make rails head works (#94)
42
+ - Allow `Base` and `FieldComponent` to support forms without objects (#95)
43
+
44
+ ### Fixed
45
+ - Fix `phone_field` helper (#74)
46
+ - Fix `datetime_local_field` helper (#76)
47
+ - Fix `time_zone_select` helper (#76)
48
+ - Resolve Rails 6.1 deprecation on ActiveModel::Errors#keys call (#91)
8
49
 
9
50
  ## [0.1.1] - 2021-09-27
10
51
 
@@ -17,7 +58,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
58
  ## [0.1.0] - 2021-09-16
18
59
 
19
60
  ### Added
20
-
21
61
  - `FormBuilder`: add `.namespace` method to allow local lookup of components (#54)
22
62
  - Add basic `ViewComponent::Form::Builder` that can be used in place of Rails' `ActionView::Helpers::FormBuilder` (#1)
23
63
  - Add all standard FormBuilder helpers provided by Rails, implemented as ViewComponents (#4)
@@ -25,6 +65,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
25
65
  - Add CHANGELOG (#50)
26
66
  - Add CI (#2)
27
67
 
28
- [Unreleased]: https://github.com/pantographe/view_component-form/compare/v0.1.1...HEAD
68
+ [Unreleased]: https://github.com/pantographe/view_component-form/compare/v0.2.0...HEAD
69
+ [0.2.0]: https://github.com/pantographe/view_component-form/compare/v0.1.3...v0.2.0
70
+ [0.1.3]: https://github.com/pantographe/view_component-form/compare/v0.1.2...v0.1.3
71
+ [0.1.2]: https://github.com/pantographe/view_component-form/compare/v0.1.1...v0.1.2
29
72
  [0.1.1]: https://github.com/pantographe/view_component-form/compare/v0.1.0...v0.1.1
30
73
  [0.1.0]: https://github.com/pantographe/view_component-form/releases/tag/v0.1.0
data/README.md CHANGED
@@ -24,7 +24,7 @@ And then execute:
24
24
 
25
25
  ## Usage
26
26
 
27
- Add a `builder` param to your `form_for` of `form_with`:
27
+ Add a `builder` param to your `form_for`, `form_with`, `fields_for` or `fields`:
28
28
 
29
29
  ```diff
30
30
  - <%= form_for @user do |f| %>
@@ -48,7 +48,13 @@ Then call your helpers as usual:
48
48
  <%= f.email_field :email %> <%# renders a ViewComponent::Form::EmailFieldComponent %>
49
49
 
50
50
  <%= f.label :password %> <%# renders a ViewComponent::Form::LabelComponent %>
51
- <%= f.password_field :password %> <%# renders a ViewComponent::Form::PasswordFieldComponent %>
51
+ <%= f.password_field :password, aria: { describedby: f.field_id(:password, :description) } %>
52
+ <%# renders a ViewComponent::Form::PasswordFieldComponent %>
53
+ <div id="<%= f.field_id(:title, :description) %>">
54
+ <%= f.hint :password, 'The password should be at least 8 characters long' %>
55
+ <%# renders a ViewComponent::Form::HintComponent %>
56
+ <%= f.error_message :password %> <%# renders a ViewComponent::Form::ErrorMessageComponent %>
57
+ </div>
52
58
  <% end %>
53
59
  ```
54
60
 
@@ -64,12 +70,15 @@ It should work out of the box, but does nothing particularly interesting for now
64
70
 
65
71
  <label for="user_last_name">Last name</label>
66
72
  <input type="text" value="Doe" name="user[last_name]" id="user_last_name" />
67
-
73
+
68
74
  <label for="user_email">E-mail</label>
69
75
  <input type="email" value="john.doe@example.com" name="user[email]" id="user_email" />
70
-
76
+
71
77
  <label for="user_password">Password</label>
72
- <input type="password" name="user[password]" id="user_password" />
78
+ <input type="password" name="user[password]" id="user_password" aria-describedby="user_password_description" />
79
+ <div id="user_password_description">
80
+ <div>The password should be at least 8 characters long</div>
81
+ </div>
73
82
  </form>
74
83
  ```
75
84
 
@@ -91,14 +100,14 @@ This allows you to pick the namespace your components will be loaded from.
91
100
  # lib/custom_form_builder.rb
92
101
  class CustomFormBuilder < ViewComponent::Form::Builder
93
102
  # Set the namespace you want to use for your own components
94
- namespace Form
103
+ namespace Custom::Form
95
104
  end
96
105
  ```
97
106
 
98
- You can change the default namespace and path:
107
+ Use the generator options to change the default namespace or the path where the file will be created:
99
108
 
100
109
  ```console
101
- bin/rails generate vcf:builder AnotherCustomFormBuilder --namespace Forms::Components --path app/forms
110
+ bin/rails generate vcf:builder AnotherCustomFormBuilder --namespace AnotherCustom::Form --path app/forms
102
111
 
103
112
  create app/forms/another_custom_form_builder.rb
104
113
  ```
@@ -107,23 +116,22 @@ bin/rails generate vcf:builder AnotherCustomFormBuilder --namespace Forms::Compo
107
116
  # app/forms/another_custom_form_builder.rb
108
117
  class AnotherCustomFormBuilder < ViewComponent::Form::Builder
109
118
  # Set the namespace you want to use for your own components
110
- namespace Forms::Components
119
+ namespace AnotherCustom::Form
111
120
  end
112
121
  ```
113
122
 
114
- :warning: **Everything below this line describes the future usage and is subject to change. It does not work yet as the gem is still under heavy development.**
115
-
116
- Now let's generate your own components to customize the rendering.
123
+ Now let's generate your own components to customize their rendering. We can use the standard view_component generator:
117
124
 
118
125
  ```console
119
- bin/rails generate vcf:component Form::TextField
126
+ bin/rails generate component Custom::Form::TextField --inline --parent ViewComponent::Form::TextFieldComponent
120
127
 
121
128
  invoke test_unit
122
- create test/components/form/text_field_component_test.rb
123
- create app/components/form/text_field_component.rb
124
- create app/components/form/text_field_component.html.erb
129
+ create test/components/custom/form/text_field_component_test.rb
130
+ create app/components/custom/form/text_field_component.rb
125
131
  ```
126
132
 
133
+ :warning: The `--parent` option is available since ViewComponent [`v2.41.0`](https://viewcomponent.org/CHANGELOG.html#2410). If you're using a previous version, you can always edit the generated `Custom::Form::CustomTextFieldComponent` class to make it inherit from `ViewComponent::Form::TextFieldComponent`.
134
+
127
135
  Change your forms to use your new builder:
128
136
 
129
137
  ```diff
@@ -131,26 +139,247 @@ Change your forms to use your new builder:
131
139
  + <%= form_for @user, builder: CustomFormBuilder do |f| %>
132
140
  ```
133
141
 
134
- You can then customize the behavior of your `Form::TextFieldComponent`:
142
+ You can then customize the behavior of your `Custom::Form::CustomTextFieldComponent`:
135
143
 
136
144
  ```rb
137
- # app/components/form/text_field_component.rb
145
+ # app/components/custom/form/text_field_component.rb
138
146
 
139
- module Form
140
- class TextFieldComponent < ViewComponent::Form::TextFieldComponent
141
- def html_class
142
- class_names("text-field", "border-error": method_errors?)
143
- end
147
+ class Admin::Form::TextFieldComponent < ViewComponent::Form::TextFieldComponent
148
+ def html_class
149
+ class_names("custom-text-field", "has-error": method_errors?)
144
150
  end
145
151
  end
146
152
  ```
147
153
 
148
- The generated form field with now have your class names:
154
+ In this case we leverage the [`#class_names`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-class_names) helper to:
155
+ - always add the `custom-text-field` class;
156
+ - add the `has-error` class if there is an error on the attribute (using `ViewComponent::Form::FieldComponent#method_errors?`).
157
+
158
+ The rendered form field will now look like this:
159
+
160
+ ```html
161
+ <input class="custom-text-field" type="text" value="John" name="user[first_name]" id="user_first_name">
162
+ ```
163
+
164
+ You can use the same approach to inject options, wrap the input in a `<div>`, etc.
165
+
166
+ We'll add more use cases to the documentation soon.
167
+
168
+ ### Building your own components
169
+
170
+ When building your own ViewComponents for using in forms, it's recommended to inherit from `ViewComponent::Form::FieldComponent`, so you get access to the following helpers:
171
+
172
+ #### `#label_text`
173
+
174
+ Returns the translated text for the label of the field (looking up for `helpers.label.OBJECT.METHOD_NAME`), or humanized version of the method name if not available.
175
+
176
+ ```rb
177
+ # app/components/custom/form/group_component.rb
178
+ class Custom::Form::GroupComponent < ViewComponent::Form::FieldComponent
179
+ end
180
+ ```
181
+
182
+ ```erb
183
+ <%# app/components/custom/form/group_component.html.erb %>
184
+ <div class="custom-form-group">
185
+ <label>
186
+ <%= label_text %><br />
187
+ <%= content %>
188
+ </label>
189
+ </div>
190
+ ```
191
+
192
+ ```erb
193
+ <%# app/views/users/_form.html.erb %>
194
+ <%= form_for @user do |f| %>
195
+ <%= f.group :first_name do %>
196
+ <%= f.text_field :first_name %>
197
+ <% end %>
198
+ <% end %>
199
+ ```
200
+
201
+ ```yml
202
+ # config/locales/en.yml
203
+ en:
204
+ helpers:
205
+ label:
206
+ user:
207
+ first_name: Your first name
208
+ ```
209
+
210
+ Renders:
149
211
 
150
212
  ```html
151
- <input class="text-field" type="text" value="John" name="user[first_name]" id="user_first_name">
213
+ <form class="edit_user" id="edit_user_1" action="/users/1" accept-charset="UTF-8" method="post">
214
+ <!-- ... -->
215
+ <label>
216
+ Your first name<br />
217
+ <input type="text" value="John" name="user[first_name]" id="user_first_name" />
218
+ </label>
219
+ </form>
220
+ ```
221
+
222
+ #### Validations
223
+
224
+ Let's consider the following model for the examples below.
225
+
226
+ ```rb
227
+ # app/models/user.rb
228
+ class User < ActiveRecord::Base
229
+ validates :first_name, presence: true, length: { minimum: 2, maximum: 255 }
230
+ end
231
+ ```
232
+
233
+ ##### Accessing validations with `#validators`
234
+
235
+ Returns all validators for the method name.
236
+
237
+ ```rb
238
+ # app/components/custom/form/group_component.rb
239
+ class Custom::Form::GroupComponent < ViewComponent::Form::FieldComponent
240
+ private
241
+
242
+ def validation_hint
243
+ if length_validator
244
+ "between #{length_validator.options[:minimum]} and #{length_validator.options[:maximum]} chars"
245
+ end
246
+ end
247
+
248
+ def length_validator
249
+ validators.find { |v| v.is_a?(ActiveModel::Validations::LengthValidator) }
250
+ end
251
+ end
252
+ ```
253
+
254
+ ```erb
255
+ <%# app/components/custom/form/group_component.html.erb %>
256
+ <div class="custom-form-group">
257
+ <label>
258
+ <%= label_text %> (<%= validation_hint %>)<br />
259
+ <%= content %>
260
+ </label>
261
+ </div>
152
262
  ```
153
263
 
264
+ ##### Using `#required?` and `#optional?`
265
+
266
+ ```erb
267
+ <%# app/components/custom/form/group_component.html.erb %>
268
+ <div class="custom-form-group">
269
+ <label>
270
+ <%= label_text %><%= " (required)" if required? %><br />
271
+ <%= content %>
272
+ </label>
273
+ </div>
274
+ ```
275
+
276
+ ##### Validation contexts
277
+
278
+ When using [validation contexts](https://guides.rubyonrails.org/active_record_validations.html#on), you can specify a context to the helpers above.
279
+
280
+ ```rb
281
+ # app/models/user.rb
282
+ class User < ActiveRecord::Base
283
+ validates :first_name, presence: true, length: { minimum: 2, maximum: 255 }
284
+ validates :email, presence: true, on: :registration
285
+ end
286
+ ```
287
+
288
+ ```erb
289
+ <%# app/views/users/_form_.html.erb %>
290
+ <%= form_with model: @user,
291
+ builder: ViewComponent::Form::Builder,
292
+ validation_context: :registration do |f| %>
293
+ <%= f.group :email do %>
294
+ <%= f.email_field :email %>
295
+ <% end %>
296
+ <% end %>
297
+ ```
298
+
299
+ In this case, `ViewComponent::Form::Builder` accepts a `validation_context` option and passes it as a default value to the `#validators`, `#required?` and `#optional?` helpers.
300
+
301
+ Alternatively, you can pass the context to the helpers:
302
+
303
+ ```erb
304
+ <%= "(required)" if required?(context: :registration) %>
305
+ ```
306
+
307
+ ```rb
308
+ def length_validator
309
+ validators(context: :registration).find { |v| v.is_a?(ActiveModel::Validations::LengthValidator) }
310
+ end
311
+ ```
312
+
313
+ ### Using your form components without a backing model
314
+
315
+ If you want to ensure that your fields display consistently across your app, you'll need to lean on Rails' own helpers. You may be used to using form tag helpers such as `text_field_tag` to generate tags, or even writing out plain HTML tags. These can't be integrated with a form builder, so they won't offer you the benefits of this gem.
316
+
317
+ You'll most likely want to use either:
318
+
319
+ - [`form_with`](https://api.rubyonrails.org/v6.1.4/classes/ActionView/Helpers/FormHelper.html#method-i-form_with) and supply a route as the endpoint, e.g. `form_with url: users_path do |f| ...`, or
320
+ - [`fields`](https://api.rubyonrails.org/v6.1.4/classes/ActionView/Helpers/FormHelper.html#method-i-fields), supplying a namespace if necessary. `fields do |f| ...` ought to work in the most basic case.
321
+
322
+ [`fields_for`](https://api.rubyonrails.org/v6.1.4/classes/ActionView/Helpers/FormHelper.html#method-i-fields_for) may also be of interest. To make consistent use of `view_component-form`, you'll want to be using these three helpers to build your forms wherever possible.
323
+
324
+ ## Supported helpers
325
+
326
+ The following helpers are currently supported by `ViewComponent::Form`.
327
+
328
+ ### `ActionView::Helpers::FormBuilder`
329
+
330
+ **Supported:** `button` `check_box` `collection_check_boxes` `collection_radio_buttons` `collection_select` `color_field` `date_field` `date_select` `datetime_field` `datetime_local_field` `datetime_select` `email_field` `fields` `fields_for` `file_field` `field_id` `grouped_collection_select` `hidden_field` `month_field` `number_field` `password_field` `phone_field` `radio_button` `range_field` `search_field` `select` `submit` `telephone_field` `text_area` `text_field` `time_field` `time_select` `time_zone_select` `to_model` `to_partial_path` `url_field` `week_field` `weekday_select`
331
+
332
+ **Partially supported:** `label` (blocks not supported) `rich_text_area` (untested)
333
+
334
+ **Unsupported for now:** `field_name`
335
+
336
+ ### Specific to `ViewComponent::Form`
337
+
338
+ **Supported:** `error_message` `hint`
339
+
340
+ ## Testing your components
341
+
342
+ ### RSpec
343
+
344
+ #### Configuration
345
+
346
+ This assumes your already have read and configured [tests for `view_component`](https://viewcomponent.org/guide/testing.html#rspec-configuration).
347
+
348
+ ```rb
349
+ # spec/rails_helper.rb
350
+ require "view_component/test_helpers"
351
+ require "view_component/form/test_helpers"
352
+ require "capybara/rspec"
353
+
354
+ RSpec.configure do |config|
355
+ config.include ViewComponent::TestHelpers, type: :component
356
+ config.include ViewComponent::Form::TestHelpers, type: :component
357
+ config.include Capybara::RSpecMatchers, type: :component
358
+ end
359
+ ```
360
+
361
+ #### Example
362
+
363
+ ```rb
364
+ # spec/components/form/text_field_component_spec.rb
365
+ RSpec.describe Form::TextFieldComponent, type: :component do
366
+ let(:object) { User.new } # replace with a model of your choice
367
+ let(:form) { form_with(object) }
368
+ let(:options) { {} }
369
+
370
+ let(:component) { render_inline(described_class.new(form, object_name, :first_name, options)) }
371
+
372
+ context "with simple args" do
373
+ it do
374
+ expect(component.to_html)
375
+ .to have_tag("input", with: { name: "user[first_name]", id: "user_first_name", type: "text" })
376
+ end
377
+ end
378
+ end
379
+ ```
380
+
381
+ For more complex components, we recommend the [`rspec-html-matchers` gem](https://github.com/kucaahbe/rspec-html-matchers).
382
+
154
383
  ## Development
155
384
 
156
385
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -7,15 +7,12 @@ module ViewComponent
7
7
  attr_accessor :default_options
8
8
  end
9
9
 
10
- if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new("6.1")
11
- require "view_component/form/class_names_helper"
12
- include ClassNamesHelper
13
- end
10
+ include ClassNamesHelper if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new("6.1")
14
11
 
15
12
  attr_reader :form, :object_name, :options
16
13
 
17
- delegate :object, to: :form
18
- delegate :errors, to: :object, prefix: true
14
+ delegate :object, to: :form, allow_nil: true
15
+ delegate :errors, to: :object, prefix: true, allow_nil: true
19
16
 
20
17
  def initialize(form, object_name, options = {})
21
18
  @form = form
@@ -28,6 +25,8 @@ module ViewComponent
28
25
  end
29
26
 
30
27
  def object_errors?
28
+ return false unless object
29
+
31
30
  object.errors.any?
32
31
  end
33
32
 
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Form
5
+ class ErrorMessageComponent < FieldComponent
6
+ class_attribute :tag, instance_reader: false, instance_writer: false, instance_accessor: false,
7
+ instance_predicate: false
8
+
9
+ self.tag = :div
10
+
11
+ def call
12
+ tag.public_send(self.class.tag, messages, **options)
13
+ end
14
+
15
+ def render?
16
+ method_errors?
17
+ end
18
+
19
+ def messages
20
+ safe_join(method_errors, tag.br)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -3,12 +3,13 @@
3
3
  module ViewComponent
4
4
  module Form
5
5
  class FieldComponent < BaseComponent
6
- class << self
7
- attr_accessor :tag_klass
8
- end
6
+ class_attribute :tag_klass, instance_reader: false, instance_writer: false, instance_accessor: false,
7
+ instance_predicate: false
9
8
 
10
9
  attr_reader :method_name
11
10
 
11
+ delegate :validation_context, to: :form
12
+
12
13
  def initialize(form, object_name, method_name, options = {})
13
14
  # See: https://github.com/rails/rails/blob/83217025a171593547d1268651b446d3533e2019/actionview/lib/action_view/helpers/tags/base.rb#L13
14
15
  @method_name = method_name.to_s.dup
@@ -31,8 +32,18 @@ module ViewComponent
31
32
  .map(&:upcase_first)
32
33
  end
33
34
 
34
- def method_errors?
35
- (object_errors.keys & object_method_names).any?
35
+ if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new("6.1")
36
+ def method_errors?
37
+ return false unless object_errors
38
+
39
+ (object_errors.attribute_names & object_method_names).any?
40
+ end
41
+ else
42
+ def method_errors?
43
+ return false unless object_errors
44
+
45
+ (object_errors.keys & object_method_names).any?
46
+ end
36
47
  end
37
48
 
38
49
  def value
@@ -51,6 +62,36 @@ module ViewComponent
51
62
  end
52
63
  end
53
64
 
65
+ # From https://github.com/rails/rails/blob/497ab719d04a2d505f4d6a76c9d359b3d7f8e502/actionview/lib/action_view/helpers/tags/label.rb#L18-L27
66
+ def label_text
67
+ content ||= ActionView::Helpers::Tags::Translator.new(object, object_name, method_name,
68
+ scope: "helpers.label").translate
69
+ content ||= method_name.humanize
70
+ content
71
+ end
72
+
73
+ def optional?(context: validation_context)
74
+ return nil if object.nil?
75
+
76
+ !required?(context: context)
77
+ end
78
+
79
+ def required?(context: validation_context)
80
+ return nil if object.nil?
81
+
82
+ validators(context: context).any?(ActiveModel::Validations::PresenceValidator)
83
+ end
84
+
85
+ def validators(context: validation_context)
86
+ method_validators.select do |validator|
87
+ if context.nil?
88
+ validator.options[:on].blank?
89
+ else
90
+ Array(validator.options[:on]).include?(context&.to_sym)
91
+ end
92
+ end
93
+ end
94
+
54
95
  private
55
96
 
56
97
  def singular_association_method_name
@@ -60,6 +101,14 @@ module ViewComponent
60
101
  def collection_association_method_name
61
102
  @collection_association_method_name ||= method_name.to_s.sub(/_ids$/, "").pluralize.to_sym
62
103
  end
104
+
105
+ def method_validators
106
+ @method_validators ||= if object.nil?
107
+ []
108
+ else
109
+ object.class.validators_on(method_name)
110
+ end
111
+ end
63
112
  end
64
113
  end
65
114
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Form
5
+ class HintComponent < FieldComponent
6
+ class_attribute :tag, instance_reader: false, instance_writer: false, instance_accessor: false,
7
+ instance_predicate: false
8
+ attr_reader :attribute_content
9
+
10
+ self.tag = :div
11
+
12
+ def initialize(form, object_name, method_name, content_or_options = nil, options = nil)
13
+ options ||= {}
14
+
15
+ content_is_options = content_or_options.is_a?(Hash)
16
+ if content_is_options
17
+ options.merge! content_or_options
18
+ @attribute_content = nil
19
+ else
20
+ @attribute_content = content_or_options
21
+ end
22
+
23
+ super(form, object_name, method_name, options)
24
+ end
25
+
26
+ def call
27
+ content_or_options = content.presence || attribute_content.presence
28
+
29
+ tag.public_send(self.class.tag, content_or_options, **options)
30
+ end
31
+
32
+ def render?
33
+ content.present? || attribute_content.present?
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ if Rails::VERSION::MAJOR >= 7
4
+ module ViewComponent
5
+ module Form
6
+ class WeekdaySelectComponent < FieldComponent
7
+ attr_reader :html_options
8
+
9
+ def initialize(form, object_name, method_name, options = {}, html_options = {})
10
+ @html_options = html_options
11
+
12
+ super(form, object_name, method_name, options)
13
+
14
+ set_html_options!
15
+ end
16
+
17
+ def call
18
+ ActionView::Helpers::Tags::WeekdaySelect.new(
19
+ object_name,
20
+ method_name,
21
+ @view_context,
22
+ options,
23
+ html_options
24
+ ).render
25
+ end
26
+
27
+ protected
28
+
29
+ def set_html_options!
30
+ @html_options[:class] = class_names(html_options[:class], html_class)
31
+ @html_options.delete(:class) if @html_options[:class].blank?
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rails/generators"
4
-
5
3
  module Vcf
6
4
  module Generators
7
5
  class BuilderGenerator < Rails::Generators::NamedBase
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class <%= class_name %> < ViewComponent::Form::Builder
2
4
  # Set the namespace you want to use for your own components
3
5
  namespace "<%= components_namespace %>"
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_view"
4
-
5
3
  module ViewComponent
6
4
  module Form
7
5
  class Builder < ActionView::Helpers::FormBuilder
@@ -29,13 +27,28 @@ module ViewComponent
29
27
  end
30
28
  end
31
29
 
30
+ attr_reader :validation_context
31
+
32
32
  def initialize(*)
33
33
  @__component_klass_cache = {}
34
34
 
35
35
  super
36
- end
37
36
 
38
- (field_helpers - %i[label check_box radio_button fields_for fields hidden_field file_field]).each do |selector|
37
+ @validation_context = options[:validation_context]
38
+ end
39
+
40
+ (field_helpers - %i[
41
+ check_box
42
+ datetime_field
43
+ datetime_local_field
44
+ fields
45
+ fields_for
46
+ file_field
47
+ hidden_field
48
+ label
49
+ phone_field
50
+ radio_button
51
+ ]).each do |selector|
39
52
  class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
40
53
  def #{selector}(method, options = {}) # def text_field(method, options = {})
41
54
  render_component( # render_component(
@@ -47,12 +60,20 @@ module ViewComponent
47
60
  end # end
48
61
  RUBY_EVAL
49
62
  end
63
+ alias phone_field telephone_field
50
64
 
51
65
  # See: https://github.com/rails/rails/blob/33d60cb02dcac26d037332410eabaeeb0bdc384c/actionview/lib/action_view/helpers/form_helper.rb#L2280
52
66
  def label(method, text = nil, options = {}, &block)
53
67
  render_component(:label, @object_name, method, text, objectify_options(options), &block)
54
68
  end
55
69
 
70
+ def datetime_field(method, options = {})
71
+ render_component(
72
+ :datetime_local_field, @object_name, method, objectify_options(options)
73
+ )
74
+ end
75
+ alias datetime_locale_field datetime_field
76
+
56
77
  def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
57
78
  render_component(
58
79
  :check_box, @object_name, method, checked_value, unchecked_value, objectify_options(options)
@@ -88,18 +109,6 @@ module ViewComponent
88
109
  render_component(:button, value, options, &block)
89
110
  end
90
111
 
91
- # SELECTORS.each do |selector|
92
- # class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
93
- # def #{selector}(*args)
94
- # render_component(
95
- # :#{selector},
96
- # *args,
97
- # super,
98
- # )
99
- # end
100
- # RUBY_EVAL
101
- # end
102
-
103
112
  # See: https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionview/lib/action_view/helpers/form_options_helper.rb
104
113
  def select(method, choices = nil, options = {}, html_options = {}, &block)
105
114
  render_component(
@@ -169,9 +178,9 @@ module ViewComponent
169
178
  )
170
179
  end
171
180
 
172
- def time_zone_select(method, options = {}, html_options = {})
181
+ def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
173
182
  render_component(
174
- :time_zone_select, @object_name, method,
183
+ :time_zone_select, @object_name, method, priority_zones,
175
184
  objectify_options(options), @default_html_options.merge(html_options)
176
185
  )
177
186
  end
@@ -182,6 +191,33 @@ module ViewComponent
182
191
  end
183
192
  end
184
193
 
194
+ def error_message(method, options = {})
195
+ render_component(:error_message, @object_name, method, objectify_options(options))
196
+ end
197
+
198
+ def hint(method, text = nil, options = {}, &block)
199
+ render_component(:hint, @object_name, method, text, objectify_options(options), &block)
200
+ end
201
+
202
+ # Backport field_id from Rails 7.0
203
+ if Rails::VERSION::MAJOR < 7
204
+ def field_id(method_name, *suffixes, namespace: @options[:namespace], index: @index)
205
+ object_name = object_name.model_name.singular if object_name.respond_to?(:model_name)
206
+
207
+ sanitized_object_name = object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").delete_suffix("_")
208
+
209
+ sanitized_method_name = method_name.to_s.delete_suffix("?")
210
+
211
+ [
212
+ namespace,
213
+ sanitized_object_name.presence,
214
+ (index unless sanitized_object_name.empty?),
215
+ sanitized_method_name,
216
+ *suffixes
217
+ ].tap(&:compact!).join("_")
218
+ end
219
+ end
220
+
185
221
  private
186
222
 
187
223
  def render_component(component_name, *args, &block)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Backport of https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-class_names
4
+ # :nocov:
4
5
  module ViewComponent
5
6
  module Form
6
7
  module ClassNamesHelper
@@ -35,3 +36,4 @@ module ViewComponent
35
36
  end
36
37
  end
37
38
  end
39
+ # :nocov:
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rails/engine"
4
-
5
3
  module ViewComponent
6
4
  module Form
7
5
  # :nodoc:
@@ -7,20 +7,24 @@ require "action_view"
7
7
  module ViewComponent
8
8
  module Form
9
9
  module TestHelpers
10
- def form_with(object, options = {})
11
- ViewComponent::Form::Builder.new(object_name, object, template, options)
10
+ def form_with(object, builder: ViewComponent::Form::Builder, **options)
11
+ builder.new(object_name, object, template, options)
12
12
  end
13
13
 
14
14
  def object_name
15
15
  :user
16
16
  end
17
17
 
18
- def template
19
- lookup_context = ActionView::LookupContext.new(ActionController::Base.view_paths)
18
+ if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new("6.1")
19
+ def template
20
+ lookup_context = ActionView::LookupContext.new(ActionController::Base.view_paths)
20
21
 
21
- if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new("6.1")
22
22
  ActionView::Base.new(lookup_context, {}, ApplicationController.new)
23
- else
23
+ end
24
+ else
25
+ def template
26
+ lookup_context = ActionView::LookupContext.new(ActionController::Base.view_paths)
27
+
24
28
  ActionView::Base.new(lookup_context, {})
25
29
  end
26
30
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ViewComponent
4
4
  module Form
5
- VERSION = "0.1.1"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -1,5 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "form/version"
3
+ require "view_component"
4
+ require "zeitwerk"
5
+
6
+ loader = Zeitwerk::Loader.for_gem
7
+ form = "#{__dir__}/form.rb"
8
+ loader.ignore(form)
9
+ loader.push_dir("#{__dir__}/form", namespace: ViewComponent::Form)
10
+ loader.setup
11
+
4
12
  require_relative "form/engine"
5
- require_relative "form/builder"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component-form
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pantographe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-27 00:00:00.000000000 Z
11
+ date: 2022-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionview
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: 6.0.0
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '7.0'
22
+ version: '7.1'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: 6.0.0
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '7.0'
32
+ version: '7.1'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: activesupport
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -39,7 +39,7 @@ dependencies:
39
39
  version: 6.0.0
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: '7.0'
42
+ version: '7.1'
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -49,7 +49,7 @@ dependencies:
49
49
  version: 6.0.0
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: '7.0'
52
+ version: '7.1'
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: view_component
55
55
  requirement: !ruby/object:Gem::Requirement
@@ -70,6 +70,20 @@ dependencies:
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
72
  version: '3.0'
73
+ - !ruby/object:Gem::Dependency
74
+ name: zeitwerk
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '2.5'
80
+ type: :runtime
81
+ prerelease: false
82
+ version_requirements: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - "~>"
85
+ - !ruby/object:Gem::Version
86
+ version: '2.5'
73
87
  description: Rails FormBuilder for ViewComponent
74
88
  email:
75
89
  - oss@pantographe.studio
@@ -89,13 +103,14 @@ files:
89
103
  - app/components/view_component/form/color_field_component.rb
90
104
  - app/components/view_component/form/date_field_component.rb
91
105
  - app/components/view_component/form/date_select_component.rb
92
- - app/components/view_component/form/datetime_field_component.rb
93
106
  - app/components/view_component/form/datetime_local_field_component.rb
94
107
  - app/components/view_component/form/datetime_select_component.rb
95
108
  - app/components/view_component/form/email_field_component.rb
109
+ - app/components/view_component/form/error_message_component.rb
96
110
  - app/components/view_component/form/field_component.rb
97
111
  - app/components/view_component/form/file_field_component.rb
98
112
  - app/components/view_component/form/grouped_collection_select_component.rb
113
+ - app/components/view_component/form/hint_component.rb
99
114
  - app/components/view_component/form/label_component.rb
100
115
  - app/components/view_component/form/month_field_component.rb
101
116
  - app/components/view_component/form/number_field_component.rb
@@ -114,6 +129,7 @@ files:
114
129
  - app/components/view_component/form/time_zone_select_component.rb
115
130
  - app/components/view_component/form/url_field_component.rb
116
131
  - app/components/view_component/form/week_field_component.rb
132
+ - app/components/view_component/form/weekday_select_component.rb
117
133
  - lib/generators/vcf/builder/builder_generator.rb
118
134
  - lib/generators/vcf/builder/templates/builder.rb.erb
119
135
  - lib/view_component/form.rb
@@ -130,6 +146,7 @@ metadata:
130
146
  changelog_uri: https://github.com/pantographe/view_component-form/blob/master/CHANGELOG.md
131
147
  source_code_uri: https://github.com/pantographe/view_component-form
132
148
  bug_tracker_uri: https://github.com/pantographe/view_component-form/issues
149
+ rubygems_mfa_required: 'true'
133
150
  post_install_message:
134
151
  rdoc_options: []
135
152
  require_paths:
@@ -145,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
162
  - !ruby/object:Gem::Version
146
163
  version: '0'
147
164
  requirements: []
148
- rubygems_version: 3.2.26
165
+ rubygems_version: 3.3.4
149
166
  signing_key:
150
167
  specification_version: 4
151
168
  summary: Rails FormBuilder for ViewComponent
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ViewComponent
4
- module Form
5
- class DatetimeFieldComponent < FieldComponent
6
- self.tag_klass = ActionView::Helpers::Tags::DatetimeField
7
- end
8
- end
9
- end