Skip to content

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.Section components within the data context, making it effortless to reuse the same set of fields and values in various contexts. Check out e.g. the overwriteProps and path properties for more information.
  • When defining a default value for a field or value, you can use the defaultValue property 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 translations property 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.Handler component.
  • 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

  1. 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>
)
}
  1. 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>
<MySection
overwriteProps={{
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.Handler
defaultData={{
// MySection has a path="/nestedPath" and therefore it is nested in the data context
nestedPath: {
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 support
type Translation = (typeof translations)[keyof typeof translations]
export function MySection() {
return (
<Form.Section translations={translations}>
<ContentOfMySection />
</Form.Section>
)
}
function ContentOfMySection() {
const { MyField } = Form.useTranslation<Translation>().MySection
return <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

Code Editor
<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.

Code Editor
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".

Your account

Fornavn
Nora
Etternavn
Mørk

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.

Your account

Fornavn
Nora

Using variant="basic"

Using variant="basic" will render the view and edit container without the additional Card outline.

Your account

Fornavn
Nora
Etternavn
Mørk

Overwrite properties

Overwriting properties makes it very flexible to reuse the same section of fields in multiple places in your forms.

Code Editor
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.

Code Editor
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.

Code Editor
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.

Fornavn
Nora
Etternavn
Mørk
Gateadresse
Strøget
Nr.
Code Editor
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.

Are you sure?
{
  "nestedPath": {
    "iAmSure": false,
    "mySelection": "less",
    "myString": "has a value"
  }
} 
Code Editor
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>,
)