Description
If you don't want to repeat all the logic that drills down to values in the source data, and ensure that changes are sent to the right place, you can surround the components with a DataContext.Provider component. This means that you feed the form with source data in one place, and give it only one onChange
callback. Then you only send the individual fields instructions about where in the data set the value that field is to process is located. The components then communicate internally and ensure that the values are retrieved and sent to the correct location.
The reference to a specific field's value in the dataset is given with a prop called path
. Paths are defined in a syntax called JSON Pointer, which is basically a slash-separated string that can go several levels, and consist of both object-properties and array indexes. Examples of paths are: /firstName
, /nested/path/to/value
and /list/2/keyInThirdObject
. More information about JSON Pointers can be found on the website of JSON Schema.
In practice, this means that you can go from:
const handleChange = useCallback((path, value) => {// Update external state})return (<div id="my-form"><Field.FirstNamevalue={data.firstName}onChange={(value) => handleChange('firstName', value)}/><Field.LastNamevalue={data.lastName}onChange={(value) => handleChange('lastName', value)}/><Field.Emailvalue={data.email}onChange={(value) => handleChange('email', value)}/><Field.Stringlabel="Special non-standardized value"value={data.specialValue}onChange={(value) => handleChange('specialValue', value)}/></div>)
to:
const handleChange = useCallback((path, value) => {// Update external state})return (<DataContext.Provider data={data} onChange={handleChange}><Field.FirstName path="/firstName" /><Field.LastName path="/lastName" /><Field.Email path="/email" /><Field.Stringpath="/specialValue"label="Special non-standardized value"/></DataContext.Provider>)
This abstracts away some logic that many are used to having available for debugging and adjustments, which can be unfamiliar and difficult to get used to. The goal of the way this is designed is for it to be well tested and predictable, so that you don't need to have this boilerplate logic available. In addition, props from the individual components make them flexible in use, and this can be continuously expanded to cover recurring needs from implementations.
Error handling
Besides how the forms are built up and the link to the surrounding data flow, these form components must ensure that the user experience is as much as possible in line with the way we have defined that it should work in practice. An example of this is when the error messages appear on the screen. Both the individual input component and any surrounding DataContext.Provider
component hold an internal state that says whether the value in the field has an error or not. In addition, it has a separate state that states whether error messages should be displayed or not.
An example of what this leads to is when a field has an invalid value, for example because the field starts empty but is required. Or if the field requires a given syntax (such as national identity number), then the error message is not displayed before or at the same time as the user fills in the field in question. However, when the user jumps out of the field, the error message will appear if the value is still not valid based on the validation props the component has received. When the user then starts to adjust the field in question, the error message is hidden again until they jump out of the field. In addition, a surrounding DataContext.Provider
will check all the fields for errors, so that you do not get to the next step in a step-divided form, or can send the form and trigger onSubmit
if there are still fields on the screen that have errors.
In the case of forms divided into several steps, the combination of the components DataContext.Provider
and StepsLayout.Steps
will also ensure that only the fields that are visible on the screen (for the relevant step, or based on what is hidden or shown via the Visibility component) provide a basis for whether one can proceed in the process or not.
Hierarchically overridable properties
Configuration of the form functionality through props for all components can be hierarchically overridden. This means that the further into the component structure you get, the higher priority props have. For example, a component that is given a path
to retrieve data from the DataContext.Provider
will rather prioritize a value
prop that the component receives directly if both parts are available:
<DataContext.Provider data={{ foo: 'I am the chosen one!' }}><Value.String path="/foo" /></DataContext.Provider>
<DataContext.Provider data={{ foo: 'I am not chosen :-(' }}><Value.String path="/foo" value="I am the one!" /></DataContext.Provider>
In the same way, components that have text properties built in, such as field label and error message for required field on Field.Email
, will choose what it receives instead of the default values if both are available:
<Field.Email />// Gets the default label, and the email-pattern validation.
<Field.Email label="Send me e-mail on this address" />// Gets the custom label, but still the default email-pattern validation.
Components
At
`DataContext.At` makes it possible to dig into a data set to set a pointer as the root for sub components, as well as iterate array-data
Context
The context object used in `DataContext.Provider`.
Provider
`DataContext.Provider` is the context provider that has to wrap the features if components of Field and Value is to be used with a common source instead of distributing values and events individually.
SubmitButton
`DataContext.SubmitButton` connects to the `DataContext.Context` to submit the active state of the DataContext, triggering `onSubmit`.