Import
import { Form } from '@dnb/eufemia/extensions/forms'render(<Form.Section />)
Description
Form.Section lets you compose together sections of fields and values to be reused in different contexts.
Relevant links
Good to know
- It allows you to easily customize and reposition nested
Form.Sectioncomponents within the data context, making it effortless to reuse the same set of fields and values in various contexts. Check out theoverwritePropsandpathproperties for more information. - When defining a default value for a field or value, you can use the
defaultValueproperty instead ofvalue. It will not take precedence over the data context likevaluedoes. - Use Form.Visibility to handle logic and hide parts of your form based on the data context.
- To provide localization for all used texts defined in the
translationsproperty of the section, you can create a localization easily. - Only imported sections with their localizations are included in the production bundle.
- It is possible to overwrite the translations later by using the
Form.Handlercomponent. - A single section can be used without
Form.Handler, just like any other field and value. - Form.Section.EditContainer and Form.Section.ViewContainer containers are available to use.
- Here is a list of blocks ready to use.
- You can use
//at the start of a field path to access data from the root of the form data context, bypassing the section path. For example,path="//rootField"inside a section at/sectionwill access/rootFieldinstead of/section/rootField. - You can use
../to reference parent section paths. Each../moves one section up before appending the rest of the path, letting you read or write sibling fields such aspath="../targetField".
Usage
- Create the section component and export it:
import { Form, Field } from '@dnb/eufemia/extensions/forms'export function MySection(props) {return (<Form.Section {...props}>{/* Fields*/}<Field.PhoneNumber path="/phoneNumber" required />{/* Views*/}<View.PhoneNumber path="/phoneNumber" /></Form.Section>)}
- Import the section component and use it in a form:
import { Form } from '@dnb/eufemia/extensions/forms'import { MySection } from './form-sections'function MyForm() {return (<Form.Handler><MySection /></Form.Handler>)}
Overwrite properties
It lets you overwrite all of the given properties if needed by using overwriteProps:
import { Form, Field } from '@dnb/eufemia/extensions/forms'const MySection = (props) => {return (<Form.Section {...props}><Field.Name.First path="/firstName" /><Field.Name.Last path="/lastName" required minLength={2} /></Form.Section>)}render(<Form.Handler><MySectionoverwriteProps={{firstName: { label: '/Custom label' },lastName: { required: false, minLength: 0 },}}/></Form.Handler>,)
Optional path support
You can optionally define a path for the section component. Fields inside the section will use this path as a prefix for their own path. This makes it possible to reuse the same section component in different contexts.
import { Form, Field } from '@dnb/eufemia/extensions/forms'const MySection = (props) => {return (<Form.Section {...props}><Field.Name.First path="/firstName" /><Field.Name.Last path="/lastName" /></Form.Section>)}render(<Form.HandlerdefaultData={{// MySection has a path="/nestedPath" and therefore it is nested in the data contextnestedPath: {firstName: 'Nora',},}}><MySection path="/nestedPath" /></Form.Handler>,)
Accessing root data with //
When a field is inside a Form.Section, you can use a path starting with // to access data from the root of the form data context, bypassing the section path. This is useful when you need to reference data outside the current section.
import { Form, Field } from '@dnb/eufemia/extensions/forms'const MySection = (props) => {return (<Form.Section {...props}>{/* This field accesses data at /sectionField relative to the section */}<Field.String path="/sectionField" />{/* This field accesses data at /rootField from the root, ignoring the section path */}<Field.String path="//rootField" />{/* You can also access nested root paths */}<Field.String path="//user/profile/name" /></Form.Section>)}render(<Form.HandlerdefaultData={{rootField: 'Root value',user: {profile: {name: 'John Doe',},},section: {sectionField: 'Section value',},}}><MySection path="/section" /></Form.Handler>,)
In the example above:
- The field with
path="/sectionField"will access/section/sectionField - The field with
path="//rootField"will access/rootField(root level) - The field with
path="//user/profile/name"will access/user/profile/name(root level)
This feature works even with nested sections - // always refers to the root of the form data context.
If you want every field inside a nested Form.Section to bypass the parent path, set the section path to start with // as well (for example path="//global" or path="//"). This works like protocol-relative URLs in HTML and resets the section context back to the root.
Accessing parent data with ../
When a section is nested inside another section, you can use a path starting with ../ to access data relative to the parent section. Each ../ moves one level up the section hierarchy before the remainder of the path is appended.
import { Form, Field } from '@dnb/eufemia/extensions/forms'const AddressSection = (props) => {return (<Form.Section {...props}><Field.String path="/street" label="Street" /><Field.String path="/postalCode" label="Postal code" /><Form.Section path="/verification">{/* Reads and writes the parent postal code */}<Field.String path="../postalCode" label="Confirm postal code" />{/* Goes two levels up (Form.Handler root) */}<Field.String path="../../customerId" label="Customer ID" /></Form.Section></Form.Section>)}render(<Form.HandlerdefaultData={{customerId: 'ABC-123',address: {street: 'Example street 1',postalCode: '0123',verification: {},},}}><AddressSection path="/address" /></Form.Handler>,)
In the example above:
path="../postalCode"resolves to/address/postalCode, enabling fields inside/address/verificationto work with the parent value.path="../../customerId"resolves to/customerId, since the section is nested two levels deep.- When you chain more
../segments than there are parent sections, the path simply starts from the root.
This makes it easy to build advanced sections where nested parts reuse sibling values without duplicating props.
Required property
You can optionally define a required property for the section component. Fields inside the section will then be required.
render(<Form.Section required={true}><Field.Name.First path="/firstName" /><Field.Name.Last path="/lastName" /></Form.Section>,)
TypeScript support
You can optionally define which properties the nested fields should accept:
import { Form, Field, SectionProps } from '@dnb/eufemia/extensions/forms'import type { Props as PhoneNumberProps } from '@dnb/eufemia/extensions/forms/Field/PhoneNumber'const MySection = (props: SectionProps<{ phoneNumber?: PhoneNumberProps }>,) => {return (<Form.Section {...props}><Field.Name.Last path="/phoneNumber" required /></Form.Section>)}render(<Form.Handler><MySection /></Form.Handler>,)
Schema support
You can also use a schema on the Form.Handler to define the validation rules for the fields inside the section.
Using Zod:
import { Form, Field, z } from '@dnb/eufemia/extensions/forms'const MySection = (props) => {return (<Form.Section {...props}><Field.PhoneNumber path="/phoneNumber" /></Form.Section>)}const formSchema = z.object({mySection: z.object({phoneNumber: z.string().regex(/^[0-9]{10}$/),}),})function MyForm() {return (<Form.Handler schema={formSchema}><MySection path="/mySection" /></Form.Handler>)}
Using JSON (Ajv):
import {Form,Field,JSONSchema,makeAjvInstance,} from '@dnb/eufemia/extensions/forms'const ajv = makeAjvInstance()const MySection = (props) => {return (<Form.Section {...props}><Field.PhoneNumber path="/phoneNumber" /></Form.Section>)}const formSchema: JSONSchema = {type: 'object',properties: {mySection: {type: 'object',properties: {phoneNumber: {type: 'string',pattern: '^[0-9]{10}$',},},required: ['phoneNumber'],},},}function MyForm() {return (<Form.Handler schema={formSchema} ajvInstance={ajv}><MySection path="/mySection" /></Form.Handler>)}
Section-level schema
You can define a schema directly on the Form.Section component. The handler or fields will automatically collect and validate all section schemas without requiring manual merging.
Using Zod:
import { Form, Field, z } from '@dnb/eufemia/extensions/forms'const sectionSchema = z.object({phoneNumber: z.string().regex(/^[0-9]{10}$/).optional().refine((val) => val !== undefined, {message: 'Field.errorRequired',}),})function MyForm() {return (<Form.Handler><Form.Section path="/mySection" schema={sectionSchema}><Field.PhoneNumber path="/phoneNumber" /></Form.Section></Form.Handler>)}
Using JSON (Ajv):
import {Form,Field,JSONSchema,makeAjvInstance,} from '@dnb/eufemia/extensions/forms'const sectionSchema: JSONSchema = {type: 'object',properties: {phoneNumber: {type: 'string',pattern: '^[0-9]{10}$',},},required: ['phoneNumber'],}const ajv = makeAjvInstance()function MyForm() {return (<Form.Handler ajvInstance={ajv}><Form.Section path="/mySection" schema={sectionSchema}><Field.PhoneNumber path="/phoneNumber" /></Form.Section></Form.Handler>)}
Translations
You can use the translations property to provide translations for the section:
import { Field, Form } from '@dnb/eufemia/extensions/forms'const translations = {// It's optional to wrap the translations in an additional "MySection" object'nb-NO': { MySection: { MyField: { label: 'Felt label' } } },'en-GB': { MySection: { MyField: { label: 'Field label' } } },}// For TypeScript supporttype Translation = (typeof translations)[keyof typeof translations]export function MySection() {return (<Form.Section translations={translations}><ContentOfMySection /></Form.Section>)}function ContentOfMySection() {const { MyField } = Form.useTranslation<Translation>().MySectionreturn <Field.String label={MyField.label} path="/custom" />}
This way it is possible to extend or change the translations for a specific section from a Form.Handler:
import { Form } from '@dnb/eufemia/extensions/forms'import { MySection } from './form-sections'const myTranslations = {'nb-NO': { MySection: { MyField: { label: 'Egendefinert' } } },'en-GB': { MySection: { MyField: { label: 'Custom' } } },}export function MyForm() {return (<Form.Handler translations={myTranslations}><MySection /></Form.Handler>)}
Edit and View container
The Form.Section supports a Form.Section.EditContainer and a Form.Section.ViewContainer container. The edit container should be used for data input with fields, while the view container is used to display the data in a read-only manner.
import { Form, Field } from '@dnb/eufemia/extensions/forms'function MyEditContainer() {return (<Form.Section.EditContainer><Field.Name.First path="/firstName" /><Field.Name.Last path="/lastName" /></Form.Section.EditContainer>)}function MyViewContainer() {return (<Form.Section.EditContainer><Value.Name.First path="/firstName" /><Value.Name.Last path="/lastName" /></Form.Section.EditContainer>)}function MySection() {return (<Form.Section><MyEditContainer /><MyViewContainer /></Form.Section>)}render(<Form.Handler><MySection /><Form.SubmitButton /></Form.Handler>,)
Note: The reason Done is used in the toolbar button instead of Save is because validation is performed in fields. If we decouple the data entered in a section, the Form.Handler will not be able to validate the data on submit. This can also lead to missing data because the user can press the submit button before Save is pressed.
Snapshot testing of a section (block)
One way to ensure a "contract" of what you expect a section to be outlined as, is to create a snapshot test:
import { Form, Tools } from '@dnb/eufemia/extensions/forms'import { GenerateRef } from '@dnb/eufemia/extensions/forms/Tools/ListAllProps'it('MySection should match snapshot', () => {const generateRef = React.createRef<GenerateRef>()render(<Form.Handler><Tools.ListAllProps generateRef={generateRef}><MySection /></Tools.ListAllProps></Form.Handler>,)const { propsOfFields, propsOfValues } = generateRef.current()expect(propsOfFields).toMatchInlineSnapshot(`...`)expect(propsOfValues).toMatchInlineSnapshot(`...`)})