In this tutorial, we are going to implement a simple React library for creating, composing and serializing form elements.
The goal is to allow developers to create forms with minimal code amount and maximal flexibility of the composition. The library should have the built-in validation and return serialized data on submit. Using the library shouldn’t require any extra code but simple declaration of an order, types, names and validation rules. The whole internal data flow will be hidden in the implementation.
Import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.jsx'; ReactDOM.render(, document.getElementById('app')); When the input text value changes, the state will be updated. Complex Example. In the following example, we will see how to use forms from child component.
Let’s get started.
Our tech stack
One thing I love in React the most is its simplicity. It allows me to write my views as reusable components being functions of the state – I give them the model and they render the view, nothing more. React takes full responsibility for re-rendering, managing the view transitions and this is done in a very efficient way.
What React cannot do for me though is state and data managing. Remember – it’s only a view library, we have to organise the data flow around the views separately.
Fortunately, React works perfectly with well-known patterns commonly used for a long time. We’re using redux as a way to manage data flow.
To use redux, we have to implement two elements: action creator and state reducer. These names might sound scary, but the idea is really simple.
To fully understand how redux works and how powerful it can be, I recommend watching the presentation and lessons on egghead by Dan Abramov, the author of redux.
To simplify the whole process, we will use ready components from the Material UI to avoid extra styling and focus on composing and validation of the form.
Bootstrapping the project
First let’s set up the project and the environment. I assume you have node already installed. Create a new folder and initialize the new npm project using
npm init
.You can either follow along in this article, or view the final result here
We need a tool for building your scripts into the one minified file – webpack is IMO an excellent choice for this purpose. Code will be written in ES2015 (aka ES6) and JSX syntax, so you also need the babel plugin to convert the code into a version readable for all modern browsers.
Begin by installing webpack globally:
npm i -g webpack
. Then install plugins and all required dependencies locally: npm i --save-dev webpack@^1.0.0 babel-loader babel-preset-es2015 babel-preset-react path
.Create a new file called
build.config.js
, containing the following configuration script:View the code on Gist.
Add build aliases at the end of
package.json
:View the code on Gist.
Install react modules:
npm i -S react react-dom
. Create a file called demo.js
– we will import modules from the library there and render the example form to see the results. For now it renders an empty div
:View the code on Gist.
To run the demo, we have to open an html file. Create a folder called
dist
and put file demo.html
with simple markup loading the script:View the code on Gist.
Now you can run build process with the watcher to see if it works:
npm run watch
Webpack will compile the demo and start watching for changes in the code. You should see output similar to the one below:
View the code on Gist.
Congratulations! The environment is ready for coding.
Form component
Create the first component called
Form
. It’s going to be the root component for fields nested inside:View the code on Gist.
The code above doesn’t add any extra value when we compare it with using the simple
<form>...</form>
. Our component should store the data from its children. To achieve this, we have to connect this component to redux.Start with creating constants for action types. We will use them to define and recognize actions in action creators and the store:
View the code on Gist.
Action creator is a function returning callback for dispatching an action. Within an action we can for example make an AJAX call and then dispatch one or many actions. Redux injects callable action to the component as a prop. When we want to trigger an action in the component, we simply call the action with required data passed as an argument.
Create two action creators for updating and resetting the form data:
View the code on Gist.
These actions will be passed into the
Form
component.Now we can respond to the actions and modify data in the model using reducers. Reducer is another function, which takes the current state and returns the new one based on the action. Redux puts the new state to the components and View is being re-rendered.
Install lodash.assign (
npm i -S lodash.assign
) and create the store:View the code on Gist.
In redux, our model is a reducer – simple function which reduces the current (or initial) state to the new one based on the action data (so it works in the same way as function passed to Array.prototype.reduce)
Important! If we modify the state, the reference of the state must also change (redux propagates changes by checking references), so always create the new objects – that’s why I used
assign({}, ...)
in the return statements above.Now connect action creators, store, and the root component together.
Install extra modules required to use redux with our component:
npm install --S redux react-redux redux-thunk redux-logger react-tap-event-plugin@^0.2.0
Create a wrapper for the
Form
:View the code on Gist.
A few lines, but also lots of the new code:
connect()
takes the React component, a function returning the current state and actions. It returns the smart component with all three elements bound together.<Provider>
is responsible for connecting all its smart children with the actual store.reduxMiddleware
is a simple middleware between dispatching actions and calling reducers. In this case we compose two middleware elements: redux-thunk, and redux-logger: -redux-thunk
is used for dispatching asynchronous actions (read more),redux-logger
is used for logging all dispatched actions in the console so we can easily track them.injectTapEventPlugin
is a fix required by material-ui
Text field
Create the first visible element – a text field. Install material-ui and create the component:
View the code on Gist.
The code above wraps
TextField
and passes the props. We have to export this component to allow the programmer for importing it – add the following line at the end of index.js
:View the code on Gist.
Let’s try to render the first example to see how the rendering code will look like:
View the code on Gist.
There is a problem: how to update the model in
Form
when user types some text? In the above example we actually defined all we need – the model is stored in Form
, name of the field is defined too, our library has all required information.Let’s not spoil this simplicity: we can hide the passing of data between components inside the implementation of our library. We have to use React’s feature called context.
Using React context
Occasionally, you want to pass data through the component tree without having to pass the props down manually at every level. React’s “context” feature lets you do this.
Add the following code to
src/components/Form.js
:View the code on Gist.
Form
now exports the model and actions via the context so they can be used in its child components.Update
Text
to see how it works:View the code on Gist.
- value is removed from the props, we get it from the context now:
this.context.values[this.props.name]
- model is updated on input change via the action in context:
this.context.update(this.props.name, value)
Let’s see how it looks. Run
npm run build
if you haven’t already, and open dist/demo.html
in a browser:Validation
Now if we are able to compose text fields in the form, it’s time to implement some validation. Let’s start with implementing three example rules (install valid-url and email-validator before):
View the code on Gist.
Each validator is a function returning an array with errors. If the returned array is empty – validation has passed.
But how to apply these validators to the
Text
field? Take a look:View the code on Gist.
-
validate
has been added to the props, so programmer can define an array with rule names that should be applied.isValid
is called on blur. It iterates through the rules, call each of them and returns array with the errors- text with errors is passed to the
TextField
updateValue
has changed slightly. When user gets back to the field with errors, we refresh validation on each change. If the value is correct, errors should disappear immediately.
Update the demo:
View the code on Gist.
Refresh the demo and see how it works:
Before we submit the form, we need to check the validation status in all nested elements and prevent submission if one of the field is filled incorrectly.
The idea is to register
isValid
method in Form
component from the field component, using the context (install lodash.without before):View the code on Gist.
-
registerValidation
adds a reference of the validating function to the array (used when field component is mounted) and returns another function removing the same reference from the registerisFormValid
checks registered validation functions and returnstrue
orfalse
. This method is also injected into the context, so all nested components can check if the form is valid or notsubmit
checks if the form is valid, sends copy of the model to the callback function and resets the model to the initial state (seereset
action creator)
Update
View the code on [Gist](https://gist.github.com/2768859ef3b724754586).Text
component as well:Validation method is registered on mounting,
removeValidationFromContext
is stored and called on unmounting.Serialization
Once we can check form validation locally and globally, let’s implement the submit button:
View the code on [Gist](https://gist.github.com/a879ac3ab4a28e5fcc4c).-
isFormValid
is taken from the context- If
isFormValid
returns false, the button is disabled - When button is clicked, we call
submit
action from the context
Export the new component same as others:
View the code on [Gist](https://gist.github.com/bb31dfefb93ceec23f7d).Update the demo:View the code on [Gist](https://gist.github.com/f64aedc8ddaf75ab662e).
and see how it works:
Summary
Congratulations! We have learned a few nice things together:
- how to set up new ES2015 project using webpack and babel
- how to use redux
- how to use React’s context
- how to take advantage of JSX design to create simple composition concept
- how to hide implementation details away in the library
The final code is available on GitHub.
Now, if you get the idea, you can freely scale your new library to handle all types of fields: passwords, check boxes, radio groups, datepickers, selects and literally any custom inputs you can imagine.
Also, these ideas are worth considering:
- try refactoring validators and custom fields into separate npm modules and compose them as needed rather than grouping everything in the one library
- create additional layer of abstraction to make switching from material-ui to other components possible (eg. if you want to apply your own styles)
- initialize new fields with default value using context to avoid missing properties in the model
If you have any questions, catch me on Twitter (@KasperWargula).
Extra resources
3r3-31. First you need to install the 3r3r6646 component. react-validation-boo [/b] , I assume that you are familiar with react and know how to configure. 3r3885.
3r3885.
npm install react-validation-boo 3r3885.
3r3885.
In order not to talk a lot, I will immediately give you a small sample code. 3r3885.
3r3885.
3r3653. import React, {Component} from 'react';
import {connect, Form, Input, logger} from 'react-validation-boo';
class MyForm extends Component {
sendForm = (event) => {
event.preventDefault ();
if (this.props.vBoo.isValid ()) {
console.log ('Get the values entered and send them to the server', this.props.vBoo.getValues ());
} else {
console.log ('We will output errors to the console', this.props.vBoo.getErrors ());
}
};
getError = (name) => {
return this.props.vBoo.hasError (name)?
{this.props.vBoo.getError (name)}
: ';
};
render () {
return
3r33824.
3r3752.
{this.getError ('name')}
3r33894.
3r33836.
{this.props.vBoo.isValid ()? 'You can send': 'Be careful !!!'}
3r33838.
3r33841.
}
}
export default connect ({
rules: () => (3r33898.[
['name', 'required'],
]
),
middleware: logger
}) (MyForm);
3r3883. 3r3884. 3r3885.
3r3885.
Let's break this code down. 3r3885.
3r3885.
Let's start with the function 3r3646. connect [/b] , we pass our validation rules and other advanced parameters to it. By calling this method we get a new function in which we transfer our component ( MyForm ), So that it will receive in props necessary methods of work with the validation of forms. 3r3885.
3r3885.
In function 3r3646. render [/b] of our component, we return the component Form which we connect with the rules of validation connect = {this.props.connect} . This necessary design to Form knew how to validate nested components. 3r3885.
3r3-300. The input field that we will check, we have passed the validation rules to connect in property 3r3646. rules [/b] . In our case, this is name must not be empty ( required ). 3r3885.
3r3885.
We are also in 3r3646. connect [/b] handed over 3r3646. middleware: logger [/b] in order to see the validation in the console. 3r3885.
3r3885.
In 3r3646. props [/b] of our component, we got a set of functions: 3r3r885.
3r3885.
3r3-3598.
3r3612. vBoo.isValid () - returns 3r3r6646. true [/b] if all input components have been validated 3r3-3617.
3r3612. vBoo.hasError (name) - returns 3r3r6646. true [/b] if the component with property name not validin
3r3612. vBoo.getError (name) - for the component with property name It returns the text of the error 3r31717.
3r3619. 3r3885.
Now we will gradually complicate, for a start in 3r3r6646. connect [/b] let's pass the language in order to be able to change the validation rules depending on the language, and also add additional fields and validation rules. 3r3885.
3r3885.
3r3653. import React, {Component} from 'react';
import {connect, Form, Input, InputCheckbox} from 'react-validation-boo';
class MyForm extends Component {
sendForm = (event) => {
event.preventDefault ();
if (this.props.vBoo.isValid ()) {
console.log ('Get the values entered and send them to the server', this.props.vBoo.getValues ());
} else {
console.log ('We will output errors to the console', this.props.vBoo.getErrors ());
}
};
getError = (name) => {
return this.props.vBoo.hasError (name)?
{this.props.vBoo.getError (name)}
: ';
};
render () {
return
3r33824.
{this.props.vBoo.getLabel ('name')}:
3r3752.
{this.getError ('name')}
3r33894.
3r33824.
{this.props.vBoo.getLabel ('email')}:
3r33762.
{this.getError ('email')}
3r33894.
3r33824.
{this.props.vBoo.getLabel ('remember')}:
3r33418.
{this.getError ('remember')}
3r33894.
3r33836.
{this.props.vBoo.isValid ()? 'You can send': 'Be careful !!!'}
3r33838.
3r33841.
}
}
export default connect ({
rules: (lang) => {
let rules =[
[
['name', 'email'],
'required',
{3rr3898. error: '% name% should not be empty'
}
], 3r33898. W2w2w227.
]; 3-333898. 3r3-33898. Rules.push (['remember', lang 'ru' ? 'required': 'valid']);
Return rules;
}, 3r33898.
Email: 'Email',
Remember: 'Remember'
}),
Lang: 'en'
}) (MyForm);
3r3883. 3r3884. 3r3885.
In this example, the checkbox is remember 3r3r664 must be installed in Russian. required [/b] , and on others it is always valid valid . 3r3885.
3r3885.
We also referred to 3r3646. connect [/b] function 3r3r6646. labels (lang) [/b] which returns the name of the fields in a readable form. 3r3885.
3r3885.
In 3r3646. props [/b] your component, there is a function getLabel (name) which returns the value passed by the function 3r3646. labels [/b] or if there is no such value, it returns name . 3r3885.
3r3885.
3r33545. The basic components of vBoo
3r3885.
Form , Input , InputRadio , InputCheckbox , Select , Textarea . 3r3885.
3r3885.
3r3653. import React, {Component} from 'react';
import {connect, Form, Input, Select, InputRadio, InputCheckbox, Textarea} from 'react-validation-boo';
class MyForm extends Component {
sendForm = (event) => {
event.preventDefault ();
if (this.props.vBoo.isValid ()) {
console.log ('Get the values entered and send them to the server', this.props.vBoo.getValues ());
} else {
console.log ('We will output errors to the console', this.props.vBoo.getErrors ());
}
};
getError = (name) => {
return this.props.vBoo.hasError (name)?
{this.props.vBoo.getError (name)}
: ';
};
render () {
return
3r33824.
{this.props.vBoo.getLabel ('name')}:
3r3752.
{this.getError ('name')}
3r33894.
3r33824.
{this.props.vBoo.getLabel ('email')}:
3r33762.
{this.getError ('email')}
3r33894.
3r33824.
{this.props.vBoo.getLabel ('gender')}:
3r3772.
3r3774. Your floor
3r3r7777. Male
3r33780. Women's
{this.getError ('gender')}
3r33894.
3r33824.
3r33824. {this.props.vBoo.getLabel ('familyStatus')}:
3r33824.
single 3r3827.
3r33894.
3r33824.
3r33383.
cohabitation
3r33894.
3r33824.
marriage
3r33894.
{this.getError ('familyStatus')}
3r33894.
3r33824.
{this.props.vBoo.getLabel ('comment')}:
3r33737. 3r33737.
{this.getError ('comment')}
3r33894.
3r33824.
{this.props.vBoo.getLabel ('remember')}:
3r33418.
{this.getError ('remember')}
3r33894.
3r33836.
{this.props.vBoo.isValid ()? 'You can send': 'Be careful !!!'}
3r33838.
3r33841.
}
}
export default connect ({
rules: () => ([
[
['name', 'email'],
'required',
{3rr3898. error: '% name% should not be empty'
} 3r33898.],
['email', 'email']} ,
W2w2w26., 'Valid']
]),
Labels: () => ({
Name: 'Name',
Email: 'E-mail', 3r33898. Gender: 'Sex',
familyStatus: 'Marital status',
comment: 'Comment',
remember: 'remember'
}), 3r3r9898. lang: 'en'
}) (MyForm);
3r3883. 3r3884. 3r3885.
3r33545. Validation Rules 3r33546. 3r3885.
Let's take a look at how to write your own validation rules. 3r3885.
In order to write a rule you need to create a class that will be inherited from the class validator . 3r3885.
3r3885.
3r3653. import {validator} from 'react-validation-boo';
class myValidator extends validator {
/3r3r9898. * name - the name of the field, if there is a label, then it will be passed to
* value - the current value of the field
* params - parameters that were passed by the 3rd argument to the validation rules (rules)
* /
validate (name, value, params) {
let lang = this.getLang ();
let pattern = /^ d + $ /;
if (! pattern.test (value)) {
let error = params.error || 'Error for field% name% with value% value%';
error = error.replace ('% name%', name);
error = error.replace ('% value%', value);
this.addError (error);
}
}
}
export default myValidator;
3r3883. 3r3884. 3r3885.
Now we connect our validator to the form. 3r3885.
3r3653. import myValidator from 'path /myValidator';
//3r39898.
export default connect ({
rules: () => ([
[
'name',
'required',
{
error: '%name% не должно быть пустым'
}
],
[
'name',
'myValidator',
{
error: 'это и будет params.error'
}
]
]),
labels: () => ({
name: 'Name'
}),
validators: {
myValidator
},
lang: 'en'
}) (MyForm);
3r3883. 3r3884. 3r3885.
In order not to prescribe all your validation rules every time, we create a separate file where they will be registered and connect it to 3r3r6646. validators: `import 'file-validation'` [/b] . And if there are any special rules for this form, then validators: Object.assign ({}, `import 'file-validation'`, {}) 3r3885.
3r3885.
3r33545. Scenarios 3r33546. 3r3885.
Consider cases when we need to change the validation rules depending on the actions performed on the form. 3r3885.
3r3885.
By default, we have a script called default , in the rules we can prescribe under what scenario to conduct this validation. 3r3885.
3r3885.
If the script is not specified, then validation will be performed for all scripts. 3r3885.
3r3885.
3r3653. rules = () => ([
[
'name',
'required',
{
error: '%name% не должно быть пустым'
}
], 3r3-39898.[
'name',
'myValidator',
{
scenario:['default', 'scenario1']
}
], 3r3-39898.[
'email',
'email',
{
scenario: 'scenario1'
}
]3r39898.]) 3r3-39898. 3r3883. 3r3884. 3r3885.
Through property 3r3646. props [/b] our component passed functions:
3r3885.
3r3-3598.
3r3612. vBoo.setScenario (scenario) - installs the script. scenario 3r3r7647. maybe both a string and an array, if we have several
scripts active at once.
3r3612. vBoo.getScenario () - returns the current script or array of scripts
3r3612. vBoo.hasScenario (name) - shows whether this script is currently installed, name line
3r3619. 3r3885.
Let's add an object in our form. scenaries in which we will store all possible scenarios, true script active, 3r3r6646. false not. 3r3885.
3r3885.
As well as features 3r3646. addScenaries [/b] and 3r3646. deleteScenaries [/b] that will add and delete scripts. 3r3885.
3r3885.
If we have a “marital status” selected “cohabitation” or “marriage”, then we add a comment field and of course this field should be validated only in this case, the script ' scenario-married '. 3r3885.
3r3885.
If the “Advanced” checkbox is set for us, then we add additional fields that will become mandatory, the script ' scenario-addition '. 3r3885.
3r3885.
3r3653. import React, {Component} from 'react';
import {connect, Form, Input, Select, InputRadio, InputCheckbox, Textarea} from 'react-validation-boo';
class MyForm extends Component {
constructor () {
super ();
this.scenaries = {
'scenario-married': false,
'scenario-addition': false
}
}
changeScenaries (addScenaries =[], deleteScenaries =[]) {
addScenaries.forEach (item => this.scenaries[item]= true);
deleteScenaries.forEach (item => this.scenaries[item]= false);
let scenario = Object.keys (this.scenaries)
.reduce ((result, item) => this.scenaries[item]? result.concat (item): result,[]);
this.props.vBoo.setScenario (scenario);
}
addScenaries = (m =[]) => this.changeScenaries (m,[]);
deleteScenaries = (m =[]) => this.changeScenaries ([], m);
sendForm = (event) => {
event.preventDefault ();
if (this.props.vBoo.isValid ()) {
console.log ('Get the values entered and send them to the server', this.props.vBoo.getValues ());
} else {
console.log ('We will output errors to the console', this.props.vBoo.getErrors ());
}
};
getError = (name) => {
return this.props.vBoo.hasError (name)?
{this.props.vBoo.getError (name)}
: ';
};
changeFamilyStatus = (event) => {
let val = event.target.value;
if (val! '1') {3r33898. this.addScenaries (['scenario-married'])
} else {
this.deleteScenaries (['scenario-married']);
}
};
changeAddition = (event) => {3r33898. let check = event.target.checked;
if (check) {
this.addScenaries (['scenario-addition'])
} else {
this.deleteScenaries (['scenario-addition']);
}
};
getCommentContent () {
if (this.props.vBoo.hasScenario ('scenario-married')) {
return (
{this.sect .)
}
return ';
}
getAdditionContent () {
if (this.props.vBoo.hasScenario ('scenario-addition')) {
return (
3r3826. {this. 3) 3r???. ;
}
return ';
}
render () {
return
3r33824.
{this.props.vBoo.getLabel ('name')}:
3r3752.
{this.getError ('name')}
3r33894.
3r33824.
{this.props.vBoo.getLabel ('email')}:
3r33762.
{this.getError ('email')}
3r33894.
3r33824.
{this.props.vBoo.getLabel ('gender')}:
3r3772.
3r3774. Your floor
3r3r7777. Male
3r33780. Women's
{this.getError ('gender')}
3r33894.
3r33824.
3r33824. {this.props.vBoo.getLabel ('familyStatus')}:
3r33824.
3r33795.
single 3r3827.
3r33894.
3r33824.
3r3804.
cohabitation
3r33894.
3r33824.
marriage
3r33894.
{this.getError ('familyStatus')}
3r33894.
{this.getCommentContent ()}
3r33824.
{this.props.vBoo.getLabel ('addition')}:
{this.getError ('addition')}
3r33894.
{this.getAdditionContent ()}
3r33836.
{this.props.vBoo.isValid ()? 'You can send': 'Be careful !!!'}
3r33838.
3r33841.
}
}
export default connect ({
rules: () => ([
[
['name', 'gender', 'familyStatus', 'email'],
'required',
{3rr3898. error: '% name% should not be empty'
}
],
['email', 'email']} 3r39898.], 3rr3898.['email', 'email']} , 3r3r9898. W2w2w228.,
W2w2w229.,
W2w2w230.,
]), 3rr3898. Labels: () => ({
Name: 'Name', 3) gender: 'Sex',
familyStatus: 'Marital status',
comment: 'Comment',
addition: 'Extras',
place: 'Location'
}),
lang: 'en'
}) (MyForm);
3r3883. 3r3884. 3r3885.
In order not to make the article very large, I will continue in the next one, where I will write how to create my components (for examplemeasure calendar or inputSearch) and validate them, how to associate with redux and more. 3r33894.
3r33891. ! function (e) {function t (t, n) {if (! (n in e)) {for (var r, a = e.document, i = a.scripts, o = i.length; o-- ;) if (-1! i[o].src.indexOf (t)) {r = i[o]; break} if (! r) {r = a.createElement ('script'), r.type = 'text /jаvascript', r.async =! ? r.defer =! ? r.src = t, r.charset = 'UTF-8'; var d = function () {var e = a.getElementsByTagName ('script')[0]; e.parentNode.insertBefore (r, e)}; '[object Opera]' e.opera? a.addEventListener? a.addEventListener ('DOMContentLoaded', d,! 1): e.attachEvent ('onload', d ): d ()}}} t ('//mediator.mail.ru/script/2820404/''_mediator') () (); 3r33892.
3r33894.