📜 ⬆️ ⬇️

Forms should be simple and declarative.



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:


The form in the reactor should not:
')

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); //  action           submitErrors  } onModelChange = (model) => { console.log("Model was updated: ", model); this.setState({ model }); } render() { return ( <Form action="/" method="post" onSubmit={this.onSubmit} onModelChange={this.onModelChange} values={this.state.model} initValues={this.props.initValues} > <Validation validator={this.validator} submitErrors={this.props.submitErrors}> <Field type="text" name="firstName"> <Field type="text" name="lastName"> <Transform transformer={this.transformer}> <Field type="number" name="minSalary"> <Field type="number" name="maxSalary"> <Transform> <Field type="email" name="email"> </Validation> <button type="submit">Send<button> </Form> ); } }; 

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.

  1. validator - a function that returns validation errors based on the passed values ​​of form fields
  2. 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:


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.

Source: https://habr.com/ru/post/354562/


All Articles