ViewPartialFormBuilder
Construct <form>
elements and their fields by combining
ActionView::Helpers::FormBuilder
with Rails View
Partials.
Usage
Building the Form
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 %>
Declaring the Fields' View Partials
Next, declare view partials that correspond to the FormBuilder
helper method you'd like to have more control over:
<%# app/views/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/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/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/form_builder/_email_field.html.erb %>
<div class="email-field-wrapper">
<%= form.email_field(method, required: true, **options)) %>
</div>
<%# app/views/form_builder/_button.html.erb %>
<div class="button-wrapper">
<%= form.button(*arguments, **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.
Arguments
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
arguments
- an Array containing the arguments the helper received, in the order they were received. This can be useful to pass to the view partial's helper by splatting them out.&block
- a callable,yield
-able block if the helper method was passed one
In cases when a ActionView::Helpers::FormBuilder
helper
method's last arguments are options (either Hash
instances or
keyword arguments), they're omitted from the arguments
array.
If you want to pass-through all arguments, options, and block parameters, you can splat them out:
<%# app/views/form_builder/_label.html.erb %>
<%= form.label(*arguments, **options, &block) %>
<%# app/views/form_builder/_select.html.erb %>
<%= form.select(*arguments, **html_options, &block) %>
Handling DOMTokenList attributes
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.
To simplify those scenarios, a partial's template-local optional attributes are
made available with the #merge_token_lists
method.
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.
Calls to #merge_token_lists
will merge the key-value pairs and return a new
Hash-like structure. The attribute's value will be transformed into an Array
.
Given call to form.text_field
and a corresponding partial declaration:
<%# app/views/users/new.html.erb %>
<%= form_with(model: post) do |form| %>
<%= form.text_field(:name, class: "text-field--modifier") %>
<% end %>
<# app/views/form_builder/_text_field.html.erb %>
<%= form.text_field(*arguments, **options.merge_token_lists(class: "text-field")) %>
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">
Rendering the Fields
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(*arguments, **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/form_builder/
.
<%# app/views/users/form_builder/_password_field.html.erb %>
<div class="password-field-wrapper">
<%= form.password_field(*arguments, **options) %>
</div>
If you'd like to render a specific partial for a field, you can declare the name
as the partial:
option:
<%# app/views/users/new.html.erb %>
<%= form_with(model: User.new) do |form| %>
<%= form.email_field(:email, partial: "emails/my_special_text_field") %>
<% end %>
Composing partials
Passing a partial:
key can be useful for layering partials on top of one
another. 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/form_builder/_search_field.html.erb %>
<%= form.search_field(
*arguments,
**options.merge_token_lists(
class: "search-field",
"data-controller": "input->search#executeQuery",
),
) %>
Then, declare the administrative interface's inputs, in terms of overriding the foundation built by the more general definitions:
<%# app/views/admin/form_builder/_search_field.html.erb %>
<%= form.search_field(
*arguments,
partial: "form_builder/search_field",
**options.merge_token_lists(
class: "search-field--admin",
"data-controller": "focus->admin-search#clearResults",
),
) %>
The rendered admin/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
"
>
Configuration
View partials lookup and resolution will be scoped to the
app/views/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"
Installation
Add this line to your application's Gemfile:
gem 'view_partial_form_builder'
And then execute:
$ bundle
Contributing
See CONTRIBUTING.md.
License
The gem is available as open source under the terms of the MIT License.