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 +4 -4
- data/CHANGELOG.md +45 -2
- data/README.md +254 -25
- data/app/components/view_component/form/base_component.rb +5 -6
- data/app/components/view_component/form/error_message_component.rb +24 -0
- data/app/components/view_component/form/field_component.rb +54 -5
- data/app/components/view_component/form/hint_component.rb +37 -0
- data/app/components/view_component/form/weekday_select_component.rb +36 -0
- data/lib/generators/vcf/builder/builder_generator.rb +0 -2
- data/lib/generators/vcf/builder/templates/builder.rb.erb +2 -0
- data/lib/view_component/form/builder.rb +54 -18
- data/lib/view_component/form/class_names_helper.rb +2 -0
- data/lib/view_component/form/engine.rb +0 -2
- data/lib/view_component/form/test_helpers.rb +10 -6
- data/lib/view_component/form/version.rb +1 -1
- data/lib/view_component/form.rb +9 -2
- metadata +25 -8
- data/app/components/view_component/form/datetime_field_component.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72d1e925f6501a2af4458ca669c87bd9a947173fc4766ec4e72c466f794c649a
|
4
|
+
data.tar.gz: 6c8be1d09936f2ba0de85be506419472bbe84d9901628cbcbb4ea3f90c21b21f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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`
|
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
|
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
|
-
|
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
|
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
|
119
|
+
namespace AnotherCustom::Form
|
111
120
|
end
|
112
121
|
```
|
113
122
|
|
114
|
-
|
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
|
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::
|
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
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
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
|
-
<
|
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
|
-
|
7
|
-
|
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
|
-
|
35
|
-
|
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 "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
|
-
|
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:
|
@@ -7,20 +7,24 @@ require "action_view"
|
|
7
7
|
module ViewComponent
|
8
8
|
module Form
|
9
9
|
module TestHelpers
|
10
|
-
def form_with(object,
|
11
|
-
|
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
|
-
|
19
|
-
|
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
|
-
|
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
|
data/lib/view_component/form.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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.
|
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:
|
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.
|
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.
|
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.
|
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.
|
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.
|
165
|
+
rubygems_version: 3.3.4
|
149
166
|
signing_key:
|
150
167
|
specification_version: 4
|
151
168
|
summary: Rails FormBuilder for ViewComponent
|