Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

How can decorators simplify the web forms development

Posted on Oct 3 Relatively recently, in TC39, a proposal for the implementation of decorators in ECMAScript reached the 3rd pre-final stage. A little later, MicroSoft released the 5th major version of TypeScript, where a new implementation of decorators started working out of the box without any experimental flags. Babel as well took the initiative, and in its documentation began to recommend using a new implementation of decorators. And all that means that decorators have finally begun to fully enter the lives of JavaScript developers.And on this hype train, I decided to tell you how, using decorators, you can improve your Developer Experience when developing forms.It is important to mention that in this article, I will write about an approach based on using the MobX library. So if you don't use it in your projects, the article may not be as useful as it could be. But you can consider reading it as a possible source of inspiration for how to develop web forms.In my previous project, I had to develop a lot of complex forms. Often, they consisted of several dozen fields.Most of the fields had to be validated. Of course, simple Validation rules, such as checking for the fullness of a Field or checking for the validity of an email address, appear quite often. But sometimes these rules were incredibly complicated. For example, depending on what the user chooses in one field, the validation rules for another field could change. And in some cases, it was necessary to turn off the validation of one field with certain values of the other. And, of course, there could be several validation rules for each field, each of which had to issue its own error message.But validation was only part of the necessary functionality. There must also be a form changes tracking. Relatively speaking, disable the submit button until the user makes some changes. But, again, there were dozens of fields on forms, so writing if's for each field was not the best solution.The situation was aggravated by the fact that some fields could represent arrays and sets of data. And if the user deleted several values from such a field, and then entered the same values manually, the form should have understood that it had returned to its original state.On top of this, the forms had to be able to reset the current state of the form to the original one. And of course, they had to be able to communicate with the server.I considered using various libraries, such as React Hook Form or Formik, but these options did not suit me. At the scale of those requirements, the code, even with these libraries, turned out to be too cumbersome and difficult to maintain. So I started thinking about my own solution.The separation of representation and logic is where I started. It was necessary to think of a way to somehow describe the logic of the form in a separate function or object and somehow minimize the need to write repeatable code as much as possible.In the end, I came to the conclusion that it is convenient to describe the logic of the form in a separate JavaScript class. Later in the text, I will call such a class a "form schema". Each property of such a class can represent a field in a form or perform some kind of utilitarian purpose. And with the help of decorators, you can assign the necessary logic separately for each property.In its simplest representation, such an object is a regular MobX store. For example, the code snippet below shows the simplest example of a form schema consisting of two fields: "First Name" and "Last Name". So far, without any logic.​What is field validation? This is one or more rules for checking the value of a field. In our case, the "field" is a property of the class. This means that several validation rules can be assigned to the corresponding property using the decorator: @validate.You simply pass several validator functions to the decorator, and the schema validates the field value itself. If there are several validators, the schema applies them sequentially. And only if all the rules are successfully passed, the schema tells that the field is valid.And again, we see the separation of the code. The logic of the form is kept separate from the declaration of validators. Of course, this is done on purpose. Thus, an approach to writing atomic validation functions is formed. And thanks to this, the general rules can be easily reused.A validator function for a schema is simply a function that returns either a string or a boolean value.If the function returns false, validation is considered successful. And if a string or true - no. Moreover, the string passed in the validator becomes an error message for the field.As the 1st input parameter, the function receives the current value of the property. And in the case of complex validation, each validator function takes an entire schema with all the properties as the 2nd parameter.The “confirm password” field must have exactly the same value as the “password” field. The date input field "From" must contain the date that was before the date in the "To" field. These are basic examples of when we need to use an entire schema for validation.The example below shows a sign-up form with an example of validating the password confirmation field.As I pointed out earlier, sometimes there may be situations when validation needs to be turned off. Fields may be optional or hidden for some reason, and in some cases, validation needs to be disabled depending on the values in other fields.Since the @validate decorator is already used for the declaration of casual validation, we cannot use it. But we can create its modifier: @validate.if. Such a modifier will work almost the same as the original one, with the only exception that, in addition to the array of validators, a predicate function needs to be passed, which tells whether validation is needed at the moment. If the predicate says that validation is not needed, the property is recognized as valid.The example below shows a scheme of three fields:This is a fairly simple validation example. You can take a look at an example of more complex validation, including conditional validation and rules that use an entire schema, on the documentation site.By default, the schema calculates the validation in the autorun function from the MobX library. Due to this, the validation of the property is recalculated automatically when it is changed. But also because of this, if other properties of the schema participated in the validation, when they are changed, the validation will be recalculated as well.The same rule works for the validation condition function. If the desired property or a property that participates in the condition has been changed, the predicate function will be called again.You should not worry about unnecessary recalculations. Thanks to MobX and MobX Form Schema optimizations, there are none of them. However, it is possible to disable automatic validation and start validating data manually. You can look at examples of manual validation at the link.These advantages may seem subjective, but I really liked this approach in the design of the code. It is well-readable and well-maintainable. It is flexible enough, and even in complex cases, it does not force you to write sophisticated logic.The downside may seem to be the need to write validators from scratch. Even the most basic ones. While other libraries supply them out of the box. But I have objections to this point:Both of these points lead to the need to provide functionality for overriding or configuring basic validators. But as you've seen for yourself, basic validators can consist of 3 lines of code. And for me, it's better to write 3 lines of code from scratch than to write them for the configuration of the out-of-the-box functionality.What's also great is that MobX From Schema works with decorators of both new and old implementations. But the new one has good typing support. Therefore, I can't pass a validator for a number to a string property.​Now let's move from validation to tracking form changes. It is not difficult to understand that the form has been changed. First, you need to save the initial state of the form. Secondly, at the desired moment, it is enough to use such a piece of code:This is an effective and simple way, but it is only suitable for simple forms. The more fields there are in the form, the longer this condition will be and the more difficult it will be to maintain such code.But this is not the only problem. In addition to simple text fields, there may be more complex fields in the form. For example, on most career-based websites, there’s a field for specifying your skill set. And the value of such a field should, in fact, be either an array or a set. And a simple reference comparison will not help here to understand whether the state of the form has changed.There is another approach: deep comparison.This approach solves the problems described above. But then there are problems with unnecessary calculations. Ideally, the form must tell if it changed or not upon any user interaction. But for each change, calling for a deep comparison can be too heavy.MobX allows you to work around both of these problems. In the form schema, when a certain field is changed, only the comparison that checks whether this particular field has been changed takes place.And to activate the tracking of changes of a form, it is enough to use the decorator: @watch.In this form, the isChanged flag will always be false if the first name is equal to "Initial name" and the last name is equal to "Initial surname". Even if the property changes its value to another one and then returns to its original state.The @watch decorator uses the reference comparison and tells the schema whether the value of the property has changed from its initial state.You may have noticed that I didn't call the makeObservable function in the example above. This is all because @watch applies observable.ref to properties by default. It is done for logical reasons - if you only need a reference comparison, you will hardly need deep observation through the observable. However, you can add it or any other observable modifications yourself without any problems.More importantly, the schema itself does not remember its initial state. But if you apply the @watch, the scheme will save the initial state only for the desired fields. Thus, there is no overhead of memory consumption.Due to reference comparison, @watch is mostly needed to observe primitive values. It is a bit more complicated when it comes to observing objects. For example, when checking whether an array has been changed, we need to check the number of elements in the initial and current states and also check that the elements match at each of the positions.But decorator modifiers come to our aid again. With their help, we can create such modifiers in which, instead of a reference comparison, there will be some other type of comparison. For example, if you want to use an array of values, use @watch.array, and if there is a set, use @watch.set. By default, these decorators will apply to shemas’s properties the observable.shallow.Once again, new decorators work better with typings. You cannot apply @watch.array to not an array property nor @watch.set to not a set property.Schemas can be nested. The simplest reason for this is the logical division of data. For example, in the user information form, information about his contacts can be a separate schema nested in the main schema.And, of course, such a nested separate schema can be used in the future without a parent scheme if such a situation is required.In addition to this, you can use the @watch.schemasArray modifier in case you need to use an array of nested schemas. Such an array, for example, can be an array of working experience information blocks in a CV form.In case you have some unusual data structure that requires an unusual comparison function, you can create a modification of the @watch decorator yourself. For this, you can use the watch.create method. But in order not to inflate the article too much, I will just modestly leave a link to the documentation if you are interested in looking at examples of using @watch.schemasArray and watch.create.In some cases, the form restoring function may be useful. Especially in editing forms with pre-filled data from the server. And since we already keep the original state of the form, it is not difficult at all for us to restore the form to its original state.And, of course, arrays, sets, nested schemas, and even your custom entities (if you describe them correctly) — all of them will be correctly restored.I will briefly mention that, as with validation, changes are tracked automatically. But, of course, the possibility of manually checking changes is also available.And so, with just a couple of decorators and methods, fully automated tracking of form changes is ready. No matter how complex the scheme is or how many nested schemes there are in it, you can always understand whether your form has changed. And you can always restore it to its original state.You can even create an IDE settings form schema. Usually, in such forms, there are many tabs, inside which there are nested tabs. And you can easily track changes and reset the form entirely, only at a specific tab or only at a nested tab. At the same time, these observations are quite cheap. When a field is changed, only this field will be checked.And, of course, the fact that @watch and its modifications are able to apply observable modifications from MobX on properties by themselves allows you to reduce your code even more.​Sometimes the data received from the server requires some kind of preprocessing before being used. For example, the server cannot send a Set or Date entity, but it may be more convenient for you to use the data in this format.There may be a reverse situation, the server may require data in a different format from the one in which it is stored in a schema.And usually, developers who experience such a need modify the data after receiving it, before using it, or before sending it. But with a form schema, such modifications can be described directly in the diagram.In the previous examples, you saw that to create a schema, you need to call the create static method. This method can accept an object as an argument, on the basis of which the schema can be filled with data.And you can also describe how the data obtained in this method should be preprocessed before the scheme starts using it. To do this, you can use the @factory decorator.Since each schema contains utilitarian data and methods, it may be useful for you to get an object that contains exclusively useful data from a schema. To do this, you can use the presentation getter of the schema, which by default creates a copy of the schema without utilitarian methods and properties.You can also use the @present decorator to change the contents of the presentation object. And you can even cut off some of its properties. For example, you hardly want to send the value of the password confirmation field to the server. To do this, you can use the @present.hidden modifier.And later, you can use this very presentation object when sending data to the server.​I created MobX Form Schema as a package with a minimal set of dependencies. So it is not necessary to use React. The only thing needed is MobX - this one has to be in the project.But nevertheless, I understand that in most cases, MobX is used with React, so I have prepared an example of using my library in a React application. But in order not to inflate the article, I'll just attach links to them: the web documentation, CodeSandbox.io, StackBlitz.com.And just to keep you interested, I'll briefly show you how a component for displaying a form with the pet's name from the "Conditional Validation" section might look like.Am I trying to assert in my article that this approach to form development is the only correct one? No, of course, there are no silver bullets in development. But it really seemed to me that this approach simplifies the development process. And, importantly, it has almost no effect on the size of your bundle - apart from MobX itself, all the functionality that I described is stored in a package of less than 4 KB. And considering that you will have to write less code, you can only win in terms of the size of the bundle.Moreover, this approach works well both on small-sized and on large-scale forms.However, yes, you need MobX. At least in my implementation. If someone does something similar for other state managers, it will be interesting for me to see it.In the article, I showed most but not all the functionality of the form schema. I have not shown how validation and changes tracking work in manual mode; I have not shown all modifiers of the decorators. And in general, if you are interested in this approach to form development, I recommend that you visit the documentation site. There are a lot of useful things there, including useful scenarios for using the form schema.I am waiting for your feedback in the comments. How do you like this approach in general?A link to the npm package.Bye.Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well Confirm For further actions, you may consider blocking this person and/or reporting abuse Francisco Inoque - Sep 24 Julius Ngwu - Sep 24 Abdalrhman Sami - Sep 20 Grace Valerie - Sep 24 Once suspended, yoskutik will not be able to comment or publish posts until their suspension is removed. Once unsuspended, yoskutik will be able to comment and publish posts again. Once unpublished, all posts by yoskutik will become hidden and only accessible to themselves. If yoskutik is not suspended, they can still re-publish their posts from their dashboard. Note: Once unpublished, this post will become invisible to the public and only accessible to Alexandrovich Dmitriy. They can still re-publish the post if they are not suspended. Thanks for keeping DEV Community safe. Here is what you can do to flag yoskutik: yoskutik consistently posts content that violates DEV Community's code of conduct because it is harassing, offensive or spammy. Unflagging yoskutik will restore default visibility to their posts. DEV Community — A constructive and inclusive social network for software developers. With you every step of your journey. Built on Forem — the open source software that powers DEV and other inclusive communities.Made with love and Ruby on Rails. DEV Community © 2016 - 2023. We're a place where coders share, stay up-to-date and grow their careers.



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

How can decorators simplify the web forms development

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×