form_props¶
form_props is a Rails form builder that outputs HTML props instead of tags. Now you can enjoy the power and convenience of Rails helpers in React!
By separating attributes from tags, form_props can offer greater flexibility than normal Rails form builders; allowing designers to stay longer in HTML land and more easily customize their form structure without needing to know Rails.
Caution¶
This project is in its early phases of development. Its interface, behavior, and name are likely to change drastically before a major version release.
Installation¶
Add to your Gemfile
and bundle install
Usage¶
form_props
is designed to be used in a PropsTemplate template (it can work with
jbuilder). For example in your new.json.props
:
would output
{
someForm: {
props: {
id: "create-post",
action: "/posts/123",
acceptCharset: "UTF-8",
method: "post"
},
extras: {
method: {
name: "_method",
type: "hidden",
defaultValue: "patch",
autoComplete: "off"
},
utf8: {
name: "utf8",
type: "hidden",
defaultValue: "\u0026#x2713;",
autoComplete: "off"
}
csrf: {
name: "utf8",
type: "authenticity_token",
defaultValue: "SomeTOken!23$",
autoComplete: "off"
}
},
inputs: {
title: {name: "post[title]", id: "post_title", type: "text", defaultValue: "hello"},
submit: {type: "submit", value: "Update a Post"}
}
}
}
You can then proceed to use this output in React like so:
import React from 'react'
export default ({props, inputs, extras}) => {
<form {...props}>
{Object.values(extras).map((hiddenProps) => (<input {...hiddenProps} key={hiddenProps.name}/>))}
<input {...inputs.title} />
<label for={inputs.title.id}>Your Name</label>
<button {...inputs.submit}>{inputs.submit.text}</button>
</form>
}
Key format¶
By default, props_template automatically camelize(:lower)
on all keys. All
documentation here reflects that default. You can change that behavior
if you wish.
Flexibility¶
form_props is only concerned about attributes, the designer can focus on tag structure and stay longer in HTML land. For example, you can decide to nest an input inside a label.
or not
Custom Components¶
With form_props
you can combine the comprehensiveness of Rails forms with
your preferred React components:
For example:
Then use it the props your own components or a external component like
react-select
:
import React from 'react'
import Select from 'react-select';
export default (({props, inputs, extras})) => {
return (
<form {...props}>
<Select
{...inputs.timeZone}
isMulti={inputs.timeZone.multiple}
/>
</form>
)
}
Error handling¶
form_props doesn't handle form errors, but you can easily add this functionality:
json.someForm do
form_props(model: @post) do |f|
f.text_field :title
end
json.errors @post.errors.to_hash(true)
end
then merge it later
form_props¶
form_props
shares most of same arguments as form_with. The differences are
remote
andlocal
options are removed.- You can change the name of the value keys generated by the form helpers
from
defaultValue
tovalue
, by usingcontrolled: true
. For example:
By default, the controlled
option is false
.
¶
props
Attributes that you can splat directly into your <form>
element.
extras
contain hidden input attributes that are created by form_props
indirectly, for example, the csrf
token. Its best to wrap this in a custom
component that does the following. An Extra component is available
Form Helpers¶
form_props
provides its own version of the following Rails form helpers:
check_box file_field submit
collection_check_boxes grouped_collection_select tel_field
collection_helpers hidden_field text_area
collection_radio_buttons month_field text_field
collection_select number_field time_field
color_field password_field time_zone_select
date_field radio_button url_field
datetime_field range_field week_field
datetime_local_field search_field weekday_select
email_field select
form_props
is a fork of form_with
, and the accompanying form builder
inherits from ActionView::Helpers::FormBuilder
.
Many of the helpers accept the same arguments and you can continue to rely on
[Rails Guides for form helpers] for guidance, but as the goal of form_props
is to focus on attributes instead of tags there are a few general differences
across all helpers that would be beneficial to know:
- The form helper
f.label
does not exist. Helpers like the below thatyield
for label structure
f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b|
b.label { b.radio_button + b.text }
end
no longer takes in blocks to do so.
defaultValue
s are not escaped. Instead, we lean on PropsTemplate to escape JSON and HTML entities.defaultValue
will not appear as a key if novalue
was set.data-disable-with
is removed on submit buttons.data-remote
is removed from form props.- For helpers that selectively render hidden inputs, we pass the attribute to
f.select
helpers do not renderselected
onoptions
, instead they follow React caveats and render on the input'svalue
. For example:
{
"type": "select",
"name": "continent[countries]",
"id": "continent_countries",
"multiple": true,
"defaultValue": ["Africa", "Europe"],
"options": [
{"value": "Africa", "label": "Africa"},
{"value": "Europe", "label": "Europe"},
{"value": "America", "label": "America", "disabled": true}
]
}
Unsupported helpers¶
form_props
does not support:
label
. We encourage you to use the tag directly in combination with other
helpers. For example:
rich_text_area
. We encourage you to use the f.text_area
helper in
combination with Trix wrapped in React, or TinyMCE's react component.
button
. We encourage you to use the tag directly.
date_select
, time_select
, datetime_select
. We encourage you to use other
alternatives like react-date-picker
in combination with other supported date
field helpers.
Text helpers¶
text_field, email_field, tel_field, file_field, url_field, hidden_field, and the slight variations password_field, search_field, color_field has the same arguments as their Rails counterpart.
When used like so
inputs.title
would output
Date helpers¶
date_field, datetime_field, datetime_local_field, month_field, week_field has the same arguments as their Rails counterparts.
When used like so
inputs.created_at
would output
{
"type": "datetime-local",
"defaultValue": "2004-06-15T01:02:03",
"name": "post[created_at]",
"id": "post_created_at"
}
Number helpers¶
number_field, range_field has the same arguments as their Rails counterparts.
When used like so
inputs.favs
would output
{
"type": "range",
"defaultValue": "2",
"name": "post[favs]",
"min": 1,
"max": 9,
"id": "post_favs"
}
Checkbox helper¶
check_box has the same arguments as its Rails counterpart.
The original Rails check_box
helper renders an unchecked value in a
hidden input. While form_props
doesn't generate the tags, the
unchecked_value
, and include_hidden
can be passed to a React component
to replicate that behavior. This repository has an example CheckBox
component used in its test that you can refer to.
When used like so:
inputs.admin
would output
{
"type": "checkbox",
"defaultValue": "on",
"uncheckedValue": "off",
"name": "post[admin]",
"id": "post_admin",
"includeHidden": true
}
Radio helper¶
radio_button has the same arguments as its Rails counterpart.
When used like so:
@post.admin = false
form_props(model: @post) do |f|
f.radio_button(:admin, true)
f.radio_button(:admin, false)
end
The keys on inputs
are a combination of the name and value. So inputs.adminTrue
would output:
and inputs.adminFalse
would output
{
"type": "radio",
"defaultValue": "false",
"name": "post[admin]",
"id": "post_admin_false",
"checked": true
}
Select helpers¶
select, weekday_select, [time_zone_select] mostly have the same arguments as their Rails counterparts. The key difference is that choices for select cannot be a string:
# BAD!!!
form_props(model: @post) do |f|
f.select(:category, "<option><option/>", multiple: false)
end
# Good
form_props(model: @post) do |f|
f.select(:category, [], multiple: false)
end
When used like so
@post.category = "lifestyle"
form_props(model: @post) do |f|
f.select(:category, ["lifestyle", "programming", "spiritual"], {selected: "", disabled: "", prompt: "Choose one"}, {required: true})
end
inputs.category
would output
{
"type": "select",
"required": true,
"name": "post[category]",
"id": "post_category",
"defaultValue":"lifestyle",
"options": [
{"disabled": true, "value": "", "label": "Choose one"},
{"value": "lifestyle", "label": "lifestyle"},
{"value": "programming", "label": "programming"},
{"value": "spiritual", "label": "spiritual"}
]
}
Of note:
1. Notice that we follow react caveats and put selected
values on defaultValue
. This rule
does not apply to the disabled
attribute on option.
2. When multiple: true
, defaultValue
is an array of values.
3. The key, defaultValue
is only set if the value is in options. For example:
would output in inputs.category
:
As the select
helper renders nested options and includeHidden
, a custom
component is required to correctly render the tag structure. A reference
Select component implementation is availble that is used in our tests.
The select
helper can also output a grouped collection.
@post = Post.new
countries_by_continent = [
["<Africa>", [["<South Africa>", "<sa>"], ["Somalia", "so"]]],
["Europe", [["Denmark", "dk"], ["Ireland", "ie"]]]
]
form_props(model: @post) do |f|
f.select(:category, countries_by_continent)
end
inputs.category
would output:
{
"type": "select",
"name": "post[category]",
"id": "post_category",
"options": [
{
"label": "<Africa>", "options": [
{"value": "<sa>", "label": "<South Africa>"},
{"value": "so", "label": "Somalia"}
]
},
{
"label": "Europe", "options": [
{"value": "dk", "label": "Denmark"},
{"value": "ie", "label": "Ireland"}
]
}
]
}
Group collection select¶
group_collection_select has the same arguments as its Rails counterpart.
Like select
, you'll need to combine this with a custom Select
component. An
example Select component is available.
When used like so:
@post = Post.new
@post.country = "dk"
label_proc = proc { |c| c.id }
continents = [
Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")]),
Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")])
]
form_props(model: @post) do |f|
f.grouped_collection_select(
:country, continents, "countries", label_proc, "country_id", "country_name"
)
end
inputs.country
would output
{
"name": "post[country]",
"id": "post_country",
"type": "select",
"defaultValue": "dk",
"options": [
{
"label":"<Africa>",
"options": [
{"value": "<sa>", "label": "<South Africa>"},
{"value": "so", "label": "Somalia"}
]
}, {
"label": "Europe",
"options": [
{"value": "dk", "label": "Denmark"},
{"value":"ie", "label": "Ireland"}
]
}
]
}
Collection select¶
collection_select, collection_radio_buttons, and collection_check_boxes have the same arguments as their Rails counterparts, but their output differs slightly.
collection_select follows the same output as f.select
. When used like so:
dummy_posts = [
Post.new(1, "<Abe> went home", "<Abe>", "To a little house", "shh!"),
Post.new(2, "Babe went home", "Babe", "To a little house", "shh!"),
Post.new(3, "Cabe went home", "Cabe", "To a little house", "shh!")
]
form_props(model: @post) do |f|
f.collection_select(:author_name, dummy_posts, "author_name", "author_name")
end
inputs.authorName
would output:
{
"type": "select",
"name": "post[author_name]",
"id": "post_author_name",
"defaultValue": "Babe",
"options": [
{"value": "<Abe>", "label": "<Abe>"},
{"value": "Babe", "label": "Babe"},
{"value": "Cabe", "label": "Cabe"}
]
}
collection_radio_buttons and collection_check_boxes usage is the same with their rails counterpart, and when used, would render:
{
"collection": [
{"name":"user[other_category_ids][]","type": "checkbox", "defaultValue": "1", "uncheckedValue":"","id":"user_category_ids_1","label": "Category 1"},
{"name":"user[other_category_ids][]","type": "checkbox", "defaultValue": "2", "uncheckedValue":"","id":"user_category_ids_2","label": "Category 2"}
],
"name": "user[other_category_ids][]",
"includeHidden": true
}
Like select, you would need a custom component to render. An example implementation for CollectionCheckBoxes and CollectionRadioButtons are available.
jbuilder¶
form_props can work with jbuilder, but needs an extra call in the beginning of
your template to FormProps.set
to inject json
. For example.
FormProps.set(json, self)
json.data do
json.hello "world"
json.form do
form_props(model: User.new, url: "/") do |f|
f.text_field(:email)
f.submit
end
end
end
Special Thanks¶
Thanks to bootstrap_form documentation for inspiration.