erikras.com
HomeAbout

๐Ÿ 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

Posted in Coding, ๐Ÿ Final Form
December 11, 2017ย -ย 5 min read
Photo by Matthew Hamilton
Photo by Matthew Hamilton

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 Form
const form = createForm({ onSubmit });
// Create Decorator
const decorator = createDecorator(
// Calculations:
{
field: "foo", // when the value of foo changes...
updates: [
{
// ...set field "doubleFoo" to twice the value of foo
doubleFoo: (fooValue, allValues) => fooValue * 2,
},
],
},
{
field: /items\[\d+\]/, // when a field matching this pattern changes...
updates: {
// ...sets field "total" to the sum of all items
total: (itemValue, allValues) =>
(allValues.items || []).reduce((sum, value) => sum + value, 0),
},
}
);
// Decorate form
const 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 Form
const form = createForm({
mutators: { setFieldData },
onSubmit,
});
form.mutators.setFieldData("firstName", { awesome: true });
form.registerField(
"firstName",
(fieldState) => {
const { awesome } = fieldState.data; // true
},
{
// ...other subscription items
data: 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.
<FormSpy
subscription={{ 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. โค๏ธ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป

Discuss on Twitter

ยฉ 2023 โ€“ Erik Rasmussen