Skip to content

Import

import { Iterate } from '@dnb/eufemia/extensions/forms'
render(<Iterate.Array />)

Description

Iterate.Array works in many ways similar to field-components. It has a value-property that can receive an array or you can give it a path if you want it to retrieve an array from a surrounding DataContext. All children components of Iterate.Array are rendered once per item the array-value consists of.

import { Iterate, Field } from '@dnb/eufemia/extensions/forms'
render(
<Iterate.Array
label="Array label"
value={['Iron Man', 'Captain America', 'The Hulk']}
>
<Field.String itemPath="/" />
</Iterate.Array>,
)

About itemPath and path

itemPath points to the root of each iterated item, while path points to the root of the data source:

import { Iterate, Field, Form } from '@dnb/eufemia/extensions/forms'
render(
<Form.Handler
defaultData={{
listOfHeroes: [
{ name: 'Iron Man' },
{ name: 'Captain America' },
{ name: 'The Hulk' },
],
}}
onChange={console.log}
>
<Iterate.Array path="/listOfHeroes">
<Field.Name.Last itemPath="/name" />
</Iterate.Array>
</Form.Handler>,
)

Individual values and dynamic paths

Since Iterate.Array renders its children once per item, the field components inside must receive values based on the different items in the array. This can be done in two ways:

1. itemPath

If field components inside Iterate.Array are given an itemPath property, this will look for values based on the array item being the root of the structure, even if the array often comes from a surrounding data set. This means that you do not need to think about which index the field should point to, because it is handled by Iterate.Array internally. You can look at the individual item as its own structure.

2. Function callback as children (render property)

If you want to be able to provide values to the individual field component directly instead of pointing to them with paths, you can give Iterate.Array a render property. It works a bit like an array-map call. The render function provides the value of the item as the first argument, the index of which item you are on as the second, and the internal array as the third.

render(
<Iterate.Array path="/listOfHeroes">
{(itemValue, itemIndex, internalArray) => {
return <Field.Name.Last itemPath="/name" />
}}
</Iterate.Array>,
)

You can also get the index by using the useItem hook:

const MyItem = () => {
const { index } = Iterate.useItem()
return <Field.Name.Last itemPath="/name" />
}
render(
<Iterate.Array path="/listOfHeroes">
<MyItem />
</Iterate.Array>,
)

The item number in labels

You can use the {itemNo} variable in the label to display the current item number. This is useful when you have a list of items and you want to display the item number in the label.

import { Iterate, Field } from '@dnb/eufemia/extensions/forms'
render(
<Iterate.Array value={['foo', 'bar']}>
<Field.String itemPath="/" label="Item no. {itemNo}" />
</Iterate.Array>,
)

The Iterate.ViewContainer and the Iterate.EditContainer also supports {itemNo} in the title property to display the current item number.

import { Iterate, Field } from '@dnb/eufemia/extensions/forms'
render(
<Iterate.Array value={['foo', 'bar']}>
<Iterate.ViewContainer title="Item no. {itemNo}">
...
</Iterate.ViewContainer>
</Iterate.Array>,
)

Initial container mode

This section describes the behavior of the EditContainer and the ViewContainer components.

By default, the container mode is set to auto. This means that the container will open (switch to edit mode) when there is an error in the container or the value is falsy (empty string, null, undefined, etc.).

When a new item is added via the Iterate.PushButton component, the item before it will change to view mode, if it had no validation errors.

Filter data

You can filter data by paths specific or all paths.

  • /myList/0 will filter out the first item of the list, including foo and bar.
  • /myList/1/foo will filter out foo inside the second item of the list.
  • /myList/*/foo will filter out all foo object keys from all items of the list.
  • /myList/*/subList/*/foo does support multi wildcard paths.

In the example below, the data given in onSubmit will still have "foo2" and "bar2" of the list.

import { Iterate, Form, Field } from '@dnb/eufemia/extensions/forms'
const myFilter = {
'/myList/0': false,
}
render(
<Form.Handler
data={{
myList: [
{ foo: 'foo1', bar: 'bar1' },
{ foo: 'foo2', bar: 'bar2' },
],
}}
onSubmit={(data, { filterData }) => {
console.log('onSubmit', filterData(myFilter))
}}
>
<Iterate.Array path="/myList">
<Field.String itemPath="/foo" label="Foo no. {itemNo}" />
<Field.String itemPath="/bar" label="Bar no. {itemNo}" />
</Iterate.Array>
</Form.Handler>,
)

Instead of false you can provide a function that returns false based on your logic.

Demos

Primitive items as fields

Code Editor
<Iterate.Array
  defaultValue={['Iron Man', 'Captain America', 'The Hulk']}
  onChange={console.log}
>
  <Field.String itemPath="/" />
</Iterate.Array>

Primitive items as values

Iron Man
Captain America
The Hulk
Code Editor
<Value.SummaryList>
  <Iterate.Array
    defaultValue={['Iron Man', 'Captain America', 'The Hulk']}
  >
    <Value.String itemPath="/" />
  </Iterate.Array>
</Value.SummaryList>

Object items

Code Editor
<Iterate.Array
  defaultValue={[
    {
      accountName: 'Brukskonto',
      accountNumber: '90901134567',
    },
    {
      accountName: 'Sparekonto',
      accountNumber: '90901156789',
    },
  ]}
  onChange={(value) => console.log('onChange', value)}
>
  <Field.Composition>
    <Field.BankAccountNumber itemPath="/accountNumber" />
    <Field.String label="Account name" itemPath="/accountName" />
  </Field.Composition>
</Iterate.Array>

Render properties with primitive items

You can provide the child as a function that receives the value of the item as the first argument, and the index of which item you are on as the second.

Code Editor
<Iterate.Array
  defaultValue={['foo', 'bar']}
  onChange={(value) => console.log('onChange', value)}
>
  {(elementValue) => <Field.String value={elementValue} />}
</Iterate.Array>

Render properties with object items

Code Editor
<Iterate.Array
  defaultValue={[
    {
      num: 1,
      txt: 'One',
    },
    {
      num: 2,
      txt: 'Two',
    },
  ]}
  onChange={(value) => console.log('onChange', value)}
>
  {({ num, txt }) => (
    <Field.Composition width="large">
      <Field.Number value={num} width="small" />
      <Field.String value={txt} width={false} />
    </Field.Composition>
  )}
</Iterate.Array>

Conditions using Visibility

The second field will be visible when the first has a value.

Code Editor
<Form.Handler>
  <Iterate.Array path="/myList" defaultValue={[{}]}>
    <Flex.Stack>
      <Field.Name.First className="firstName" itemPath="/firstName" />

      <Form.Visibility
        animate
        visibleWhen={{
          itemPath: '/firstName',
          hasValue: (value) => Boolean(value),
        }}
      >
        <Field.Name.Last className="lastName" itemPath="/lastName" />
      </Form.Visibility>
    </Flex.Stack>
  </Iterate.Array>
</Form.Handler>

Dynamic path value

Code Editor
<Form.Handler
  defaultData={{
    count: 0,
  }}
>
  <Flex.Stack>
    <Field.Number path="/count" width="small" showStepControls />
    <Iterate.Array
      path="/items"
      countPath="/count"
      countPathTransform={({ value, index }) => {
        return 'myObject' in (value || {})
          ? value
          : {
              myObject: index,
            }
      }}
    >
      <Field.Number itemPath="/myObject" label="Item no. {itemNo}" />
    </Iterate.Array>
  </Flex.Stack>
</Form.Handler>

Animated container

With an optional title and Iterate.Toolbar.

Title 1


Code Editor
const MyForm = () => {
  const { count } = Iterate.useCount('myForm')
  return (
    <Form.Handler
      defaultData={{
        myList: ['Item 1'],
      }}
      id="myForm"
    >
      <Form.Card>
        <Iterate.Array path="/myList" placeholder={<>Empty list</>}>
          <Iterate.AnimatedContainer title="Title {itemNo}">
            <Field.String label="Label" itemPath="/" />

            <Iterate.Toolbar>
              <Iterate.RemoveButton />
            </Iterate.Toolbar>
          </Iterate.AnimatedContainer>
        </Iterate.Array>

        <Iterate.PushButton
          path="/myList"
          pushValue={`Item ${String(count('/myList') + 1)}`}
          text="Add new item"
        />
      </Form.Card>
    </Form.Handler>
  )
}
render(<MyForm />)

Toggle between a view and edit container

Accounts

Account holder 1

Fornavn
Tony
Etternavn
Rogers

Code Editor
const MyEditItemForm = () => {
  return (
    <Field.Composition>
      <Field.Name.First itemPath="/firstName" width="medium" />
      <Field.Name.Last itemPath="/lastName" width="medium" required />
    </Field.Composition>
  )
}
const MyEditItem = () => {
  return (
    <Iterate.EditContainer
      title="Edit account holder {itemNo}"
      titleWhenNew="New account holder {itemNo}"
    >
      <MyEditItemForm />
    </Iterate.EditContainer>
  )
}
const MyViewItem = () => {
  const item = Iterate.useItem()
  console.log('index:', item.index)
  return (
    <Iterate.ViewContainer title="Account holder {itemNo}">
      <Value.SummaryList>
        <Value.Name.First itemPath="/firstName" showEmpty />
        <Value.Name.Last itemPath="/lastName" placeholder="-" />
      </Value.SummaryList>
    </Iterate.ViewContainer>
  )
}
const CreateNewEntry = () => {
  return (
    <Iterate.PushContainer
      path="/accounts"
      title="New account holder"
      openButton={
        <Iterate.PushContainer.OpenButton text="Add another account" />
      }
      showOpenButtonWhen={(list) => list.length > 0}
    >
      <MyEditItemForm />
    </Iterate.PushContainer>
  )
}
const MyForm = () => {
  return (
    <Form.Handler
      data={{
        accounts: [
          {
            firstName: 'Tony',
            lastName: 'Rogers',
          },
        ],
      }}
      onChange={(data) => console.log('DataContext/onChange', data)}
      onSubmit={async (data) => console.log('onSubmit', data)}
    >
      <Flex.Stack>
        <Form.MainHeading>Accounts</Form.MainHeading>

        <Form.Card gap={false}>
          <Iterate.Array path="/accounts">
            <MyViewItem />
            <MyEditItem />
          </Iterate.Array>

          <CreateNewEntry />
        </Form.Card>

        <Form.SubmitButton variant="send" />
      </Flex.Stack>
    </Form.Handler>
  )
}
render(<MyForm />)

Customize the view and edit containers

Persons

Tony with long name that maybe will wrap over to a new lineTTony with long name that maybe will wrap over to a new line
MariaMMaria

Using a line divider

Accounts

FornavnTony
EtternavnLast

FornavnMaria
EtternavnLast

Initially open


{
  "myList": [
    {}
  ]
} 
Code Editor
<Form.Handler required>
  <Wizard.Container>
    <Wizard.Step>
      <Form.Card>
        <Iterate.Array path="/myList" defaultValue={[{}]}>
          <Iterate.ViewContainer>
            <Value.String label="Item {itemNo}" itemPath="/foo" />
          </Iterate.ViewContainer>
          <Iterate.EditContainer>
            <Field.String
              label="Item {itemNo}"
              itemPath="/foo"
              defaultValue="foo"
            />
          </Iterate.EditContainer>
        </Iterate.Array>

        <Iterate.PushButton
          text="Add"
          path="/myList"
          variant="tertiary"
          pushValue={{}}
        />
      </Form.Card>

      <Wizard.Buttons />
    </Wizard.Step>

    <Wizard.Step>
      <Iterate.Array path="/myList" defaultValue={[{}]}>
        <Iterate.EditContainer>
          <Field.String
            label="Item {itemNo}"
            itemPath="/foo"
            defaultValue="foo"
          />
        </Iterate.EditContainer>
        <Iterate.ViewContainer>
          <Value.String label="Item {itemNo}" itemPath="/foo" />
        </Iterate.ViewContainer>
      </Iterate.Array>

      <Wizard.Buttons />
    </Wizard.Step>
  </Wizard.Container>

  <Tools.Log top />
</Form.Handler>

Required

With a Iterate.PushContainer to add a new item.

The new item gets inserted at the beginning of the array by using the reverse property.

New account holder


Code Editor
const MyViewItem = () => {
  return (
    <Iterate.ViewContainer title="Account holder {itemNo}">
      <Value.SummaryList>
        <Value.Name.First itemPath="/firstName" />
        <Value.Name.Last itemPath="/lastName" />
      </Value.SummaryList>
    </Iterate.ViewContainer>
  )
}
const MyEditItem = () => {
  return (
    <Iterate.EditContainer
      title="Edit account holder {itemNo}"
      titleWhenNew="New account holder {itemNo}"
    >
      <MyEditItemContent />
    </Iterate.EditContainer>
  )
}
const MyEditItemContent = () => {
  return (
    <Field.Composition width="large">
      <Field.Name.First itemPath="/firstName" required />
      <Field.Name.Last itemPath="/lastName" required />
    </Field.Composition>
  )
}
render(
  <Form.Handler>
    <Form.Card>
      <Iterate.PushContainer
        path="/myListOfPeople"
        title="New account holder"
      >
        <MyEditItemContent />
      </Iterate.PushContainer>

      <Iterate.Array
        path="/myListOfPeople"
        reverse
        animate
        required
        errorMessages={{
          'Field.errorRequired': 'Custom message',
        }}
      >
        <MyViewItem />
        <MyEditItem />
      </Iterate.Array>
    </Form.Card>

    <Form.SubmitButton />
  </Form.Handler>,
)

With a Iterate.PushButton to add a new item.

Code Editor
<Form.Handler>
  <Form.Card>
    <Iterate.Array
      path="/items"
      animate
      required
      errorMessages={{
        'Field.errorRequired': 'Custom message',
      }}
      validateInitially
    >
      <Flex.Horizontal>
        <Field.String itemPath="/" />
        <Iterate.RemoveButton />
      </Flex.Horizontal>
    </Iterate.Array>

    <Iterate.PushButton
      path="/items"
      pushValue="baz"
      text="Add item to hide error"
    />
  </Form.Card>

  <Form.SubmitButton />
</Form.Handler>

Minium one item

There are several ways to achieve this:

By using a schema

This example uses the minItems in a schema with a custom error message.

const schema = {
type: 'object',
properties: {
myList: {
type: 'array',
minItems: 1,
},
},
}

It will show the error message when the array is empty.

Remove me to see the minItems error.

{
  "myList": [
    {
      "foo": "Remove me to see the minItems error."
    }
  ]
} 

By using the toolbarVariant property

This example uses the container's toolbarVariant property with the value minimumOneItem.

It hides the toolbar in the EditContainer when there is only one item in the array. And it hides the remove button in the ViewContainer when there is only one item in the array.

Statsborgerskap

{
  "countries": [
    null
  ]
} 
Code Editor
const MyForm = () => {
  const { getCountryNameByIso } = Value.SelectCountry.useCountry()
  return (
    <Form.Handler
      onSubmit={async (data) => console.log('onSubmit', data)}
      onSubmitRequest={() => console.log('onSubmitRequest')}
    >
      <Flex.Stack>
        <Form.MainHeading>Statsborgerskap</Form.MainHeading>

        <Form.Card>
          <Iterate.Array
            path="/countries"
            defaultValue={[null]}
            onChangeValidator={(arrayValue) => {
              const findFirstDuplication = (arr) =>
                arr.findIndex((e, i) => arr.indexOf(e) !== i)
              const index = findFirstDuplication(arrayValue)
              if (index > -1) {
                return new Error(
                  `You cannot have duplicate items: ${getCountryNameByIso(
                    String(arrayValue.at(index)),
                  )}`,
                )
              }
            }}
          >
            <Iterate.ViewContainer toolbarVariant="minimumOneItem">
              <Value.SelectCountry
                label="Land du er statsborger i"
                itemPath="/"
              />
            </Iterate.ViewContainer>

            <Iterate.EditContainer toolbarVariant="minimumOneItem">
              <Field.SelectCountry
                label="Land du er statsborger i"
                itemPath="/"
                required
              />
            </Iterate.EditContainer>
          </Iterate.Array>

          <Iterate.PushButton
            path="/countries"
            pushValue={null}
            text="Legg til flere statsborgerskap"
          />
        </Form.Card>

        <Form.SubmitButton variant="send" />

        <Tools.Log />
      </Flex.Stack>
    </Form.Handler>
  )
}
render(<MyForm />)

With DataContext and add/remove buttons

Avengers

Iron Man


Captain America


Code Editor
<Form.Handler
  data={{
    avengers: [
      {
        nickname: 'Iron Man',
        firstName: 'Tony',
        lastName: 'Stark',
      },
      {
        nickname: 'Captain America',
        firstName: 'Steve',
        lastName: 'Rogers',
      },
    ],
  }}
  onChange={(data) => console.log('DataContext/onChange', data)}
>
  <Flex.Stack>
    <Form.MainHeading>Avengers</Form.MainHeading>

    <Form.Card>
      <Iterate.Array
        path="/avengers"
        onChange={(value) => console.log('Iterate/onChange', value)}
      >
        <Iterate.AnimatedContainer
          title={
            <Value.String
              label={false}
              itemPath="/nickname"
              placeholder="A Nick name"
            />
          }
        >
          <Field.Name
            itemPath="/nickname"
            width="medium"
            label="Nick name"
          />

          <Field.Composition>
            <Field.Name.First itemPath="/firstName" width="medium" />
            <Field.Name.Last itemPath="/lastName" width="medium" />
          </Field.Composition>

          <Iterate.Toolbar>
            <Iterate.RemoveButton showConfirmDialog />
          </Iterate.Toolbar>
        </Iterate.AnimatedContainer>
      </Iterate.Array>

      <Iterate.PushButton
        text="Add another avenger"
        path="/avengers"
        pushValue={{}}
      />
    </Form.Card>
  </Flex.Stack>
</Form.Handler>

Static generated in a Table

NameAge
EtternavnIron Man
EtternavnCaptain America
EtternavnThe Hulk
Code Editor
<Table>
  <thead>
    <Tr>
      <Th>Name</Th>
      <Th>Age</Th>
    </Tr>
  </thead>
  <tbody>
    <Iterate.Array
      withoutFlex
      defaultValue={[
        {
          name: 'Iron Man',
          age: 45,
        },
        {
          name: 'Captain America',
          age: 123,
        },
        {
          name: 'The Hulk',
          age: 3337,
        },
      ]}
    >
      <Tr>
        <Td>
          <Value.Name.Last itemPath="/name" />
        </Td>
        <Td>
          <Value.Number itemPath="/age" />
        </Td>
      </Tr>
    </Iterate.Array>
  </tbody>
</Table>

Value composition

value 1value 2
Code Editor
<Value.Composition>
  <Iterate.Array
    defaultValue={[
      {
        value: 'value 1',
      },
      {
        value: 'value 2',
      },
    ]}
  >
    <Value.String itemPath="/value" />
  </Iterate.Array>
</Value.Composition>

Array validator

You can also add a validator to ensure that the array contains at least one item:

const validator = (arrayValue) => {
if (!(arrayValue?.length > 0)) {
return new Error('You need at least one item')
}
}
Code Editor
<Form.Handler
  defaultData={{
    items: ['foo'],
  }}
  onSubmit={async () => console.log('onSubmit')}
>
  <Form.Card>
    <Iterate.Array
      path="/items"
      onChangeValidator={(arrayValue) => {
        if (!(arrayValue && arrayValue.length > 1)) {
          return new Error('You need at least two items')
        }
      }}
      animate
    >
      <Flex.Horizontal align="flex-end">
        <Field.String
          label="Item no. {itemNo}"
          itemPath="/"
          width="medium"
          size="medium"
        />
        <Iterate.RemoveButton showConfirmDialog />
      </Flex.Horizontal>
    </Iterate.Array>

    <Iterate.PushButton top path="/items" pushValue={null} text="Add" />
    <Form.SubmitButton />
  </Form.Card>
</Form.Handler>

Nested Iterate

{
  "outer": [
    {
      "inner": [
        "foo",
        "bar"
      ]
    }
  ]
} 
Code Editor
<Form.Handler
  data={{
    outer: [
      {
        inner: ['foo', 'bar'],
      },
    ],
  }}
>
  <Iterate.Array path="/outer">
    <Iterate.Array itemPath="/inner">
      <Field.String label="Item {itemNo}" itemPath="/" />
    </Iterate.Array>
  </Iterate.Array>

  <Tools.Log />
</Form.Handler>

Nested Iterate with PushContainer

This demo uses the Iterate.PushContainer component to add new items to an nested array by using the itemPath property.

New person

Citizenship's


"undefined"