
Many were faced with the choice of a particular library for working with forms in ReactJS. When I chose the right one for me, different libraries seemed perfect BUT: a form in configs or callbacks in the onSubmit event, or asynchronous submit. Why do the forms for the reactor
not correspond to the principles of the reactor , why do they look like something special? If these questions occurred to you, or you like forms, invite you to read the article.
Let's imagine the forms as they should be.
The form in the reactor should:- provide controllability of fields and events
- best fit html projection
- respect declarative and composition
- use typical methods of working with React components
- have predictable behavior
The form in the reactor should not:')
- set the management model
- be redundant or require additional data
- require customization or mandatory use of helper functions
Now we will try to describe the ideal form based on these rules:<Form action="/" method="post"> <Validation> <Field type="text" name="firstName"> <Field type="text" name="lastName"> <Transform> <Field type="number" name="minSalary"> <Field type="number" name="maxSalary"> <Transform> <Field type="email" name="email"> </Validation> <button type="submit">Send<button> </Form>
It looks almost like a regular html form, with the exception of
Field instead of input and the unknown
Validation and
Transform . You probably already guessed that the
Validation tag should check the value of the fields and create error messages for them. The
Transform tag, in turn, is needed to calculate the minSalary and maxSalary fields.
Did I say something about React?
Fast forward to the realities of the reactor and describe the same form: class MySexyForm extends React.Component { constructor(props) { super(props); this.state = { model: {} }; this.validator = (model, meta) => { let errors = { ...meta.submitErrors }; if(model.firstName && model.firstName.length > 2) { errors = { firstName: ["First name length must be at minimum 2"] }; } if(model.lastName && model.lastName.length > 2) { errors = { ...errors, lastName: ["Last name length must be at minimum 2"] }; } return errors; }; this.transformer = (field, value, model) => { switch (field) { case "minSalary": if (parseInt(value) > parseInt(model.maxSalary)) { return { maxSalary: value }; } case "maxSalary": if (parseInt(value) < parseInt(model.minSalary)) { return { minSalary: value }; } } return {}; }; } onSubmit = (event) => (model) => { event.preventDefault(); console.log("Form submitting:", model); this.props.sendSexyForm(model);
I will not consider the Field component in detail, let’s imagine that it renders input with the transmitted prop and additional value and onChange. As well as error messages for this field.
It is necessary to explain the emergence of new fields initValues, values, onModelChange, onSubmit, validator, transformer.
Let's start with the prop added to the
Form .
The
onSubmit event handler allows
you to intercept the form submission event, to access this event and to the current values of the form fields through the model argument.
The
onModelChange event handler allows
you to track changes in form fields.
With the help of
values, we can control the values of the fields, and
initValues allows
us to set the initial values.
This basic functionality provides the majority of libraries for working with forms in a reactor, nothing unusual, everything is as it should be.
Consider the
Validation tag, it has two prop.
- validator - a function that returns validation errors based on the passed values of form fields
- submitErrors is an additional rest field passed by the second argument to the validator function, in which we send the errors received from the server after the submission. Essentially, all arguments passed to Validation with the exception of the validator are passed to rest.
Unfortunately, I have not seen a similar or similar implementation of validation, although it seems obvious: we have a validation function that receives data and returns errors based on them, no side effect logic, everything is as it should be in a reactor.
Let's go to the
Transform component. It intercepts the changes from the nested fields and calls the transformer function, which takes three arguments:
- field - the name of the field in which the change occurred
- value - new value of this field
- model - the current value of the form fields with the previous value of the changed field
It should return an object of the form {[field]: value} that will be used to update other form fields.
Also an obvious implementation of calculated fields.
And ... what do we have in the end?
Since we initially used a declarative approach and a composition, we can combine the components of validation and transformation and use them for separate groups of fields.
In the Form component, there are no extra props responsible for additional functionality (transformation and validation).
Field gets its value, error information and callback functions by means of
contexts , which allows you to create additional components for working with the form and delegate responsibility. The form itself has a form similar to html projection, which simplifies understanding.
react-painlessform
I have written my own library that helps to make forms simple and clear. The code is available on
Github .
And also see a
live example from the article.
Thanks for attention
Once you’ve read it to the end, you love the forms a lot, or the article was interesting, I’ll be happy to read the comments and hear your opinion about the forms in the reactor.