Construct <form>
elements and their fields by combining
ActionView::Helpers::FormBuilder
with Rails View
Partials.
First, render a <form>
element with form_with
the necessary
fields:
<%# app/views/users/new.html.erb %>
<%= form_with(model: user) do |form| %>
<%= form.label(:name) %>
<%= form.text_field(:name, class: "text-field", required: true) %>
<%= form.label(:email) %>
<%= form.email_field(:email, class: "text-field text-field--large", required: true) %>
<%= form.label(:password) %>
<%= form.password_field(:email, class: "text-field", required: true) %>
<%= form.button(class: "button button--primary") %>
<% end %>
Next, declare view partials that correspond to the FormBuilder
helper method you'd like to have more control over:
<%# app/views/application/form_builder/_text_field.html.erb %>
<input
type="text"
name="<%= form.object_name %>[<%= method %>]"
class="text-field"
<% options.each do |attribute, value| %>
<%= attribute %>="<%= value %>"
<% end %>
>
<%# app/views/application/form_builder/_email_field.html.erb %>
<input
type="email"
name="<%= form.object_name %>[<%= method %>]"
class="text-field text-field--large"
<% options.each do |attribute, value| %>
<%= attribute %>="<%= value %>"
<% end %>
>
<%# app/views/application/form_builder/_button.html.erb %>
<button
class="button button--primary"
<% options.each do |attribute, value| %>
<%= attribute %>="<%= value %>"
<% end %>
>
<%= value %>
</button>
You'll have local access to the FormBuilder
instance as the template-local
form
variable. You can mix and match between declaring HTML elements, and
generating HTML through Rails' helpers:
<%# app/views/application/form_builder/_email_field.html.erb %>
<div class="email-field-wrapper">
<%= form.email_field(method, required: true, **options)) %>
</div>
<%# app/views/application/form_builder/_button.html.erb %>
<div class="button-wrapper">
<%= form.button(value, options, &block) %>
</div>
Templates with calls to FormBuilder#fields
and
FormBuilder::fields_for
will yield instances of
ViewPartialFormBuilder
as block arguments.
With the exception of fields
and fields_for
, view partials for all other
FormBuilder
field methods can be declared.
When a partial for a helper method is not declared, ViewPartialFormBuilder
will fall back to the default helper method's behavior.
Every view partial has access to the arguments it was invoked with. For example,
the FormBuilder#button
accepts two arguments: method
and value
.
Arguments are made available as partial-local variables (along with key-value
pairs in the local_assigns
).
In addition, each view partial receives:
-
form
- a reference to the instance ofViewPartialFormBuilder
, which is a descendant ofActionView::Helpers::FormBuilder
-
block
- the block if the helper method was passed one. Forward it along to field helpers as&block
.
An HTML element's class
attribute is treated by browsers as a
DOMTokenList
:
set of space-separated tokens. Such a set is returned by
Element.classList
, ...HTMLAnchorElement.relList
...It is indexed beginning with
0
as with JavaScript Array objects.DOMTokenList
is always case-sensitive.
When rendering a field's DOMTokenList-backed attributes (like class
or
"data-controller"
when specifying StimulusJS
controllers), transforming and combining singular String
instances into lists of token can be very useful.
These optional attributes are available through the options
or html_options
partial-local variables. Their name will depend on the partial's corresponding
ActionView::Helpers::FormBuilder
interface.
To "merge" attributes together, you can combine Ruby's String
interpolation
and Hash#delete
:
<%# app/views/users/new.html.erb %>
<%= form_with(model: post) do |form| %>
<%= form.text_field(:name, class: "text-field--modifier") %>
<% end %>
<# app/views/application/form_builder/_text_field.html.erb %>
<%= form.text_field(
method,
class: "text-field #{options.delete(:class)}",
**options
) %>
The resulting HTML <input>
element will merge have its class
attribute set to a list containing both sets of ERB-side class:
values:
<input type="text" name="post[name]" class="text-field text-field--modifier">
The fields' view partial files behave like any other: their contents will be used to populate the original call-site.
To opt-out of view partial rendering for a field, first call #default
on the
block-local form
variable:
<%# app/views/users/form_builder/_email_field.html.erb %>
<%= form.default.email_field(method, options) %>
When passing a model:
or scope:
to calls to form_with
,
a pluralized version of the FormBuilder's object name will be prepended to the
look up path.
For example, when calling form_with(model: User.new)
, a partial declared in
app/views/users/form_builder/
would take precedent over a partial declared in
app/views/application/form_builder/
.
<%# app/views/users/form_builder/_password_field.html.erb %>
<div class="password-field-wrapper">
<%= form.password_field(method, options) %>
</div>
If you'd like to render a specific partial for a field, make sure that you pass
along the form:
(along with any other partial-local variables) as part of the
render
call's locals:
option:
<%# app/views/users/new.html.erb %>
<%= form_with(model: User.new) do |form| %>
<%= render("emails/my_special_email_field", {
form: form,
method: :email,
options: { class: "user-email" },
) %>
<% end %>
<%# app/views/emails/_my_special_email_field.html.erb %>
<%= form.email_field(
method,
class: "my-special-email #{options.delete(:class)},
**options
) %>
Layering partials on top of one another can be useful to share foundational styles and configuration across your fields. For instance, consider an administrative interface that shares styles with a consumer facing site, but has additional bells and whistles.
Declare the consumer facing inputs (in this example, <input type="search">
):
<%# app/views/application/form_builder/_search_field.html.erb %>
<%= form.search_field(
method,
class: "
search-field
#{options.delete(:class)}
",
"data-controller": "
input->search#executeQuery
#{options.delete(:"data-controller")}
",
**options
) %>
Then, declare the administrative interface's inputs, in terms of overriding the foundation built by the more general definitions:
<%# app/views/admin/application/form_builder/_search_field.html.erb %>
<%= form.search_field(
method,
class: "
search-field--admin
#{options.delete(:class}
",
"data-controller": "
focus->admin-search#clearResults
#{options.delete(:"data-controller")}
",
) %>
The rendered admin/application/form_builder/search_field
partial combines
options and arguments from both partials:
<input
type="search"
class="
search-field
search-field--admin
"
data-controller="
input->search#executeQuery
focus->admin-search#clearResults
"
>
When constructing fields within a form_with(model: ...)
block, partials will
use the model:
instance's tableize
-d model name to resolve
partials.
For example, posts/form_builder/_text_field.html.erb
will be resolved ahead of
form_builder/_text_field.html.erb
:
<%# app/views/posts/form_builder/_text_field.html.erb %>
<%= form.text_field(method, class: "post-text #{options.delete(:class)}", **options) %>
<%# app/views/application/form_builder/_text_field.html.erb %>
<%= form.text_field(method, class: "text #{options.delete(:class)}", **options) %>
The rendered posts/form_builder/text_field
partial could combine options and
arguments from both partials:
<input type="text" class="post-text text">
Models declared within modules will be delimited with /
. For example,
Special::Post
instances would first resolve partials within the
app/views/special/posts/form_builder
directory, before falling back to
app/views/application/form_builder
.
View partials lookup and resolution will be scoped to the
app/views/application/form_builder
directory.
To override this destination to another directory (for example,
app/views/fields
, or app/views/users/fields
), set
ViewPartialFormBuilder.view_partial_directory
:
# config/initializers/view_partial_form_builder.rb
ViewPartialFormBuilder.view_partial_directory = "fields"
Add this line to your application's Gemfile:
gem 'view_partial_form_builder'
And then execute:
$ bundle
See CONTRIBUTING.md.
The gem is available as open source under the terms of the MIT License.