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 separting attributes from tags, form_props can offer greater flexbility 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
gem "form_props"
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
:
json.some_form do
form_props(@post) do |f|
f.text_field :title
f.submit
end
end
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.
<label for={inputs.name.id}>
Your Name
<input {...inputs.name} type="text"/>
</label>
or not
<label for={inputs.name.id}>Your Name</label>
<input {...inputs.name} />
Custom Components
With form_props
you can combine the comprehensiveness of Rails forms with
your prefered React components:
For example:
json.some_form do
form_props(@post) do |f|
f.time_zone_select(:time_zone)
...
end
end
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(@post) do |f|
f.text_field :title
end
json.errors @post.errors.to_hash(true)
end
then merge it later
<MyTextComponent {...someForm.inputs.title, error: ...someForm.errors.title}>
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:
json.some_form do
form_props(@post, controlled: true) do |f|
f.text_field :title
end
end
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
Object.values(extras).map((hiddenProps) => (<input {...hiddenProps} type="hidden"/>))}
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 beneficial to know:
- The form helper
f.label
do not exist. Helpers like the below thatyield
s 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 selectively render hidden inputs, we passed the attribute to
-
f.select
helpers does not renderselected
onoptions
, instead it follows react caveats and renders 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:
<label for={inputs.name.id} />
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
form_props(model: @post) do |f|
f.text_field(:title)
end
inputs.title
would output
{
"type": "text",
"defaultValue": "Hello World",
"name": "post[title]",
"id": "post_title"
}
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
form_props(model: @post) do |f|
f.datetime_field(:created_at)
end
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
@post.favs = 2
form_props(model: @post) do |f|
f.range_field(:favs, in: 1...10)
end
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 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:
@post.admin = "on"
form_props(model: @post) do |f|
f.check_box(:admin, {}, "on", "off")
end
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. The radio button is unique
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:
{
"type": "radio",
"defaultValue": "true",
"name": "post[admin]",
"id": "post_admin_true"
}
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 has the same arguments as its Rails counterpart. They 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:
- Notice that we follow react caveats and put
selected
values ondefaultValue
. This rule does not apply to thedisabled
attribute on option. - When
multiple: true
,defaultValue
is an array of values. - The key,
defaultValue
is only set if the value is in options. For example:
form_props(model: @post) do |f|
f.select(:category, [])
end
would output in inputs.category
:
{
"type": "select",
"name": "post[category]",
"id": "post_category",
"options": []
}
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 its Rails counterpart.
Like select
, you'll need 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 has the same arguments its Rails counterpart, 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.