Forms should be simple and declarative

Many got up to choosing a particular library for working with forms in ReactJS. When I chose the one that suits me, different libraries seemed perfect BUT: the form on the configs or the callbacks in the onSubmit event, or the asynchronous submit. Why do n't the react forms match the principles of the react , why do they look like something special? If these questions occurred to you, or you like forms, I invite you to read the article.
Let's imagine the forms what they should be.
The form in the reaction should:
- provide manageability of fields and events
- match html projection as much as possible
- comply with declarative and composition
- use typical methods for working with React components
- have predictable behavior
The form in the reaction should not:
- set management model
- be redundant or require additional data
- require customization or mandatory use of helper functions
Now let's try to describe the ideal form based on these rules:
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 evaluate the minSalary and maxSalary fields.
Did I say something about React?
Fast forward to the realities of the reaction 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); // абстрактный action после выполнения которого в форму приходят ошибки сабмита в виде submitErrors пропа
}
onModelChange = (model) => {
console.log("Model was updated: ", model);
this.setState({ model });
}
render() {
return (
I will not consider the Field component in detail, imagine that it renders input with the prop passed to the Field and additional value and onChange. As well as error messages for this field.It is worth explaining the appearance 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 event of the form submit , access this event and the current values of the form fields through the model argument.
The onModelChange event handler allows you to track changes in form fields.
Using values, we can control the values of the fields, and initValues allows you to set the initial values.
This basic functionality provides most libraries for working with forms in a react, nothing unusual, everything is as it should be.
Consider the tagValidation , he got two prop
- validator - a function that returns validation errors based on the passed form field values
- submitErrors - an additional rest field passed by the second argument to the validator function, in it we pass the errors received from the server after the submission. In fact, rest is passed all the arguments passed to Validation except validator
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 it, no side effect of logic, everything is as it should be in the reaction.
Let's move on to the Transform component . It intercepts changes in nested fields and calls a function - transformer, which takes three arguments:
- field - the name of the field in which the change occurred
- value - the 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} which will be used to update other fields of the form.
Also an obvious implementation of computed fields.
And ... what do we have in the end?
Since we initially used the declarative approach and composition, we can combine the components of validation and transformation and use them for individual groups of fields.
There are no extra prop in the Form component responsible for additional functionality (transformation and validation).
Field gets its meaning, 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 look similar to the html projection, which simplifies understanding.
react-painlessform
I wrote my own library that helps 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 have read to the end, then you are very fond of forms, or the article was interesting, I will be glad to read the comments and hear your opinion about the forms in the reaction.