view_component-form 0.3.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 303f19a27ddf8ea02dfcaa077fb20686c5100dda7be4f4338db348de5da11958
4
- data.tar.gz: 3c33a63d1d96faa2fb67d32d12b0d90af844099e19157402487ebdc84a17fb5f
3
+ metadata.gz: 7f547b2b85b760834bd94eeb1e4b4fefbed6d79a0cc666f79e1f01acf53cc9db
4
+ data.tar.gz: 4e5a9a6eb4cf27b54e0d64af4bc8a232755e45a2c875c9e7f16dbf5b3c4d12d5
5
5
  SHA512:
6
- metadata.gz: b52a03445c25fffd9f96d84748c5cb0b242b567f1671403578816b384f4b8e64666bdf2f2f645a336708940e0243b7edc8f80915a151294dc82324fc9af776ba
7
- data.tar.gz: 2e9f447ac870c23cce962c909123aff114fe92024a98282f79fa329499e4aa42a737246fd59d157e9c6e6f026842090cc6a87296360062f1ef3ea43a260a486f
6
+ metadata.gz: 93a65ae988497a89ab9eed80e4724860aa8caa0d0fb7301820b9d36ed85b9931062db60e9cd44468412124848fe7cb0b791524deb7806b3d14d9a6ea0c25b17d
7
+ data.tar.gz: c944cf76b14524c771773b4bc9872bc1d922753a3783917be7b9f8aae7cb527ff1f98b61f28082535d9bc64f49bb019ecc9d9dee4b442c28f186773372fbd7ac
data/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ 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.4.0] - 2026-11-11
11
+ ### Added
12
+ - Support for plain Ruby objects not implementing ActiveModel::Validations (#190)
13
+ - Forward keyword arguments in `render_component` to the component (#238)
14
+ - Support for Ruby 4.0
15
+ ### Changed
16
+ - Make rich_textarea accept block (#231)
17
+ - `form_with` test helper names the form after the object's model, falling back to `:user` (#165)
18
+ ### Fixed
19
+ - Prevent eager loading `test_helpers.rb` from altering `ActionController::Live` behavior (#243)
20
+ - Handle `false` form object in `FieldComponent` (#233)
21
+ - Fix `TestView` test helper with ActionText on Rails 8.2 (#234)
22
+ - Fix `select` with a block (#236)
23
+ - Render the `label` block argument as its translation, like Rails (#240)
24
+ ### Removed
25
+ - Drop Ruby 3.2 support (EOL)
26
+ - Drop Rails 7.2 support (EOL)
27
+ - Drop Rails 8.0 support (EOL)
28
+ - Drop view_component 3 (incompatible with Rails 8.1+)
29
+
30
+ ## [0.3.1] - 2026-08-07
31
+ ### Fixed
32
+ - Detect required association validators for `_id` and `_ids` fields (#229)
33
+ ### Changed
34
+ - Remove upper bound for Rails dependencies (#206)
8
35
 
9
36
  ## [0.3.0] - 2025-10-24
10
37
  ### Added
@@ -145,7 +172,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
145
172
  - Add CHANGELOG (#50)
146
173
  - Add CI (#2)
147
174
 
148
- [Unreleased]: https://github.com/pantographe/view_component-form/compare/v0.2.11...HEAD
175
+ [Unreleased]: https://github.com/pantographe/view_component-form/compare/v0.4.0...HEAD
176
+ [0.4.0]: https://github.com/pantographe/view_component-form/compare/v0.3.1...v0.4.0
177
+ [0.3.1]: https://github.com/pantographe/view_component-form/compare/v0.3.0...v0.3.1
178
+ [0.3.0]: https://github.com/pantographe/view_component-form/compare/v0.2.11...v0.3.0
149
179
  [0.2.11]: https://github.com/pantographe/view_component-form/compare/v0.2.10...v0.2.11
150
180
  [0.2.10]: https://github.com/pantographe/view_component-form/compare/v0.2.9...v0.2.10
151
181
  [0.2.9]: https://github.com/pantographe/view_component-form/compare/v0.2.8...v0.2.9
data/README.md CHANGED
@@ -13,8 +13,8 @@ Development of this gem is sponsored by:
13
13
 
14
14
  This gem is tested on:
15
15
 
16
- - Rails 7.2+ (with or without ActionText)
17
- - Ruby 3.2+
16
+ - Rails 8.1+ (with or without ActionText)
17
+ - Ruby 3.3+
18
18
 
19
19
  ## Installation
20
20
 
@@ -168,6 +168,28 @@ class Form::YearFieldComponent < ViewComponent::Form::FieldComponent # or ViewCo
168
168
  end
169
169
  ```
170
170
 
171
+ `render_component` also passes keyword arguments on to your component:
172
+
173
+ ```rb
174
+ # app/helpers/form_builder.rb
175
+ class FormBuilder < ViewComponent::Form::Builder
176
+ def badge(method, label:, **options)
177
+ render_component(:badge, @object_name, method, label:, **objectify_options(options))
178
+ end
179
+ end
180
+ ```
181
+
182
+ ```rb
183
+ # app/components/form/badge_component.rb
184
+ class Form::BadgeComponent < ViewComponent::Form::FieldComponent
185
+ def initialize(form, object_name, method_name, label:, **options)
186
+ @label = label
187
+
188
+ super(form, object_name, method_name, options)
189
+ end
190
+ end
191
+ ```
192
+
171
193
  When inheriting from `ViewComponent::Form::FieldComponent`, you get access to the following helpers:
172
194
 
173
195
  #### `#label_text`
@@ -352,6 +374,34 @@ You'll most likely want to use either:
352
374
 
353
375
  [`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.
354
376
 
377
+ ### Using a plain Ruby object as model
378
+
379
+ The form object doesn't have to implement the ActiveModel API: a plain Ruby object works too. It has no errors or validators, so its fields are never shown with errors nor marked as required.
380
+
381
+ As Rails can't infer the param key and the route from such an object, pass `scope:` and `url:` to `form_with`.
382
+
383
+ ```rb
384
+ # app/models/post.rb
385
+ class Post
386
+ attr_accessor :title, :body
387
+
388
+ def initialize(title:, body:)
389
+ @title = title
390
+ @body = body
391
+ end
392
+ end
393
+ ```
394
+
395
+ ```erb
396
+ <%# app/views/posts/_form.html.erb %>
397
+ <%= form_with model: @post, scope: :post, url: posts_path, builder: ViewComponent::Form::Builder do |f| %>
398
+ <%= f.label :title %>
399
+ <%= f.text_field :title %>
400
+ <%= f.label :body %>
401
+ <%= f.text_field :body %>
402
+ <% end %>
403
+ ```
404
+
355
405
  ## Supported helpers
356
406
 
357
407
  The following helpers are currently supported by `ViewComponent::Form`.
@@ -411,6 +461,38 @@ end
411
461
 
412
462
  For more complex components, we recommend the [`rspec-html-matchers` gem](https://github.com/kucaahbe/rspec-html-matchers).
413
463
 
464
+ ### Minitest
465
+
466
+ #### Configuration
467
+
468
+ ```rb
469
+ # test/test_helper.rb
470
+ require "view_component/test_case"
471
+ require "view_component/form/test_helpers"
472
+
473
+ ViewComponent::TestCase.include(ViewComponent::Form::TestHelpers)
474
+ ```
475
+
476
+ #### Example
477
+
478
+ ```rb
479
+ # test/components/form/text_field_component_test.rb
480
+ class Form::TextFieldComponentTest < ViewComponent::TestCase
481
+ test "renders a text field" do
482
+ object = User.new # replace with a model of your choice
483
+ form = form_with(object)
484
+
485
+ render_inline(Form::TextFieldComponent.new(form, object_name, :first_name, {}))
486
+
487
+ assert_selector "input[type=text][name='user[first_name]'][id='user_first_name']"
488
+ end
489
+ end
490
+ ```
491
+
492
+ ### Object name
493
+
494
+ `form_with` names the form after the object's model (e.g. `user` for a `User`), like Rails' `form_with(model:)`. For objects that aren't models, it falls back to `object_name`, which returns `:user`. With another model than `User`, pass `form.object_name` to your components instead of `object_name`.
495
+
414
496
  ## Development
415
497
 
416
498
  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.
@@ -22,13 +22,13 @@ module ViewComponent
22
22
  end
23
23
 
24
24
  def object_errors?
25
- return false unless object
25
+ return false unless object.respond_to?(:errors)
26
26
 
27
27
  object.errors.any?
28
28
  end
29
29
 
30
30
  def object_errors
31
- return nil unless object
31
+ return nil unless object.respond_to?(:errors)
32
32
 
33
33
  object.errors
34
34
  end
@@ -39,6 +39,8 @@ module ViewComponent
39
39
  end
40
40
 
41
41
  def value
42
+ return unless object
43
+
42
44
  object.public_send(method_name)
43
45
  end
44
46
 
@@ -63,13 +65,13 @@ module ViewComponent
63
65
  end
64
66
 
65
67
  def optional?(context: validation_context)
66
- return false if object.nil?
68
+ return false unless object
67
69
 
68
70
  !required?(context: context)
69
71
  end
70
72
 
71
73
  def required?(context: validation_context)
72
- return false if object.nil?
74
+ return false unless object
73
75
 
74
76
  validators(context: context).any?(ActiveModel::Validations::PresenceValidator)
75
77
  end
@@ -95,10 +97,10 @@ module ViewComponent
95
97
  end
96
98
 
97
99
  def method_validators
98
- @method_validators ||= if object.nil?
99
- []
100
+ @method_validators ||= if object.class.respond_to?(:validators_on)
101
+ object.class.validators_on(*object_method_names)
100
102
  else
101
- object.class.validators_on(method_name)
103
+ []
102
104
  end
103
105
  end
104
106
  end
@@ -37,6 +37,11 @@ module ViewComponent
37
37
  end
38
38
  end
39
39
  delegate :translation, to: :builder
40
+
41
+ # The component is the block argument, so make it render like Rails' LabelBuilder
42
+ def to_s
43
+ translation
44
+ end
40
45
  end
41
46
  end
42
47
  end
@@ -3,8 +3,13 @@
3
3
  module ViewComponent
4
4
  module Form
5
5
  class RichTextAreaComponent < FieldComponent
6
- if defined?(ActionView::Helpers::Tags::ActionText) # rubocop:disable Style/IfUnlessModifier
7
- self.tag_klass = ActionView::Helpers::Tags::ActionText
6
+ if defined?(ActionView::Helpers::Tags::ActionText)
7
+ def call
8
+ options[:value] = content if content
9
+
10
+ ActionView::Helpers::Tags::ActionText.new(object_name, method_name, @view_context, options)
11
+ .render
12
+ end
8
13
  end
9
14
  end
10
15
  end
@@ -15,6 +15,9 @@ module ViewComponent
15
15
  end
16
16
 
17
17
  def call
18
+ # Tags::Select uses its block (if any) as option tags, in place of choices
19
+ options_block = proc { content } if content
20
+
18
21
  ActionView::Helpers::Tags::Select.new(
19
22
  object_name,
20
23
  method_name,
@@ -22,7 +25,7 @@ module ViewComponent
22
25
  choices,
23
26
  options,
24
27
  html_options,
25
- &content
28
+ &options_block
26
29
  ).render
27
30
  end
28
31
 
@@ -6,10 +6,7 @@ module ViewComponent
6
6
  include ViewComponent::Form::Renderer
7
7
  include ViewComponent::Form::ValidationContext
8
8
  include ViewComponent::Form::Helpers::Rails
9
-
10
- if Gem::Version.new(::Rails::VERSION::STRING) >= Gem::Version.new("8.0")
11
- include ViewComponent::Form::Helpers::Rails8
12
- end
9
+ include ViewComponent::Form::Helpers::Rails8
13
10
  include ViewComponent::Form::Helpers::Custom
14
11
  end
15
12
  end
@@ -3,9 +3,9 @@
3
3
  module ViewComponent
4
4
  module Form
5
5
  module Helpers
6
- # rubocop:disable Metrics/ModuleLength
6
+ # rubocop:disable-next Metrics/ModuleLength
7
7
  module Rails
8
- # rubocop:disable Metrics/MethodLength
8
+ # rubocop:disable-next Metrics/MethodLength
9
9
  def self.included(base)
10
10
  base.class_eval do # rubocop:disable Metrics/BlockLength
11
11
  (field_helpers - %i[
@@ -38,7 +38,6 @@ module ViewComponent
38
38
  alias_method :phone_field, :telephone_field
39
39
  end
40
40
  end
41
- # rubocop:enable Metrics/MethodLength
42
41
 
43
42
  def text_area(method, options = {})
44
43
  render_component(:text_area, @object_name, method, objectify_options(options))
@@ -172,7 +171,6 @@ module ViewComponent
172
171
  end
173
172
  end
174
173
  end
175
- # rubocop:enable Metrics/ModuleLength
176
174
  end
177
175
  end
178
176
  end
@@ -25,8 +25,8 @@ module ViewComponent
25
25
  end
26
26
 
27
27
  if defined?(ActionView::Helpers::Tags::ActionText)
28
- def rich_textarea(method, options = {})
29
- render_component(:rich_text_area, @object_name, method, objectify_options(options))
28
+ def rich_textarea(method, options = {}, &)
29
+ render_component(:rich_text_area, @object_name, method, objectify_options(options), &)
30
30
  end
31
31
  end
32
32
  end
@@ -7,7 +7,7 @@ module ViewComponent
7
7
  class NamespaceAlreadyAddedError < Error; end
8
8
  class NotImplementedComponentError < Error; end
9
9
 
10
- # rubocop:disable Metrics/MethodLength
10
+ # rubocop:disable-next Metrics/MethodLength
11
11
  def self.included(base)
12
12
  base.class_eval do
13
13
  original_initialize_method = instance_method(:initialize)
@@ -37,12 +37,11 @@ module ViewComponent
37
37
  end
38
38
  end
39
39
  end
40
- # rubocop:enable Metrics/MethodLength
41
40
 
42
41
  private
43
42
 
44
- def render_component(component_name, *, &)
45
- component = component_klass(component_name).new(self, *)
43
+ def render_component(component_name, *, **, &)
44
+ component = component_klass(component_name).new(self, *, **)
46
45
  component.render_in(@template, &)
47
46
  end
48
47
 
@@ -5,14 +5,20 @@ require "action_controller/test_case"
5
5
  require "action_view"
6
6
 
7
7
  class TestView < ActionView::Base
8
- include ActionText::TagHelper if defined?(ActionText)
8
+ if defined?(ActionText)
9
+ include ActionText::TagHelper
10
+ # Rails 8.2+ `rich_textarea_tag` calls `main_app`; requires the Rails app to be initialized
11
+ include Rails.application.routes.mounted_helpers
12
+ end
9
13
  end
10
14
 
11
- module ViewComponent
15
+ module ViewComponent # rubocop:disable Style/OneClassPerFile
12
16
  module Form
13
17
  module TestHelpers
14
18
  def form_with(object, builder: ViewComponent::Form::Builder, **options)
15
- builder.new(object_name, object, template, options)
19
+ name = object.class.respond_to?(:model_name) ? object.class.model_name.param_key : object_name
20
+
21
+ builder.new(name, object, template, options)
16
22
  end
17
23
 
18
24
  def object_name
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ViewComponent
4
4
  module Form
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -21,6 +21,7 @@ loader = Zeitwerk::Loader.for_gem
21
21
  form = "#{__dir__}/form.rb"
22
22
  loader.ignore(form)
23
23
  loader.push_dir("#{__dir__}/form", namespace: ViewComponent::Form)
24
+ loader.do_not_eager_load("#{__dir__}/form/test_helpers.rb")
24
25
  loader.setup
25
26
 
26
27
  require_relative "form/engine"
metadata CHANGED
@@ -1,10 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component-form
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pantographe
8
+ - Hans Lemuet
8
9
  bindir: bin
9
10
  cert_chain: []
10
11
  date: 1980-01-02 00:00:00.000000000 Z
@@ -15,47 +16,35 @@ dependencies:
15
16
  requirements:
16
17
  - - ">="
17
18
  - !ruby/object:Gem::Version
18
- version: 7.2.0
19
- - - "<"
20
- - !ruby/object:Gem::Version
21
- version: '8.2'
19
+ version: 8.1.0
22
20
  type: :runtime
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
23
  requirements:
26
24
  - - ">="
27
25
  - !ruby/object:Gem::Version
28
- version: 7.2.0
29
- - - "<"
30
- - !ruby/object:Gem::Version
31
- version: '8.2'
26
+ version: 8.1.0
32
27
  - !ruby/object:Gem::Dependency
33
28
  name: activesupport
34
29
  requirement: !ruby/object:Gem::Requirement
35
30
  requirements:
36
31
  - - ">="
37
32
  - !ruby/object:Gem::Version
38
- version: 7.2.0
39
- - - "<"
40
- - !ruby/object:Gem::Version
41
- version: '8.2'
33
+ version: 8.1.0
42
34
  type: :runtime
43
35
  prerelease: false
44
36
  version_requirements: !ruby/object:Gem::Requirement
45
37
  requirements:
46
38
  - - ">="
47
39
  - !ruby/object:Gem::Version
48
- version: 7.2.0
49
- - - "<"
50
- - !ruby/object:Gem::Version
51
- version: '8.2'
40
+ version: 8.1.0
52
41
  - !ruby/object:Gem::Dependency
53
42
  name: view_component
54
43
  requirement: !ruby/object:Gem::Requirement
55
44
  requirements:
56
45
  - - ">="
57
46
  - !ruby/object:Gem::Version
58
- version: 2.34.0
47
+ version: 4.0.0
59
48
  - - "<"
60
49
  - !ruby/object:Gem::Version
61
50
  version: '5.0'
@@ -65,7 +54,7 @@ dependencies:
65
54
  requirements:
66
55
  - - ">="
67
56
  - !ruby/object:Gem::Version
68
- version: 2.34.0
57
+ version: 4.0.0
69
58
  - - "<"
70
59
  - !ruby/object:Gem::Version
71
60
  version: '5.0'
@@ -159,14 +148,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
159
148
  requirements:
160
149
  - - ">="
161
150
  - !ruby/object:Gem::Version
162
- version: 3.2.0
151
+ version: 3.3.0
163
152
  required_rubygems_version: !ruby/object:Gem::Requirement
164
153
  requirements:
165
154
  - - ">="
166
155
  - !ruby/object:Gem::Version
167
156
  version: '0'
168
157
  requirements: []
169
- rubygems_version: 3.6.9
158
+ rubygems_version: 4.0.20
170
159
  specification_version: 4
171
160
  summary: Rails FormBuilder for ViewComponent
172
161
  test_files: []