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.Arraylabel="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.HandlerdefaultData={{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/0will filter out the first item of the list, includingfooandbar./myList/1/foowill filter outfooinside the second item of the list./myList/*/foowill filter out allfooobject keys from all items of the list./myList/*/subList/*/foodoes 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.Handlerdata={{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
<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
<Value.SummaryList> <Iterate.Array defaultValue={['Iron Man', 'Captain America', 'The Hulk']} > <Value.String itemPath="/" /> </Iterate.Array> </Value.SummaryList>
Object items
<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.
<Iterate.Array defaultValue={['foo', 'bar']} onChange={(value) => console.log('onChange', value)} > {(elementValue) => <Field.String value={elementValue} />} </Iterate.Array>
Render properties with object items
<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.
<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
<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.
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
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
- Using
variant="filled"will render the Iterate.ViewContainer and Iterate.EditContainer with a background color. - Using
toolbarVariant="custom"will render the Iterate.Toolbar without any spacing so you can customize it to your needs.
Using a line divider
Initially open
<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.
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.
<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.
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.
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
<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
| Name | Age |
|---|---|
| EtternavnIron Man | |
| EtternavnCaptain America | |
| EtternavnThe Hulk |
<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.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')}}
<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
<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.