From ecf310b5d6dbd0a52660b6eb8a057be8ae0d1281 Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Thu, 25 Apr 2019 23:06:42 -0400 Subject: [PATCH 1/3] Add complex forms --- src/pages/complex-forms-with-formik.md | 280 +++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 src/pages/complex-forms-with-formik.md diff --git a/src/pages/complex-forms-with-formik.md b/src/pages/complex-forms-with-formik.md new file mode 100644 index 0000000..9b8968f --- /dev/null +++ b/src/pages/complex-forms-with-formik.md @@ -0,0 +1,280 @@ +--- +title: Highly complex forms with Formik +date: 2019-04-24 +updated: +description: + I fell deeper in love with Formik after building the most complex form + I've encountered. +tags: javascript, react, formik +cover_image: https://i.imgur.com/rLxAGsz.jpg +--- + +(Image credit: +[Leonel Fernandez](https://unsplash.com/photos/REZp_5-2wzA)) + +Formik is a wonderful library. When combined with the Yup validation +library, it becomes almost trivial to handle touched inputs and +validation—even with large forms with many different types of fields. + +I recently used Formik and Yup to implement the most complex form I've +seen first-hand; a tool for constructing +[Stellar transactions](https://www.stellar.org/developers/guides/concepts/transactions.html). +Transactions on Stellar are composed of 3 main parts. There's the +transaction body, 1 or more signatures, and up to 200 +[operations](https://www.stellar.org/developers/guides/concepts/operations.html), +of which there are 12 different types with up to 10 properties. At a +(very) high level, this form needed: + +- A main form for the transaction +- Several signatures, each a plain string +- Multiple operations, each a different complex object + +I also wanted to have the operations behave like a "sub-form," added to +the transaction when a user presses enter. This meant I was looking at +doing two patterns that I'd specifically struggled with in the past. +I've found both sub-forms and an arbitrary number of inputs tricky to +implement—with the complex schema of my arbitrary number of sub-forms, I +was nervous. To my delight, I found Formik's included utilities vastly +simplified the implementation. + +# Creating a sub-form + +The fundamental problem with a sub-form is that HTML doesn't allow +`
` to appear within another `` node. I wanted my transaction +form to contain the operation forms before the 'Submit transaction' +button; + +``` ++------------------------+ +| [ source ] | +| [ memo ] | +| | +| { operation form 1 } | +| { operation form 2 } | +| | +| { signer form 1 } | +| { signer form 2 } | +| | +| < submit transaction > | ++------------------------+ +``` + +Formik provides such an effective abstraction over HTML forms, though, +that this problem became trivial to solve. Because Formik provides a +`submitForm` function to the render callback, it's easy to imperatively +trigger a form submission from outside the form. By change from a true +submit button to a regular button that submits on click, I can get the +best of both worlds. + +```js +// To simplify, I've removed some of the normal wiring needed to make +//this a working example. +() => ( + ( + <> + + + + +
+ +
+
+ +
+
+ +
+
+ +
+ + + )} + /> +); +``` + +With this, I got the behavior I was seeking. On the page it appears as a +single form, submitted as a single unit once it's completed. Each +operation, meanwhile, can be attached to the transaction itself when its +portion of the form is submitted. + +I discovered several footguns in this naive implementation. + +- Difficult to tell when an operation had been attached to the + transaction. +- Easy to submit a transaction before attaching the last operation. + +I adjusted the behavior in 2 ways in response. I changed operations to +display as text (rather than inputs) after being attached, with a button +to change to an edit mode. I also blocked submission of the top-most +form while a sub-form was being edited. These changes dramatically cut +the number of errors I made while manually testing. I wanted to call out +these problems specifically because they seemed like likely problems +when following this pattern. + +# Lists of inputs + +The signatures for the transaction are simple strings, but there can be +up to 20 of them. Dealing with lists in forms introduces a lot more +complexity. Tracking which index to update, adding new elements, and +removing existing elements all add logic. + +Formik, luckily, provides a utility specifically to help in this case; +`FieldArray`. It provides +[a number of typical array methods](https://jaredpalmer.com/formik/docs/api/fieldarray#fieldarray-helpers), +which made it trivial to handle this situation that I had previously +found so frustrating. + +```js + ( +
+ {values.signers.map((signer, i) => ( + <> + replace(i, e.target.value)} + // Formik is smart enough to understand indexes in input names. + name={`signers.${i}`} + value={signer} + /> + + + ))} + +
+ )} +/> +``` + +There are many other array utilities, but these three simple ones were +sufficient for my needs. My final code grew much more complex as I +fine-tuned the UX I wanted to offer the user (largely discovered through +mistakes I found myself making), but the core of it is quite simple. + +# Lists of many different complex inputs + +Happily, Formik provides such fantastic handling of input names when +determining changes, I found no additional complexity when the lists +were made of complex objects. + +```js + ( +
+ {values.operations.map((operation, i) => ( + <> + replace(i, e.target.value)} + // field1 + name={`operations.${i}.field1`} + value={operation} + /> + replace(i, e.target.value)} + // field2 + name={`operations.${i}.field2`} + value={operation} + /> + + ))} + +
+ )} +/> +``` + +However there was one hiccup left. I didn't have 1 type of complex +object, I had 12, with a total of 49 properties made up of 16 distinct +field types—and some of these types needed more than 1 input. Because +the fields needed by operations are defined by the Stellar protocol, I +also wanted to be able to quickly update them in the future with a high +degree of confidence. I also wanted complete clientside validation for +all values. + +Given the scope and constraints, it became clear to me that the +individual forms should be generated from a schema. After some trial and +error iteration, I found a pattern that worked quite well. + +I build the schema as an object, keyed by the operation name. Each +operation has a display name and a `fields` object. Each field is +defined as an object with 5 keys: a name, a function to render the +input, a label, a placeholder, and a validation function (from Yup). + +```js +const OperationsSchema = { + createAccount: { + id: 'createAccount', + label: 'Create Account', + fields: { + destination: { + name: 'destination', + render: props =>
, + label: 'Destination', + placeholder: '', + validation: stellarAddress().required(), + }, + startingBalance: { + name: 'startingBalance', + render: props => , + label: 'Starting balance', + placeholder: '', + validation: amount().required(), + }, + source: { + name: 'source', + render: props =>
, + label: 'Source account', + placeholder: '', + validation: stellarAddress(), + }, + }, + }, + // ... +}; +``` + +I found this dramatically reduced the amount of code I had to write. By +generating each operation's form from this schema, I only had to write +validation rules and a component for each field type. This isn't to say +that it was a trivial amount of code! The operation schema, form inputs, +and validation rules total just over 1000 lines. These building blocks, +however, leave few cracks for bugs to hide in, and significantly reduced +the amount of testing I felt necessary. + +Most field types are trivial, simple compositions of Formik helpers; +only separated for easy of future exension or styling. The complex field +types got unit tests to ensure their behavior. The rendering logic is +trivial, simply mapping over the `selectedOperation.fields` and calling +`render()`. The validation rules are easy to unit test, and follow Yup's +conventions. + +I'm so confident in the way these blocks fit together, I didn't feel a +need to write integration tests for the final forms—it would amount to +verifying that the schema objects have the right properties and that +there are no typos. + +The application itself is still an alpha and hasn't been open sourced +yet, so I don't want to share a link before it's ready for prime time. +But I've included a short demonstration of the completed form below. +It's rough! It hasn't passed by a designer yet! But I find it easy to +use and clear (if hideous and a little jarring at times). + + + +That's it! I wish you luck on the complex forms you make in the future. From a8baf40ea600ff96fa24a9cc8e1e8cf0739c1a30 Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Fri, 26 Apr 2019 00:02:25 -0400 Subject: [PATCH 2/3] Update complex forms --- src/pages/complex-forms-with-formik.md | 74 +++++++++++++++++--------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/src/pages/complex-forms-with-formik.md b/src/pages/complex-forms-with-formik.md index 9b8968f..3e0b9f6 100644 --- a/src/pages/complex-forms-with-formik.md +++ b/src/pages/complex-forms-with-formik.md @@ -19,23 +19,24 @@ validation—even with large forms with many different types of fields. I recently used Formik and Yup to implement the most complex form I've seen first-hand; a tool for constructing [Stellar transactions](https://www.stellar.org/developers/guides/concepts/transactions.html). -Transactions on Stellar are composed of 3 main parts. There's the -transaction body, 1 or more signatures, and up to 200 +(There's a demonstration video at the bottom of this post) Transactions +on Stellar are composed of 3 main parts: the transaction body, 1 or more +signatures, and up to 200 [operations](https://www.stellar.org/developers/guides/concepts/operations.html), -of which there are 12 different types with up to 10 properties. At a -(very) high level, this form needed: +of which there are 12 different types with between 1 and 10 properties. +At a (very) high level, this form needed: - A main form for the transaction - Several signatures, each a plain string - Multiple operations, each a different complex object -I also wanted to have the operations behave like a "sub-form," added to -the transaction when a user presses enter. This meant I was looking at -doing two patterns that I'd specifically struggled with in the past. -I've found both sub-forms and an arbitrary number of inputs tricky to -implement—with the complex schema of my arbitrary number of sub-forms, I -was nervous. To my delight, I found Formik's included utilities vastly -simplified the implementation. +I also wanted to have the operations behave like a "sub-form," being +added to the transaction when a user presses enter. This meant I was +looking at doing two patterns that I'd specifically struggled with in +the past. I've found both sub-forms and an arbitrary number of inputs +tricky to implement—with the complex schema of my arbitrary number of +sub-forms, I was nervous. To my delight, I found Formik's included +utilities vastly simplified the implementation. # Creating a sub-form @@ -62,9 +63,9 @@ button; Formik provides such an effective abstraction over HTML forms, though, that this problem became trivial to solve. Because Formik provides a `submitForm` function to the render callback, it's easy to imperatively -trigger a form submission from outside the form. By change from a true +trigger a form submission from outside the form. By changing from a true submit button to a regular button that submits on click, I can get the -best of both worlds. +behavior I want. ```js // To simplify, I've removed some of the normal wiring needed to make @@ -110,9 +111,9 @@ I discovered several footguns in this naive implementation. I adjusted the behavior in 2 ways in response. I changed operations to display as text (rather than inputs) after being attached, with a button -to change to an edit mode. I also blocked submission of the top-most -form while a sub-form was being edited. These changes dramatically cut -the number of errors I made while manually testing. I wanted to call out +to change to an edit mode. I also blocked submission of the main form +while a sub-form was being edited. These changes dramatically cut the +number of errors I made while manually testing. I wanted to call out these problems specifically because they seemed like likely problems when following this pattern. @@ -209,12 +210,19 @@ all values. Given the scope and constraints, it became clear to me that the individual forms should be generated from a schema. After some trial and -error iteration, I found a pattern that worked quite well. +error iteration, I found a pattern that worked quite well. I broke the +problem down into 3 set of building blocks. -I build the schema as an object, keyed by the operation name. Each -operation has a display name and a `fields` object. Each field is -defined as an object with 5 keys: a name, a function to render the -input, a label, a placeholder, and a validation function (from Yup). +- A schema describing the fields of each form. +- A component for each field type. +- A validation rule for each field type. + +I built the schema as an object, keyed by the operation name. Each +operation has a display name, an `id` (identical to the object key), and +a `fields` object. Each field is defined as an object with 5 properties: +an input name, a function to render the input, a label, a placeholder, +and a validation function (from Yup). Some fields also have a list of +options. ```js const OperationsSchema = { @@ -259,10 +267,26 @@ the amount of testing I felt necessary. Most field types are trivial, simple compositions of Formik helpers; only separated for easy of future exension or styling. The complex field -types got unit tests to ensure their behavior. The rendering logic is -trivial, simply mapping over the `selectedOperation.fields` and calling -`render()`. The validation rules are easy to unit test, and follow Yup's -conventions. +types got unit tests to ensure their behavior. The rendering logic +(below) is trivial, simply mapping over the `selectedOperation.fields` +and calling `render()`. The validation rules are easy to unit test, and +follow Yup's conventions. + +```js +
+ + {fields.map(({ render, name, options, label, placeholder }) => + render({ + key: name, + name, + label, + placeholder, + options, + }), + )} + +
+``` I'm so confident in the way these blocks fit together, I didn't feel a need to write integration tests for the final forms—it would amount to From 4db3a0d44cd42f5484ea06f132ec50914147aa52 Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Wed, 25 Sep 2019 20:56:02 -0400 Subject: [PATCH 3/3] WIP update --- src/pages/complex-forms-with-formik.md | 189 ++++++++++--------------- 1 file changed, 74 insertions(+), 115 deletions(-) diff --git a/src/pages/complex-forms-with-formik.md b/src/pages/complex-forms-with-formik.md index 3e0b9f6..cf304ab 100644 --- a/src/pages/complex-forms-with-formik.md +++ b/src/pages/complex-forms-with-formik.md @@ -16,106 +16,78 @@ Formik is a wonderful library. When combined with the Yup validation library, it becomes almost trivial to handle touched inputs and validation—even with large forms with many different types of fields. -I recently used Formik and Yup to implement the most complex form I've -seen first-hand; a tool for constructing -[Stellar transactions](https://www.stellar.org/developers/guides/concepts/transactions.html). -(There's a demonstration video at the bottom of this post) Transactions -on Stellar are composed of 3 main parts: the transaction body, 1 or more -signatures, and up to 200 -[operations](https://www.stellar.org/developers/guides/concepts/operations.html), -of which there are 12 different types with between 1 and 10 properties. -At a (very) high level, this form needed: - -- A main form for the transaction -- Several signatures, each a plain string -- Multiple operations, each a different complex object - -I also wanted to have the operations behave like a "sub-form," being -added to the transaction when a user presses enter. This meant I was -looking at doing two patterns that I'd specifically struggled with in -the past. I've found both sub-forms and an arbitrary number of inputs -tricky to implement—with the complex schema of my arbitrary number of -sub-forms, I was nervous. To my delight, I found Formik's included -utilities vastly simplified the implementation. +I used Formik and Yup for a particularly complex form. It needed 1 form +to input some metadata about a transaction, and a sub-form of one of 12 +different varieties—called an "operation" here. The backend for this +system accepts up to 200 operations per transaction, so this form needed +to reflect that. + +Some additional constraints I was working under: over the next several +years, it's likely that the validation rules will need to be adjusted an +updated, so another design goal was ease of maintenance in that regard. # Creating a sub-form The fundamental problem with a sub-form is that HTML doesn't allow `
` to appear within another `` node. I wanted my transaction form to contain the operation forms before the 'Submit transaction' -button; - -``` -+------------------------+ -| [ source ] | -| [ memo ] | -| | -| { operation form 1 } | -| { operation form 2 } | -| | -| { signer form 1 } | -| { signer form 2 } | -| | -| < submit transaction > | -+------------------------+ -``` +button. -Formik provides such an effective abstraction over HTML forms, though, -that this problem became trivial to solve. Because Formik provides a -`submitForm` function to the render callback, it's easy to imperatively -trigger a form submission from outside the form. By changing from a true -submit button to a regular button that submits on click, I can get the -behavior I want. +Formik provides a `submitForm` function +[as part of the "Formik bag"](https://jaredpalmer.com/formik/docs/api/formik#formik-render-methods-and-props), +which let's us imperatively trigger a form submission from outside the +form. By changing from a true submit button to a regular button that +submits on click, I can get the behavior I want. ```js -// To simplify, I've removed some of the normal wiring needed to make -//this a working example. () => ( ( + render={({ submitForm, ...props }) => ( <> - - - - -
- -
-
- -
-
- -
-
- -
- + + + )} /> ); ``` -With this, I got the behavior I was seeking. On the page it appears as a -single form, submitted as a single unit once it's completed. Each -operation, meanwhile, can be attached to the transaction itself when its -portion of the form is submitted. +I modelled this with Yup with `OperationForm` as an object array. +Because I wanted to make it easy to change the operation forms, I built +these as an object. -I discovered several footguns in this naive implementation. +```js +const formSchema = { + input: { + id: 'input', + label: 'string', + fields: [ + { + name: 'string', + render: props => , + placeholder: 'string', + default: 'input value', + validation: yup.string('Any yup rule'), + }, + ], + }, + // … +}; +``` -- Difficult to tell when an operation had been attached to the - transaction. -- Easy to submit a transaction before attaching the last operation. +I found this let me build up a catalog of useful input components and +validation rules, letting me express the full range of form values. By +building custom yup validators for data structures specific to this +service, I'm confident I'll be able to keep this up-to-date with minimal +effort as changes occur. -I adjusted the behavior in 2 ways in response. I changed operations to -display as text (rather than inputs) after being attached, with a button -to change to an edit mode. I also blocked submission of the main form -while a sub-form was being edited. These changes dramatically cut the -number of errors I made while manually testing. I wanted to call out -these problems specifically because they seemed like likely problems -when following this pattern. +There are 2 parts to rendering this: gathering up validation rules, and +rendering the inputs. I wrote a helper function to extract the input +name and validation rule to build a yup schema, putting it on a `ref` in +my form component. With my `render` function, I can return +`fields.map(({render}) => render())` # Lists of inputs @@ -127,47 +99,46 @@ removing existing elements all add logic. Formik, luckily, provides a utility specifically to help in this case; `FieldArray`. It provides [a number of typical array methods](https://jaredpalmer.com/formik/docs/api/fieldarray#fieldarray-helpers), -which made it trivial to handle this situation that I had previously -found so frustrating. +which went a long way in helping with this situation that I had +previously found so frustrating. + +I found I could get away with only using `push` and `remove`. Formik +understands dot notation in input names, so it can automatically wire up +`Field`s if you provide the name. ```js ( -
- {values.signers.map((signer, i) => ( + name="someArrayField" + render={({ push, remove }) => ( + <> + {values.someArrayField.map((signer, i) => ( <> - replace(i, e.target.value)} - // Formik is smart enough to understand indexes in input names. - name={`signers.${i}`} - value={signer} - /> + ))} -
+ )} /> ``` -There are many other array utilities, but these three simple ones were -sufficient for my needs. My final code grew much more complex as I -fine-tuned the UX I wanted to offer the user (largely discovered through -mistakes I found myself making), but the core of it is quite simple. +The only struggle I had here, really, was how to present the add and +remove interactions to the user. Formik made it so easy for me to +implement the basic functionality that I had some extra time and +motivation to play with different experiences. # Lists of many different complex inputs -Happily, Formik provides such fantastic handling of input names when -determining changes, I found no additional complexity when the lists -were made of complex objects. +Formik provides such fantastic handling of input names when determining +changes, I found no additional complexity when the lists were made of +complex objects, as in the case of the operations sub-forms. In the real +app, each of these form bodies were constructed from the object +described above. ```js {values.operations.map((operation, i) => ( <> - replace(i, e.target.value)} - // field1 - name={`operations.${i}.field1`} - value={operation} - /> - replace(i, e.target.value)} - // field2 - name={`operations.${i}.field2`} - value={operation} - /> + + ))}