Import
import { Iterate } from '@dnb/eufemia/extensions/forms'render(<Iterate.PushContainer />)
Description
Iterate.PushContainer enables users to create a new item in the array. It can be used instead of the Iterate.PushButton, but with fields in the container.
It allows the user to fill in the fields without storing them in the data context.
Good to know:
- Fields inside the container must have an
itemPathdefined, instead of apath. - If the user enters data without committing it to the outer context, that data will be lost when navigating to another step in the Wizard. To prevent this, you can use the
preventUncommittedChangesproperty on the PushContainer. When enabled, it will display an error message if the user tries to proceed without committing their changes. - You can provide
data,defaultDatain addition toisolatedDatato prefill the fields. - The
pathyou define needs to point to an existing Iterate.Array path.
Usage
You may place it below the Iterate.Array component like this:
import { Iterate, Field } from '@dnb/eufemia/extensions/forms'render(<Form.Handler><Iterate.Array path="/myList">...</Iterate.Array><Iterate.PushContainer path="/myList" title="New item title"><Field.Name.Last itemPath="/name" /></Iterate.PushContainer></Form.Handler>,)
Prevent the form from being submitted
To prevent the Form.Handler from being submitted when there are fields with errors inside the PushContainer, you can use the bubbleValidation property.
import { Form, Field, Iterate } from '@dnb/eufemia/extensions/forms'render(<Form.Handler><Iterate.Array path="/myList">...</Iterate.Array><Iterate.PushContainer path="/myList" bubbleValidation><Field.Name.Last itemPath="/name" required /></Iterate.PushContainer></Form.Handler>,)
Show a button to create a new item
By default, it keeps the form open after a new item has been created. You can change this behavior by using the openButton and showOpenButtonWhen properties.
These properties allow you to render a button (openButton) and determine when to show it based on the logic provided by the showOpenButtonWhen function. The showOpenButtonWhen function receives the current list of items as an argument.
The button will be shown instead of the content provided by the children when the showOpenButtonWhen function returns true.
import { Iterate, Field } from '@dnb/eufemia/extensions/forms'render(<Form.Handler><Iterate.Array path="/myList">...</Iterate.Array><Iterate.PushContainerpath="/myList"title="New item title"openButton={<Iterate.PushContainer.OpenButton text="Add another item" />}showOpenButtonWhen={(list) => list.length > 0}>Will be hidden based on the showOpenButtonWhen function</Iterate.PushContainer></Form.Handler>,)
The Iterate.PushContainer.OpenButton accepts the same properties as the Button component.
Show the next item number in the open button
You can use the {nextItemNo} variable in the text or children property to display the next item number.
import { Iterate, Field, Value } from '@dnb/eufemia/extensions/forms'render(<Form.Handler><Iterate.Array path="/myList">...</Iterate.Array><Iterate.PushContainerpath="/myList"title="New item title"openButton={<Iterate.PushContainer.OpenButton text="Add no. {nextItemNo}" />}showOpenButtonWhen={(list) => list.length > 0}><Field.Name.Last itemPath="/name" /></Iterate.PushContainer></Form.Handler>,)
Technical details
Under the hood, it uses the Form.Isolation component to isolate the data from the rest of the form. It also uses the the Iterate.EditContainer inside the Iterate.Array component to render the fields.
All fields inside the container will be stored in the data context at this path: /pushContainerItems/0.
Demos
Prevent uncommitted changes
This example uses the preventUncommittedChanges property to display an error message if the user has made changes and attempts to navigate to the next Wizard step.
Try entering something in the input field, then navigate to the next step. An error message will appear to indicate that changes must be committed first.
<Form.Handler> <Wizard.Container> <Wizard.Step title="Step 1"> <Form.Card> <Form.SubHeading>People</Form.SubHeading> <Iterate.Array path="/people" animate placeholder="No people"> <Value.Name.First itemPath="/firstName" /> </Iterate.Array> <Iterate.PushContainer path="/people" title="New person" preventUncommittedChanges bubbleValidation openButton={ <Iterate.PushContainer.OpenButton top variant="tertiary" text="Add new person" /> } showOpenButtonWhen={(list) => list.length > 0} > <Field.Name.First itemPath="/firstName" /> </Iterate.PushContainer> </Form.Card> <Wizard.Buttons /> </Wizard.Step> <Wizard.Step title="Step 2"> <Iterate.Array path="/people"> <Value.Name.First itemPath="/firstName" /> </Iterate.Array> <Wizard.Buttons /> </Wizard.Step> </Wizard.Container> </Form.Handler>
Initially open
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 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 />)
With existing data
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 />)
Isolated data
This demo shows how to use the isolatedData property to provide data to the PushContainer.
const formData = { persons: [ { firstName: 'Ola', lastName: 'Nordmann', }, { firstName: 'Kari', lastName: 'Nordmann', }, { firstName: 'Per', lastName: 'Hansen', }, ], } function RepresentativesView() { return ( <Iterate.ViewContainer> <Value.Composition> <Value.String itemPath="/firstName" /> <Value.String itemPath="/lastName" /> </Value.Composition> </Iterate.ViewContainer> ) } function RepresentativesEdit() { return ( <Iterate.EditContainer> <Field.Name.First itemPath="/firstName" /> <Field.Name.Last itemPath="/lastName" /> </Iterate.EditContainer> ) } function ExistingPersonDetails() { const { data, getValue } = Form.useData() const person = getValue(data['selectedPerson'])?.data || {} return ( <Flex.Stack> <Field.Name.First readOnly itemPath="/firstName" value={person.firstName} /> <Field.Name.Last readOnly itemPath="/lastName" value={person.lastName} /> </Flex.Stack> ) } function NewPersonDetails() { return ( <Flex.Stack> <Field.Name.First required itemPath="/firstName" /> <Field.Name.Last required itemPath="/lastName" /> </Flex.Stack> ) } function PushContainerContent() { const { data, update } = Form.useData() const selectedPerson = data['selectedPerson'] // Because of missing TypeScript support // Clear the PushContainer data when the selected person is "other", // so the fields do not inherit existing data. React.useLayoutEffect(() => { if (selectedPerson === 'other') { update('/pushContainerItems/0', {}) } }, [selectedPerson, update]) return ( <> <Field.Selection variant="radio" required path="/selectedPerson" dataPath="/persons" > <Field.Option value="other" label="Other person" /> </Field.Selection> <HeightAnimation top> <Form.Visibility visibleWhen={{ path: '/selectedPerson', hasValue: (value) => typeof value === 'string' && value !== 'other', }} > <ExistingPersonDetails /> </Form.Visibility> <Form.Visibility visibleWhen={{ path: '/selectedPerson', hasValue: (value) => value === 'other', }} > <NewPersonDetails /> </Form.Visibility> </HeightAnimation> </> ) } function RepresentativesCreateNew() { return ( <Iterate.PushContainer path="/representatives" title="Add new representative" isolatedData={{ persons: formData.persons.map((data, i) => { return { title: [data.firstName, data.lastName].join(' '), value: `/persons/${i}`, data, } }), }} openButton={ <Iterate.PushContainer.OpenButton variant="tertiary" text="Add new representative" /> } showOpenButtonWhen={(list) => list.length > 0} > <PushContainerContent /> </Iterate.PushContainer> ) } render( <Form.Handler> <Form.MainHeading>Representatives</Form.MainHeading> <Flex.Stack> <Form.Card> <Iterate.Array path="/representatives"> <RepresentativesView /> <RepresentativesEdit /> </Iterate.Array> <RepresentativesCreateNew /> </Form.Card> <Form.Card> <Form.SubHeading>Data Context</Form.SubHeading> <Tools.Log placeholder="-" /> </Form.Card> </Flex.Stack> </Form.Handler>, )