Getting started
Table of Contents
- Getting started
- Creating forms
- TypeScript support
- State management
- Async form handling
- Field components
- Value components
- Conditionally display content
- Async form behavior
- Validation and error handling
- Conditionally display messages
- Country specific validation
- Localization and translation
- Layout
- Best practices
- Debugging
- Create your own component
Quick start
Here's how you import the components from within scopes, such as Form and Field:
import { Form, Field } from '@dnb/eufemia/extensions/forms'
Field components can be used directly as they are, for example Field.Email:
import { Field } from '@dnb/eufemia/extensions/forms'render(<Field.Email />)
NB: In the above example, only the email field will be a part of your application bundle. Unused code will be tree-shaken away.
And here is how you can use the Form component:
Creating forms
To build an entire form, there are surrounding components such as form Form.Handler and Wizard.Container that make data flow and layout easier and save you a lot of extra code, without compromising flexibility.
The needed styles are included in the Eufemia core package via dnb-ui-components.
TypeScript support
You can define the TypeScript type structure for your form data. This will help you to get better code completion and type checking.
NB: Use type instead of interface for the type definition.
import { Form, OnSubmit } from '@dnb/eufemia/extensions/forms'type MyData = {firstName?: string}const submitHandler: OnSubmit<MyData> = (data) => {console.log(data.firstName satisfies string)}function MyForm() {return (<Form.Handler<MyData> onSubmit={submitHandler}><MyComponent /></Form.Handler>)}const MyComponent = () => {const { data } = Form.useData<MyData>()return data.firstName}
Read more about TypeScript support and the other methods in the Form.Handler section or in the Form.useData hook docs.
State management
While you can use a controlled method of handling the sate of fields or your form, it is recommended to use a declarative approach, where you keep the state of your form inside the data context, instead of pulling it out via your own useState hooks (imperative).
- You don't need React useState to handle your form data.
So how does that work?
Lets say you have a form with a name field. In order to tell e.g. the onSubmit event what the data structure is, you define a declaration (path) about it right on the field itself:
<Field.String path="/name" />
Why call it path?
Because it can describe the structure of the data in several layers deep:
<Field.String path="/user/name" />
Which results in the following data structure:
{user: {name: 'Hanna'}}
Paths of fields (path="/dataPath") must be unique to ensure proper error handling and data input. If Eufemia detects duplicate paths, it will print the following warning in the console: Eufemia Path declared multiple times: /duplicatePath.
This is called a JSON Pointer, which is a standardized way of pointing to a specific part of a JavaScript/JSON object.
These JSON pointers are used to both read and write data, and is also used to validate the form data.
What is a JSON Pointer?
A JSON Pointer is a string of tokens separated by / characters, these tokens either specify keys in objects or indexes in arrays.
const data = {foo: {bar: [{baz: 'value',},],},}const pointer = '/foo/bar/0/baz' // points to 'value'
Data handling
How do I handle complex data logic?
You can show or hide parts of your form based on your own logic. This is done by using the Form.Visibility component (yes, it even can animate the visibility).
And you can access and modify your form data with Form.useData or Form.getData or even Form.setData.
Here is an example of how to use these methods:
import { Form } from '@dnb/eufemia/extensions/forms'const myFormId = 'unique-id' // or a function, object or React Context referencefunction MyForm() {return (<Form.Handler id={myFormId}><MyComponent /></Form.Handler>)}function MyComponent() {const {getValue,update,remove,set,data,filterData,reduceToVisibleFields,} = Form.useData() // optionally provide an id or reference}// You can also use the setData:Form.setData(myFormId, { companyName: 'DNB' })// ... and the getData – method whenever you need to:const { getValue, data, filterData, reduceToVisibleFields } =Form.getData(myFormId)
getValuewill return the value of the given path.updatewill update the value of the given path.removewill remove the given path from the data context (fields will reapply their values afterwards).setwill set the whole dataset.datawill return the whole dataset (unvalidated).filterDatawill filter the data based on your own logic.reduceToVisibleFieldswill reduce the given data set to only contain the visible fields (mounted fields).
As you can see in the code above, you can even handle the state outside the Form.Handler context. You find more details on this topic in the Form.useData documentation.
Filter data
In this section we will show how to filter out some data based on your own logic. You can filter data by any given criteria.
Concepts of filtering data
There are many ways to utilize filterData, here are some concepts to exclude a data entry:
- When a field is disabled.
- When a field is hidden.
- When a field has an error.
- When a field has a value that matches a given criteria.
- When a field has a path that starts with e.g.
_.
Here is an example of how to filter data when a path starts with _:
const filterDataHandler = ({ path }) => {if (/\/_/.test(path)) {// When returning false, the data entry will be left outreturn false}}render(<Form.HandlerdefaultData={{foo: 'foo',_bar: 'bar', // Will be excluded}}onSubmit={(data, { filterData }) => {const filteredData = filterData(filterDataHandler)console.log(filteredData) // Will be { foo: 'foo' }}}><MyComponent /></Form.Handler>,)
How do I use it?
You can utilizing the filterData method in:
- Form.Handler component.
- Form.useData hook.
- Form.getData method.
- Form.Visibility component.
You can provide either an object with the paths (1) or a handler (2).
Here is an example of how to filter data outlining the different ways to filter data:
// 1. Method – by using paths (JSON Pointer)const filterDataPaths = {'/foo': ({ value, data, props, error }) => value !== 'bar','/foo': false, // Will exclude the field with the path "/foo"'/array/0': false, // Will exclude the first item of the array'/array/*/objectKey': false, // Will exclude all objects inside the array with a key of "objectKey"}// 2. Method – by using a handler.// It will iterate over all fields and data entries regardless what path they have.const filterDataHandler = ({ path, value, data, props, error }) => {if (/\/_/.test(path)) {// When returning false, the data entry will be left outreturn false}}const MyComponent = () => {const { filterData } = Form.useData()const filteredDataA = filterData(filterDataPaths)const filteredDataB = filterData(filterDataHandler)console.log(filteredDataA)console.log(filteredDataB)return <Field.String path="/foo" />}render(<Form.Handler><MyComponent /></Form.Handler>,)
You may check out an interactive example of how to filter data.
Filter data during submit
When submitting data to the server, you might want to exclude data that has been hidden (unmounted) by the user. You have two built-in options to achieve this:
User entered data will always be stored internally in the data context, even if a Field is temporarily shown (mounted/unmounted).
The reduceToVisibleFields method
You can use the reduceToVisibleFields function to get only the data of visible (mounted) fields. Check out the example in the demo section.
The filterData method
For filtering data during form submit you can use the filterData method given as a parameter to the onSubmit (Form.Handler) event callback:
render(<Form.HandleronSubmit={(data, { filterData }) => {// Same method as in the previous exampleconst filteredDataA = filterData(filterDataPaths)const filteredDataB = filterData(filterDataHandler)console.log(filteredDataA)console.log(filteredDataB)}}><Field.String path="/foo" /></Form.Handler>,)
You may check out an interactive example of how to filter data during the submit event.
Transforming data
Each Field.* and Value.* component supports transformer functions.
These functions allow you to manipulate the field value to a different format than it uses internally and vice versa.
<Field.Stringpath="/myField"transformOut={transformOut}transformIn={transformIn}/>
transformOut(out of theField.*component) transforms the internal value before it gets forwarded to the data context or returned as e.g. theonChangevalue parameter.transformIn(in to theField.*orValue.*component) transforms the external value before it is displayed and used internally.
Complex objects in the data context
If you need to store complex objects in the data context instead of simple values like strings, numbers, or booleans, you can use transformer functions.
Suppose you want to store a country object instead of just a country code like NO or SV when using Field.SelectCountry.
You can achieve this by using the transformIn and transformOut functions:
import { Field } from '@dnb/eufemia/extensions/forms'import type { CountryType } from '@dnb/eufemia/extensions/forms/Field/SelectCountry'// From the Field (internal value) to the data context or event parameterconst transformOut = (internal, country) => {if (internal) {return country}}// To the Field (from e.g. defaultValue)const transformIn = (external: CountryType) => {return external?.iso || 'NO'}const MyForm = () => {return (<Form.Handler><Field.SelectCountrypath="/country"transformIn={transformIn}transformOut={transformOut}defaultValue="NO"/><Value.SelectCountry path="/country" transformIn={transformIn} /></Form.Handler>)}
Async form handling
It depends on your use case if this feature is needed. But when it is, it's often a time consuming task to implement. Eufemia Forms has therefor a built-in feature that enables async form behavior.
More details about the async form behavior can be found in the async form behavior section.
Field components
In short, field components are interactive components that the user can interact with. Read more about fields in the What are fields? section.
Value components
Beside the interactive Field components, there is also the static Value components. Use these to show summaries or read-only parts of your application with benefits such as linking to source data and standardized formatting based on the type of data to be displayed.
Inherit visibility from fields
User entered data will always be stored internally in the data context, even if a Field is temporarily shown (mounted/unmounted).
Value.* components will render the value regardless of the visibility of the field.
You can use the inheritVisibility property on the Value.* components to inherit the visibility from the field with the same path.
Conditionally display content
You can conditionally display content using the Form.Visibility component. This allows you to show or hide its children based on the validation (A.) or the value (B.) of another path (data entry).
<Form.VisibilityanimatevisibleWhen={{path: '/myField',hasValue: (value) => value === 'foo', // A. Value basedisValid: true, // B. Validation based}}><Field.String path="/myField" /></Form.Visibility>
Async form behavior
This feature allows you to perform asynchronous operations such as fetching data from an API – without additional state management.
You can enable async form submit behavior on the form Form.Handler by using:
<Form.Handler onSubmit={async () => {}}>...</Form.Handler>
It will disable all fields and show an indicator on the Form.SubmitButton while the form is pending (examples).
When using Wizard.Container you can use in addition:
<Wizard.Container onStepChange={async () => {}}>...</Wizard.Container>
It will disable all fields and show an indicator on the Wizard.NextButton while the step is pending (examples).
onChange and autosave
You can use an async function for the onChange event handler, either on the form Form.Handler:
<Form.Handler onChange={async () => {}}>...</Form.Handler>
or on every field:
<Field.PhoneNumber path="/myField" onChange={async () => {}} />
They can be used in combination as well – including async validator functions.
When the user makes a value change, it will show an indicator on the corresponding field label.
This feature cannot only be used for autosave, but for any other real-time async operations.
Here is an example of an async change behavior:
// Async event handlerconst onChange = debounceAsync(async function (data) {try {await makeRequest(data)} catch (error) {return error}// Optionally, you can return an object with these keys, depending your needsreturn {info: 'Info message',warning: 'Warning message',// and either an errorerror: new Error('Error message'),// or success (when used for autosave)success: 'saved',} as const})
More info about the async change behavior in the form Form.Handler section.
Async field validation
A similar indicator behavior will occur when using async functions for field validation, such as onChangeValidator or onBlurValidation, your form will exhibit async behavior. This means that the validation needs to be successfully completed before the form can be submitted.
Validation and error handling
Every field component has a built-in validation that is based on the type of data it handles. This validation is automatically applied to the field when the user interacts with it. The validation is also applied when the user submits the form.
In addition, you can add your own validation to a field component. This is done by adding a required, pattern, schema, onChangeValidator or onBlurValidation property.
Fields which have the disabled property or the readOnly property, will skip validation.
For monitoring and setting your form errors, you can use the useValidation hook.
Validation and the user experience (UX)
Eufemia Forms provides a built-in validation system that is based on the type of data it handles. This validation is automatically applied to the field when the user interacts with it. The validation is also applied when the user submits the form.
In general, the validation is based on the following principles:
- Trust the user and assume they are doing their best.
- Avoid interrupting the user while they are entering data.
- Provide clear and concise error messages, ending with a period.
The validation system is flexible and can be customized to fit your specific needs. You can add your own validation to a field component by using the required, pattern, schema, onChangeValidator or onBlurValidation property.
Validation order
If there are multiple errors, they will be shown in a specific order, and only the first error in that order will appear.
The validation order is as follows:
requirederror(withconditionally)schema(includingpattern)onChangeValidatoronBlurValidator
Error messages
Eufemia Forms comes with built-in error messages. But you can also customize and override these messages by using the errorMessages property both on fields (field level) and on the Form.Handler (global level).
You may use the errorMessages property for two purposes:
- Provide your onw error messages.
- Overwrite the default error messages.
Both can be done on a global level or on a field level.
How ever, for when overwriting the default error messages on a global level, you can also use internationalization (i18n).
Read more about error messages.
Summary for errors
To improve user experience communication regarding errors and their locations, WCAG/UU suggests summarizing error messages when errors have occurred.
Eufemia Forms will easily link up with the GlobalStatus component and will only display it if there are errors or when the form is being submitted.
<GlobalStatus /><Form.Handler>My Form</Form.Handler>
If you need a custom unique ID, you can just assign globalStatusId to the Form.Handler:
<GlobalStatus id="my-form-status" /><Form.Handler globalStatusId="my-form-status">My Form</Form.Handler>
required
The required property is a boolean that indicates whether the field is required or not:
<Field.PhoneNumber required />
Note: You can use the required property on the Form.Handler or Wizard.Step components (example). Additionally, the Form.Section component as well as the Field.Provider provider has a required property, which will make all nested fields within that section required.
<Form.Handler required><Field.PhoneNumber /><Field.String /></Form.Handler>
When you need to opt-out of the required field validation, you can use the required={false} property. This will also add a "(optional)" suffix to the field label(labelSuffix).
<Form.Handler required><Field.String label="I'm required" /><Field.String label="I'm not" required={false} /><Field.Email required={false} labelSuffix="(recommended)" /></Form.Handler>
pattern
The pattern property is a regular expression (RegExp) that the value of the field must match:
<Field.PhoneNumber pattern="Your RegExp" />
schema
The schema property is a schema that the value of the field must match in order to be valid. You can use either a Zod schema or a JSON Schema (Ajv) schema:
Using Zod schemas
import { Form, Field, z } from '@dnb/eufemia/extensions/forms'const schema = z.string().min(5)<Form.Handler><Field.String schema={schema} /></Form.Handler>
Using JSON Schema (Ajv)
NB: An Ajv instance is required on the Form.Handler.
import {Form,Field,makeAjvInstance,} from '@dnb/eufemia/extensions/forms'const ajv = makeAjvInstance()const schema = {/* Ajv Schema */}render(<Form.Handler ajvInstance={ajv}><Field.PhoneNumber schema={schema} /></Form.Handler>,)
onBlurValidator and onChangeValidator
The onBlurValidator and onChangeValidator properties accepts a function that takes the current value of the field as an argument and returns an error message if the value is invalid:
const onChangeValidator = (value) => {const isInvalid = new RegExp('Your RegExp').test(value)if (isInvalid) {return new Error('Invalid value message')}}render(<Field.PhoneNumber onChangeValidator={onChangeValidator} />)
You can find more info about error messages in the error messages docs.
Connect with another field
You can also use the connectWithPath (or connectWithItemPath for within Iterate.Array) function to connect the validator (onChangeValidator and onBlurValidator) to another field. This allows you to rerun the validator function once the value of the connected field changes:
import { Form, Field } from '@dnb/eufemia/extensions/forms'const onChangeValidator = (value, { connectWithPath }) => {const { getValue } = connectWithPath('/myReference')const amount = getValue()if (amount >= value) {return new Error(`The amount should be greater than ${amount}`)}}render(<Form.Handler><Field.Number path="/myReference" defaultValue={2} /><Field.Numberpath="/withOnChangeValidator"defaultValue={2}onChangeValidator={onChangeValidator} // NB: You may use "onBlurValidator" depending on use case./></Form.Handler>,)
By default, the onChangeValidator function will only run when the "/withOnChangeValidator" field is changed. When the error message is shown, it will update the message with the new value of the "/myReference" field.
You can also change this behavior for testing purposes by using the following properties:
validateInitiallywill run the validation initially.validateContinuouslywill run the validation on every change, including when the connected field changes.validateUnchangedwill validate without any changes made by the user, including when the connected field changes.
Async validation
Async validation is also supported. The validator function can return a promise (async/await) that resolves to an error message.
In this example we use onBlurValidator to only validate the field when the user leaves the field:
const onChangeValidator = async (value) => {try {const isInvalid = await makeRequest(value)if (isInvalid) {return new Error('Invalid value') // Show this message below the field}} catch (error) {return error}}render(<Field.PhoneNumber onBlurValidator={onChangeValidator} />)
Async validator with debounce
While when using async validation on every keystroke, it's a good idea to debounce the validation function to avoid unnecessary requests. This can be done by using the debounceAsync helper function:
import { debounceAsync } from '@dnb/eufemia/shared/helpers'const onChangeValidator = debounceAsync(async function myValidator(value) {try {const isInvalid = await makeRequest(value)if (isInvalid) {return new Error('Invalid value') // Show this message below the field}} catch (error) {return error}})render(<Field.PhoneNumber onChangeValidator={onChangeValidator} />)
error
The error property is a controlled error message that is always displayed by default.
render(<Field.String error={new Error('Invalid value')} />)
You can also provide the error message as a string:
render(<Field.String error="Invalid value" />)
Or as a React.Element:
render(<Field.Stringerror={<>A <strong>formatted</strong> error message.</>}/>,)
As well as an array with multiple error messages:
render(<Field.Stringerror={['First error message',<>Second <strong>formatted</strong> error message.</>,new Error('Third error message'),new FormError('fourth.message'),new FormError('fifth.message'),]}errorMessages={{'fourth.message': 'Fourth error message','fifth.message': (<>Fifth <strong>formatted</strong> error message.</>),}}/>,)
Conditionally display messages
You can provide a function to the info, warning and error properties to conditionally change this behavior based on specific requirements.
Using this conditional function, the error message (or info or warning message) will only be shown after the user interacts with the field — for instance, after they change its value and move focus away (blur). This allows you to display error messages dynamically, ensuring they appear only when the field's value is invalid and the user has engaged with it.
render(<Field.Numbererror={(value, { conditionally }) => {if (value === 123) {return conditionally(() => new Error('Invalid value'))}}}/>,)
More info about it in the Field.Number component.
Country specific validation
You can change the country used for your form by setting the countryCode property on the Form.Handler component.
Fields that support the countryCode property will use the country code from the data context and adjust their behavior accordingly. Read more about the specific behavior changes in their respective documentation.
Localization and translation
You can set the locale for your form by using the locale property on the Form.Handler component. This will ensure that the correct language is used for all the fields in your form.
import { Form } from '@dnb/eufemia/extensions/forms'function MyForm() {return (<Form.Handler locale="en-GB"><Field.PhoneNumber /></Form.Handler>)}
Translations provided by Eufemia
Eufemia provides forms translations for the following locales:
Add additional translations
import { mergeTranslations } from '@dnb/eufemia/shared'import { Form } from '@dnb/eufemia/src/extensions/forms'import svSE_forms from '@dnb/eufemia/extensions/forms/constants/locales/sv-SE'import svSE_forms_countries from '@dnb/eufemia/extensions/forms/constants/locales/countries/sv-SE'// or import danish translations, like so:import daDK_forms from '@dnb/eufemia/extensions/forms/constants/locales/da-DK'import daDK_forms_countries from '@dnb/eufemia/extensions/forms/constants/locales/countries/da-DK'const translations = mergeTranslations(svSE_forms, svSE_forms_countries)render(<Form.Handler translations={translations} locale="sv-SE">Your form</Form.Handler>,)
Instead of providing the forms translations per form, you can also provide them globally using the Provider component.
If so, you can use the mergeTranslations function from @dnb/eufemia/shared to merge the forms translations with the shared translations like this:
import { Provider, mergeTranslations } from '@dnb/eufemia/shared'import svSE from '@dnb/eufemia/shared/locales/sv-SE'import svSE_forms from '@dnb/eufemia/extensions/forms/constants/locales/sv-SE'const translations = mergeTranslations(svSE, svSE_forms)render(<Provider translations={translations} locale="sv-SE">Your app, including Eufemia Forms</Provider>,)
Customize translations
You can customize the internal translations in a flat structure (dot-notation):
{'nb-NO': { 'PhoneNumber.label': 'Egendefinert' },'en-GB': { 'PhoneNumber.label': 'Custom' },}
or an object based structure:
{'nb-NO': { PhoneNumber: { label: 'Egendefinert' } },'en-GB': { PhoneNumber: { label: 'Custom' } },}
How to customize translations in a form
You can customize the translations in a form by using the translations property on the Form.Handler.
Alternatively, you can use the global Eufemia Provider.
You can find all available strings and keys in the properties tab of each field or value component. For example, check out the PhoneNumber field properties.
import { Form, Field } from '@dnb/eufemia/extensions/forms'const myTranslations = {'nb-NO': { 'PhoneNumber.label': 'Egendefinert' },'en-GB': { 'PhoneNumber.label': 'Custom' },}render(<Form.Handler translations={myTranslations}><Field.PhoneNumber /></Form.Handler>,)
Consume the translations
You can consume both the internal or your own translations with the Form.useTranslation hook:
import { Form } from '@dnb/eufemia/extensions/forms'const myTranslations = {'nb-NO': { myString: 'Min egendefinerte streng' },'en-GB': {// Cascaded translationsNested: {stringWithArgs: 'My custom string with an argument: {myKey}',},// Flat translations'Nested.stringWithLinebreaks':'My custom string with a {br}line-break',},}const MyComponent = () => {const t = Form.useTranslation<typeof myTranslations>()// Internal translationsconst existingString = t.Field.errorRequired// Your translationsconst myString = t.myString// Use the "formatMessage" function to handle strings with argumentsconst myStringWithArgsA = t.formatMessage(t.Nested.stringWithArgs, {myKey: 'myValue',})// You can also get the string with a key (dot-notation)const myStringWithArgsB = t.formatMessage('Nested.stringWithArgs', {myKey: 'myValue',})// Render line-breaksconst jsxOutput = t.renderMessage(t.Nested.stringWithLinebreaks)return <>MyComponent</>}render(<Form.Handler translations={myTranslations}><MyComponent /></Form.Handler>,)
Here is a demo of how to use the translations in a form.
When creating your own field, you can use the Form.useTranslation hook to localize your field.
Layout
When building your application forms, preferably use the following layout components. They seamlessly places all the fields and components of Eufemia Forms correctly into place.
- Flex.Stack layout component for easy and consistent application forms.
- Form.Card with the stack property
<Form.Card>...</Form.Card>for the default card outline of forms. - Form.Appearance for changing sizes (height) of e.g. input fields.
Best practices
Debugging
Logging
You can use the Tools.Log component to log data to the console. The whole data context will be logged.
import { Tools } from '@dnb/eufemia/src/extensions/forms'function MyForm() {return (<Form.Handler><Tools.Log /><Form.Isolation><Tools.Log /></Form.Isolation></Form.Handler>)}
You can also provide your own data to the Tools.Log component by using the data property.
Finding errors
In order to debug your forms, you can use the Tools.Errors component. It will render a list of all errors in your form, including the field path and the error message.
fieldErrorswill render all field errors.formErrorswill render all form errors initiated by e.g.schema.
import { Tools } from '@dnb/eufemia/src/extensions/forms'function MyForm() {return (<Form.Handler><Tools.Errors /></Form.Handler>)}
Create your own component
Eufemia Forms consists of helper components and tools so you can declaratively create interactive form components that flawlessly integrates between existing data and your custom form components. This ensures a common look and feel, even when ready-made components are combined with your local custom components.
Read more about creating your own component