React Form Builder
Several server side frameworks are designed to handle form data if the inputs are named in a certain way. For e.g., in Rails, if you have an input named user[name]
, and is submitted as part of a form, then the server would parse and convert it to a Hash
as such: { "user" => { "name" => <value> } }
. This allows the server to access the input value as: params["user"]["name"]
, where params
is the variable through which the parsed Hash
is accessible.
Don't worry if you don't understand Rails; the point is that the convention of square brackets inside an input's name allows the server to store the value in a nice data structure which is then easier to work with. The only problem is that it can get cumbersome to write such form with inputs having these square brackets in their name, especially if it's a very complex form:
<form action="/users" method="post">
<input type="text" name="user[name][first]" />
<input type="text" name="user[name][last]" />
<input type="text" name="user[address][first_line][house_no]" />
<input type="text" name="user[address][first_line][street]" />
<input type="text" name="user[address][second_line][state]" />
<input type="text" name="user[address][second_line][country]" />
<input type="text" name="user[hobbies][]" />
<input type="text" name="user[hobbies][]" />
<input type="text" name="user[hobbies][]" />
</form>
Wouldn't it be nice to not care about whether or not you've properly put those square brackets and everything is working properly because of it?
Using the @unobtrusive/react-form-builder
package, the above form can be re-written like such:
<builder.form action="/users" method="post" resource="user">
<builder.fields name="name">
<builder.input type="text" name="first" />
<builder.input type="text" name="last" />
</builder.fields>
<builder.fields name="address">
<builder.fields name="first_line">
<builder.input type="text" name="house_no" />
<builder.input type="text" name="street" />
</builder.fields>
</builder.fields>
<builder.fields name="address">
<builder.fields name="second_line">
<builder.input type="text" name="state" />
<builder.input type="text" name="country" />
</builder.fields>
</builder.fields>
<builder.input type="text" name="hobbies" collection />
<builder.input type="text" name="hobbies" collection />
<builder.input type="text" name="hobbies" collection />
</builder.form>
Now the form goes from a nice flat structure to a nested one, but those extra lines of code will make sure that the form, when submitted, will be compatible with the server. Also, it's a lot better to read as it follows a visual grouping of what inputs belong together.
Installation
Via NPM
$ npm install @unobtrusive/react-form-builder
Or via Yarn
$ yarn add @unobtrusive/react-form-builder
Features
There are 5 components provided by this package:
form
fields
input
select
textarea
all namespaced inside the builder
object. So the usage would look like:
import builder from "@unobtrusive/react-form-builder";
<builder.form {/* ... */}>
<builder.input {/* ... */} />
{/* ... */}
</builder.form>
All the components mentioned above are just thin wrappers on the original components. You just need to pass some special props to these in order for them to work. The rest of the props (including the children) are just forwarded unchanged to the original component.
Also note that even the ref passed to these components will be forwarded to the original components, which is also the reason why these are very thin wrappers.
form
component
Custom props
-
resource
: Will be used to determine the first part of the input names
Usage
<builder.form action="/users" method="post" resource="user">
{/* ... */}
</builder.form>
/* == (is equivalent to) */
<form action="/users" method="post">
{/* ... */}
</form>
Except for resource
, every other prop is forwarded to the original component. The resource
prop is internally used by the component to set up things which will then be used by the other components in the list.
input
component
Custom props
-
name
: Will be used to determine what goes inside the square brackets. Should be used in conjunction withform
-
collection
: Will be used to determine if the input is part of a collection or not. If it is, then empty square brackets are appended at the end
Usage
<builder.form action="/users" method="post" resource="user">
<builder.input type="email" name="email" />
</builder.form>
/* == (is equivalent to) */
<form action="/users" method="post">
<input type="email" name="user[email]" />
</form>
If inputs are part of a collection:
<builder.form action="/users" method="post" resource="user">
<builder.input type="checkbox" name="superpowers" collection />
<builder.input type="checkbox" name="superpowers" collection />
<builder.input type="checkbox" name="superpowers" collection />
<builder.input type="checkbox" name="superpowers" collection />
</builder.form>
/* == (is equivalent to) */
<form action="/users" method="post">
<input type="checkbox" name="user[superpowers][]" />
<input type="checkbox" name="user[superpowers][]" />
<input type="checkbox" name="user[superpowers][]" />
<input type="checkbox" name="user[superpowers][]" />
</form>
fields
component
Doesn't render any component. It is used to create a nested structure for more complex form.
Custom props
-
name
: Same asinput
. Except, it is used when names become complex and they have multiple parts to them -
resource
: Same asform
. Except, should only be used when the parent form is not rendered by the builder. For e.g., when the form is rendered from somewhere else and you only want to render a part of it using the builder -
collection
: Same asinput
. Except, it would act on the inputs passed to it as children instead of acting on the component itself (also because thefields
component doesn't render anything)
Usage
<builder.form action="/users" method="post" resource="user">
<builder.fields name="address">
<builder.input type="number" name="latitude" />
<builder.input type="number" name="longitude" />
</builder.fields>
</builder.form>
/* == (is equivalent to) */
<form action="/users" method="post">
<input type="number" name="user[address][latitude]" />
<input type="number" name="user[address][longitude]" />
</form>
Can also be used alongside simple named inputs:
<builder.form action="/users" method="post" resource="user">
<builder.input type="email" name="email" />
<builder.fields name="address">
<builder.input type="number" name="latitude" />
<builder.input type="number" name="longitude" />
</builder.fields>
</builder.form>
/* == (is equivalent to) */
<form action="/users" method="post">
<input type="email" name="user[email]" />
<input type="number" name="user[address][latitude]" />
<input type="number" name="user[address][longitude]" />
</form>
Any level of nesting is possible:
<builder.form action="/users" method="post" resource="user">
<builder.fields name="address">
<builder.fields name="coordinates">
<builder.fields name="geometric">
<builder.input type="number" name="latitude" />
<builder.input type="number" name="longitude" />
</builder.fields>
</builder.fields>
</builder.fields>
</builder.form>
/* == (is equivalent to) */
<form action="/users" method="post" resource="user">
<input type="number" name="user[address][coordinates][geometric][latitude]" />
<input type="number" name="user[address][coordinates][geometric][longitude]" />
</form>
When the main form is not rendered using the builder:
<form action="/users" method="post">
{/* ... */}
<builder.fields name="address" resource="user">
<builder.input type="number" name="latitude" />
<builder.input type="number" name="longitude" />
</builder.fields>
</form>
/* == (is equivalent to) */
<form action="/users" method="post">
{/* ... */}
<input type="number" name="user[address][latitude]" />
<input type="number" name="user[address][longitude]" />
</form>
If you don't pass the resource
prop in the above case, then it will throw an error!
For grouped collection:
<builder.form action="/users" method="post" resource="user">
<builder.fields name="addresses" collection>
<builder.input type="number" name="latitude" />
<builder.input type="number" name="longitude" />
<builder.input type="number" name="latitude" />
<builder.input type="number" name="longitude" />
<builder.input type="number" name="latitude" />
<builder.input type="number" name="longitude" />
{/* ... */}
</builder.fields>
</builder.form>
/* == (is equivalent to) */
<form action="/users" method="post">
<input type="number" name="user[addresses][][latitude]" />
<input type="number" name="user[addresses][][longitude]" />
<input type="number" name="user[addresses][][latitude]" />
<input type="number" name="user[addresses][][longitude]" />
<input type="number" name="user[addresses][][latitude]" />
<input type="number" name="user[addresses][][longitude]" />
{/* ... */}
</form>
textarea
component
Same as input
.
select
component
Same as input
. For rendering option
, you can use the usual component.