๐ Final Form โ Decorators, Calculated Fields, and Warnings
Two examples of how powerful decorators and mutators can be, when combined with the subscription-based paradigm of ๐ Final Form
Last week, I introduced the concept of mutators and array fields to ๐ Final Form. This week, Iโd like to introduce the concept of decorators, and show two examples of how powerful decorators and mutators can be, when combined with the subscription-based paradigm of ๐ Final Form.
Decorators
The term comes from ye olde ancient forgotten land of OOP.
In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.
In terms of ๐ Final Form, a decorator is just a function that takes the form object, subscribes to it somehow, and returns a function that can be called to โundecorateโ (undo all of its subscriptions). This allows a decorator to listen for changes and enact side-effects.
This simple pattern allowed me to write a 45 line โ and those are post-Prettier lines! โ library to perform realtime calculations between fields, not unlike a spreadsheet.
๐ Final Form Calculate
This library creates a ๐ Final Form decorator based on a set of calculation rules you provide. Hereโs what it looks like:
import { createForm } from "final-form";import createDecorator from "final-form-calculate";// Create Formconst form = createForm({ onSubmit });// Create Decoratorconst decorator = createDecorator(// Calculations:{field: "foo", // when the value of foo changes...updates: [{// ...set field "doubleFoo" to twice the value of foodoubleFoo: (fooValue, allValues) => fooValue * 2,},],},{field: /items\[\d+\]/, // when a field matching this pattern changes...updates: {// ...sets field "total" to the sum of all itemstotal: (itemValue, allValues) =>(allValues.items || []).reduce((sum, value) => sum + value, 0),},});// Decorate formconst undecorate = decorator(form);// Use form as normal
Here is a live demo showing what this looks like.
Kind of a silly example, but there are valid use cases for this feature.
๐ Final Form Set Field Data
This library is a trivial little thing that provides a mutator that set any arbitrary metadata for a field. You use it like this:
import { createForm } from "final-form";import setFieldData from "final-form-set-field-data";// Create Formconst form = createForm({mutators: { setFieldData },onSubmit,});form.mutators.setFieldData("firstName", { awesome: true });form.registerField("firstName",(fieldState) => {const { awesome } = fieldState.data; // true},{// ...other subscription itemsdata: true,});
Why would you want this? Well, for one, it allows you to createโฆ..
โ ๏ธ Field Warnings โ ๏ธ
Way back in September of 2016 โ in the horrible Dark Times before Prettierโa user and prolific contributor to Redux-Form submitted a pull request for a feature that never would have occurred to me. He needed fields to have what he called โwarningsโ, which are just like synchronous validation errors, but they donโt prevent submission. The way it was implemented was the way new features are often added to software: by taking the existing validation pipeline and duplicating all the methods and data structures to create a warning pipeline. It worked perfectly, of course, but as is often the case with code duplication, it created problems in the future, where someone fixed a bug in the error-handling code, but not in the mirror image warning-handling code. And, of course, it wasnโt long before someone came along and said, โYou know, itโd be cool if we could set, just, like, any arbitrary metadata on a field that we want, maaaan.โ
With the ability to set arbitrary metadata into fields provided by
final-form-set-field-data
, coupled with the subscription-based form state
paradigm of ๐ Final Form, it becomes trivial to implement your own warning
system. Behold:
const WarningEngine = ({ mutators: { setFieldData } }) => (// FormSpy lets you listen to any part of the form state you want.// If you provide an onChange prop, FormSpy will not render to the DOM.<FormSpysubscription={{ values: true }}onChange={({ values }) => {setFieldData("firstName", {warning: values.firstName ? undefined : "Recommended",});setFieldData("lastName", {warning: values.lastName ? undefined : "Recommended",});}}/>);
This component subscribes to the form values, so its onChange function gets called every time they change, and it sets or removes a warning value in the fields. All you have to do is include this somewhere inside your form โ after all the fields, so that they are registered when onChange is called the first time.
Here it is in action: (tab through the fields to see the warnings appear)
Conclusion
My goals with ๐ Final Form is to be as lean and flexible as possible. Rather than implementing a feature like field arrays or field warnings or calculated fields directly in the core library, the core library allows ways for them to be implemented outside, so that each application can only use the tools and pieces it needs, and unforeseen use cases can be implemented for their specific project and not for all. What interesting form features can you imagine building?
Happy coding, yโall. โค๏ธ๐ฉโ๐ป๐จโ๐ป