ViewComponent::Form
ViewComponent::Form provides a FormBuilder
with the same interface as ActionView::Helpers::FormBuilder
, but using ViewComponents for rendering the fields. It's a starting point for writing your own custom ViewComponents.
v1.0.0
.
Development of this gem is sponsored by:
Compatibility
This gem is tested on:
- Rails 6.0+ (with or without ActionText)
- Ruby 2.7+
Installation
Add this line to your application's Gemfile:
gem 'view_component-form'
And then execute:
$ bundle install
Usage
Add a builder
param to your form_for
, form_with
, fields_for
or fields
:
- <%= form_for @user do |f| %>
+ <%= form_for @user, builder: ViewComponent::Form::Builder do |f| %>
You can also define a default FormBuilder at the controller level using default_form_builder.
Then call your helpers as usual:
<%# app/views/users/_form.html.erb %>
<%= form_for @user, builder: ViewComponent::Form::Builder do |f| %>
<%= f.label :first_name %> <%# renders a ViewComponent::Form::LabelComponent %>
<%= f.text_field :first_name %> <%# renders a ViewComponent::Form::TextFieldComponent %>
<%= f.label :last_name %> <%# renders a ViewComponent::Form::LabelComponent %>
<%= f.text_field :last_name %> <%# renders a ViewComponent::Form::TextFieldComponent %>
<%= f.label :email %> <%# renders a ViewComponent::Form::LabelComponent %>
<%= f.email_field :email %> <%# renders a ViewComponent::Form::EmailFieldComponent %>
<%= f.label :password %> <%# renders a ViewComponent::Form::LabelComponent %>
<%= f.password_field :password, aria: { describedby: f.field_id(:password, :description) } %>
<%# renders a ViewComponent::Form::PasswordFieldComponent %>
<div id="<%= f.field_id(:title, :description) %>">
<%= f.hint :password, 'The password should be at least 8 characters long' %>
<%# renders a ViewComponent::Form::HintComponent %>
<%= f.error_message :password %> <%# renders a ViewComponent::Form::ErrorMessageComponent %>
</div>
<% end %>
It should work out of the box, but does nothing particularly interesting for now.
<form class="edit_user" id="edit_user_1" action="/users/1" accept-charset="UTF-8" method="post">
<input type="hidden" name="_method" value="patch" />
<input type="hidden" name="authenticity_token" value="[...]" />
<label for="user_first_name">First name</label>
<input type="text" value="John" name="user[first_name]" id="user_first_name" />
<label for="user_last_name">Last name</label>
<input type="text" value="Doe" name="user[last_name]" id="user_last_name" />
<label for="user_email">E-mail</label>
<input type="email" value="[email protected]" name="user[email]" id="user_email" />
<label for="user_password">Password</label>
<input type="password" name="user[password]" id="user_password" aria-describedby="user_password_description" />
<div id="user_password_description">
<div>The password should be at least 8 characters long</div>
</div>
</form>
The ViewComponent::Form::*
components are included in the gem.
FormBuilder
and the components
Customizing the First, generate your own FormBuilder
:
bin/rails generate vcf:builder CustomFormBuilder
create app/helpers/custom_form_builder.rb
This allows you to pick the namespace your components will be loaded from.
# app/helpers/custom_form_builder.rb
class CustomFormBuilder < ViewComponent::Form::Builder
# Set the namespace you want to use for your own components
namespace "Custom::Form"
end
Use the generator options to change the default namespace or the path where the file will be created:
bin/rails generate vcf:builder AnotherCustomFormBuilder --namespace AnotherCustom::Form --path app/forms
create app/forms/another_custom_form_builder.rb
# app/forms/another_custom_form_builder.rb
class AnotherCustomFormBuilder < ViewComponent::Form::Builder
# Set the namespace you want to use for your own components
namespace "AnotherCustom::Form"
end
Another approach is to include only some modules instead of inheriting from the whole class:
# app/forms/modular_custom_form_builder.rb
class ModularCustomFormBuilder < ActionView::Helpers::FormBuilder
# Provides `render_component` method and namespace management
include ViewComponent::Form::Renderer
# Exposes a `validation_context` to your components
include ViewComponent::Form::ValidationContext
# All standard Rails form helpers
include ViewComponent::Form::Helpers::Rails
# Backports of Rails 7 form helpers (can be removed if you're running Rails >= 7)
# include ViewComponent::Form::Helpers::Rails7Backports
# Additional form helpers provided by ViewComponent::Form
# include ViewComponent::Form::Helpers::Custom
# Set the namespace you want to use for your own components
namespace "AnotherCustom::Form"
end
Now let's generate your own components to customize their rendering. We can use the standard view_component generator:
bin/rails generate component Custom::Form::TextField --inline --parent ViewComponent::Form::TextFieldComponent
invoke test_unit
create test/components/custom/form/text_field_component_test.rb
create app/components/custom/form/text_field_component.rb
--parent
option is available since ViewComponent v2.41.0
. 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
.
Change your forms to use your new builder:
- <%= form_for @user, builder: ViewComponent::Form::Builder do |f| %>
+ <%= form_for @user, builder: CustomFormBuilder do |f| %>
You can then customize the behavior of your Custom::Form::CustomTextFieldComponent
:
# app/components/custom/form/text_field_component.rb
class Admin::Form::TextFieldComponent < ViewComponent::Form::TextFieldComponent
def html_class
class_names("custom-text-field", "has-error": method_errors?)
end
end
In this case we leverage the #class_names
helper to:
- always add the
custom-text-field
class; - add the
has-error
class if there is an error on the attribute (usingViewComponent::Form::FieldComponent#method_errors?
).
The rendered form field will now look like this:
<input class="custom-text-field" type="text" value="John" name="user[first_name]" id="user_first_name">
You can use the same approach to inject options, wrap the input in a <div>
, etc.
We'll add more use cases to the documentation soon.
Building your own components
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:
#label_text
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.
# app/components/custom/form/group_component.rb
class Custom::Form::GroupComponent < ViewComponent::Form::FieldComponent
end
<%# app/components/custom/form/group_component.html.erb %>
<div class="custom-form-group">
<label>
<%= label_text %><br />
<%= content %>
</label>
</div>
<%# app/views/users/_form.html.erb %>
<%= form_for @user do |f| %>
<%= f.group :first_name do %>
<%= f.text_field :first_name %>
<% end %>
<% end %>
# config/locales/en.yml
en:
helpers:
label:
user:
first_name: Your first name
Renders:
<form class="edit_user" id="edit_user_1" action="/users/1" accept-charset="UTF-8" method="post">
<!-- ... -->
<label>
Your first name<br />
<input type="text" value="John" name="user[first_name]" id="user_first_name" />
</label>
</form>
Validations
Let's consider the following model for the examples below.
# app/models/user.rb
class User < ActiveRecord::Base
validates :first_name, presence: true, length: { minimum: 2, maximum: 255 }
end
#validators
Accessing validations with Returns all validators for the method name.
# app/components/custom/form/group_component.rb
class Custom::Form::GroupComponent < ViewComponent::Form::FieldComponent
private
def validation_hint
if length_validator
"between #{length_validator.options[:minimum]} and #{length_validator.options[:maximum]} chars"
end
end
def length_validator
validators.find { |v| v.is_a?(ActiveModel::Validations::LengthValidator) }
end
end
<%# app/components/custom/form/group_component.html.erb %>
<div class="custom-form-group">
<label>
<%= label_text %> (<%= validation_hint %>)<br />
<%= content %>
</label>
</div>
#required?
and #optional?
Using <%# app/components/custom/form/group_component.html.erb %>
<div class="custom-form-group">
<label>
<%= label_text %><%= " (required)" if required? %><br />
<%= content %>
</label>
</div>
Validation contexts
When using validation contexts, you can specify a context to the helpers above.
# app/models/user.rb
class User < ActiveRecord::Base
validates :first_name, presence: true, length: { minimum: 2, maximum: 255 }
validates :email, presence: true, on: :registration
end
<%# app/views/users/_form_.html.erb %>
<%= form_with model: @user,
builder: ViewComponent::Form::Builder,
validation_context: :registration do |f| %>
<%= f.group :email do %>
<%= f.email_field :email %>
<% end %>
<% end %>
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.
Alternatively, you can pass the context to the helpers:
<%= "(required)" if required?(context: :registration) %>
def length_validator
validators(context: :registration).find { |v| v.is_a?(ActiveModel::Validations::LengthValidator) }
end
Using your form components without a backing model
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.
You'll most likely want to use either:
form_with
and supply a route as the endpoint, e.g.form_with url: users_path do |f| ...
, orfields
, supplying a namespace if necessary.fields do |f| ...
ought to work in the most basic case.
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.
Supported helpers
The following helpers are currently supported by ViewComponent::Form
.
ActionView::Helpers::FormBuilder
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
Partially supported: label
(blocks not supported) rich_text_area
(untested)
Unsupported for now: field_name
ViewComponent::Form
Specific to Supported: error_message
hint
Testing your components
RSpec
Configuration
This assumes your already have read and configured tests for view_component
.
# spec/rails_helper.rb
require "view_component/test_helpers"
require "view_component/form/test_helpers"
require "capybara/rspec"
RSpec.configure do |config|
config.include ViewComponent::TestHelpers, type: :component
config.include ViewComponent::Form::TestHelpers, type: :component
config.include Capybara::RSpecMatchers, type: :component
end
Example
# spec/components/form/text_field_component_spec.rb
RSpec.describe Form::TextFieldComponent, type: :component do
let(:object) { User.new } # replace with a model of your choice
let(:form) { form_with(object) }
let(:options) { {} }
let(:component) { render_inline(described_class.new(form, object_name, :first_name, options)) }
context "with simple args" do
it do
expect(component.to_html)
.to have_tag("input", with: { name: "user[first_name]", id: "user_first_name", type: "text" })
end
end
end
For more complex components, we recommend the rspec-html-matchers
gem.
Development
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.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/pantographe/view_component-form. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the ViewComponent::Form project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.