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 e.g. theoverwritePropsandpathproperties for more information. - When defining a default value for a field or value, you can use the
defaultValueproperty instead of value. It will not take precedence over the data context like value does. - 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. - An Form.Section.EditContainer and Form.Section.ViewContainer container is available to be used.
- Here is a list of blocks ready to use.
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
Optionally you can define a path to the section component. Fields inside the section will get this path as a prefix of 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>,)
Required property
Optionally you can define a required property to the section component. Fields inside the section then be required.
render(<Form.Section required={true}><Field.Name.First path="/firstName" /><Field.Name.Last path="/lastName" /></Form.Section>,)
TypeScript support
Optionally you can define what the properties of 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 to define the properties of the nested fields.
Using Zod schemas
import { Form, Field, z } from '@dnb/eufemia/extensions/forms'const MySection = (props) => {return (<Form.Section {...props}><Field.PhoneNumber path="/phoneNumber" /></Form.Section>)}const schema = z.object({mySection: z.object({phoneNumber: z.string().regex(/^[0-9]{10}$/),}),})render(<Form.Handler schema={schema}><MySection path="/mySection" /></Form.Handler>,)
Using JSON Schema (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 schema: JSONSchema = {type: 'object',properties: {mySection: {type: 'object',properties: {phoneNumber: {type: 'string',pattern: '^[0-9]{10}$',},},required: ['phoneNumber'],},},}render(<Form.Handler schema={schema} ajvInstance={ajv}><MySection path="/mySection" /></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 an Form.Section.EditContainer and 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 way.
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 and not Save is used in the toolbar button is because validation is done 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(`...`)})
Demos
Without Form.Handler
<Form.Section data={{ myField: 'Value', }} onChange={console.log} > <Field.String path="/myField" /> </Form.Section>
With a nested path
This lets you reuse the same section of fields in multiple places in your forms.
const MyNameSection = (props: SectionProps) => { return ( <Form.Section {...props}> <Form.Card> <Field.Name.First path="/firstName" /> <Field.Name.Last path="/lastName" /> </Form.Card> </Form.Section> ) } render( <Form.Handler onSubmit={async (data) => console.log('onSubmit', data)} defaultData={{ nestedPath: { firstName: 'Nora', lastName: 'Mørk', }, }} > <MyNameSection path="/nestedPath" /> <Form.SubmitButton variant="send" /> </Form.Handler>, )
With a Edit and View container
This example uses the Form.Section.EditContainer and Form.Section.ViewContainer containers with the default variant="outline".
Show errors on the whole section
When a field in the section has an error and the section has containerMode set to auto (default), the whole section will switch to edit mode. The errors will be shown when validateInitially is set to true.
Using variant="basic"
Using variant="basic" will render the view and edit container without the additional Card outline.
Overwrite properties
Overwriting properties makes it very flexible to reuse the same section of fields in multiple places in your forms.
const MyNameSection = (props) => { return ( <Form.Section {...props}> <Form.Card> <Field.Composition width="large"> <Field.Name.First path="/firstName" /> <Field.Name.Last path="/lastName" required minLength={10} /> </Field.Composition> </Form.Card> </Form.Section> ) } render( <Form.Handler onSubmit={async (data) => console.log('onSubmit', data)} defaultData={{ nestedPath: { firstName: '', lastName: 'M', }, }} > <MyNameSection path="/nestedPath" overwriteProps={{ firstName: { required: true, label: 'Custom', }, lastName: { required: false, minLength: 2, }, }} /> <Form.SubmitButton variant="send" /> </Form.Handler>, )
Schema support
This feature lets you extend the requirements of the fields in the section with a JSON Schema.
const MyNameSection = (props: SectionProps) => { return ( <Form.Section {...props}> <Form.Card> <Field.Composition width="large"> <Field.Name.First path="/firstName" /> <Field.Name.Last path="/lastName" required minLength={10} /> </Field.Composition> </Form.Card> </Form.Section> ) } const mySchema: JSONSchema = { type: 'object', properties: { nestedPath: { type: 'object', properties: { firstName: { type: 'string', minLength: 3, }, lastName: { type: 'string', minLength: 2, }, }, required: ['firstName', 'lastName'], }, }, } const ajv = makeAjvInstance() render( <Form.Handler onSubmit={async (data) => console.log('onSubmit', data)} schema={mySchema} ajvInstance={ajv} defaultData={{ nestedPath: { firstName: '', lastName: 'M', }, }} > <MyNameSection path="/nestedPath" /> <Form.SubmitButton variant="send" /> </Form.Handler>, )
Required support
You can easily make a section of fields required by setting the required property on the section itself.
const MyNameSection = (props: SectionProps) => { return ( <Form.Section {...props}> <Form.Card> <Field.Composition width="large"> <Field.Name.First path="/firstName" /> <Field.Name.Last path="/lastName" /> </Field.Composition> </Form.Card> </Form.Section> ) } const schema: JSONSchema = { type: 'object', required: ['myRequiredSection'], } const ajv = makeAjvInstance() render( <Flex.Stack> <Form.Handler onSubmit={async (data) => console.log('onSubmit', data)}> <MyNameSection required /> <Form.SubmitButton variant="send" /> </Form.Handler> <Form.Handler onSubmit={async (data) => console.log('onSubmit', data)} schema={schema} ajvInstance={ajv} > <MyNameSection path="/myRequiredSection" /> <Form.SubmitButton variant="send" /> </Form.Handler> </Flex.Stack>, )
Nested sections
You can nest sections inside each other.
render( <Form.Handler onSubmit={async (data) => console.log('onSubmit', data)} defaultData={{ nestedPath: { name: { first: 'Nora', last: 'Mørk', }, address: { street: 'Strøget', nr: '', }, }, }} > <MySection path="/nestedPath" required /> <Form.SubmitButton variant="send" /> </Form.Handler>, ) function MySection(props: SectionProps) { return ( <Form.Section {...props}> <Form.Card> <MyNameSection path="/name" /> <MyAddressSection path="/address" /> <MyValueSection /> </Form.Card> </Form.Section> ) } function MyNameSection(props: SectionProps) { return ( <Form.Section {...props}> <Field.Composition width="large"> <Field.Name.First path="/first" /> <Field.Name.Last path="/last" /> </Field.Composition> </Form.Section> ) } function MyAddressSection(props: SectionProps) { return ( <Form.Section {...props}> <Field.Composition width="large"> <Field.String label="Gateadresse" path="/street" width="stretch" /> <Field.String label="Nr." path="/nr" width="small" /> </Field.Composition> </Form.Section> ) } function MyValueSection(props: SectionProps) { return ( <Form.Section {...props}> <Value.SummaryList> <Form.Section path="/name"> <Value.Composition gap="small"> <Value.Name.First path="/first" /> <Value.Name.Last path="/last" /> </Value.Composition> </Form.Section> <Form.Section path="/address"> <Value.Composition gap="small"> <Value.String label="Gateadresse" path="/street" /> <Value.String label="Nr." path="/nr" placeholder="–" /> </Value.Composition> </Form.Section> </Value.SummaryList> </Form.Section> ) }
With Visibility logic
The Form.Visibility component lets you show or hide parts of your form based on the data given in the section itself.
const MySection = ({ children, ...props }) => { return ( <Form.Section {...props}> <Form.Card> <Field.Boolean label="Are you sure?" variant="buttons" path="/iAmSure" /> <Form.Visibility pathTrue="/iAmSure" animate> <Field.Selection label="Choose" variant="radio" path="/mySelection" > <Field.Option value="less" title="Less" /> <Field.Option value="more" title="More" /> </Field.Selection> <Form.Visibility visibleWhen={{ path: '/mySelection', hasValue: 'more', }} animate > <Field.String label="My String" path="/myString" /> </Form.Visibility> </Form.Visibility> {children} </Form.Card> <Tools.Log /> </Form.Section> ) } render( <Form.Handler onChange={console.log} defaultData={{ nestedPath: { iAmSure: false, mySelection: 'less', myString: 'has a value', }, }} > <MySection path="/nestedPath"> <Form.Visibility visibleWhen={{ path: '/myString', hasValue: (value) => value !== 'has a value', }} animate > <P> Result: <Value.String path="/nestedPath/myString" inline /> </P> </Form.Visibility> </MySection> </Form.Handler>, )