-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Sven van de Scheur
committed
Jun 1, 2023
1 parent
bd04ec0
commit 23d7ea7
Showing
8 changed files
with
246 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './multiple'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './multiple.container'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import {Component, Description, Label} from '@components'; | ||
import {IRenderable, RenderComponent} from '@lib/renderer'; | ||
import {IComponentProps, Values} from '@types'; | ||
import {ComponentSchema} from 'formiojs'; | ||
import React, {useState} from 'react'; | ||
|
||
export interface IMultipleComponent extends ComponentSchema { | ||
type: string; | ||
} | ||
|
||
export interface IMultipleProps extends IComponentProps { | ||
component: IMultipleComponent; | ||
value: Values; | ||
} | ||
|
||
/** | ||
* Implements `multiple: true` behaviour. | ||
* | ||
* Provide a thin wrapper around a component with controls for adding multiple instances. Utilizes | ||
* <RenderComponent/> to render individual instances. | ||
*/ | ||
export const Multiple: React.FC<IMultipleProps> = props => { | ||
const {component, form, path, value = [], setValue} = props; // FIXME: Awaits future pr. | ||
const [keys, setKeys] = useState<number[]>(Array.from({length: 1}, (_, i) => i)); | ||
|
||
/** Finds next key by increasing the max key with 1. */ | ||
const getKey = (): number => { | ||
if (!keys.length) { | ||
return 0; | ||
} | ||
const max = Math.max(...keys); | ||
return max + 1; | ||
}; | ||
|
||
/** Add item. */ | ||
const add = () => { | ||
setKeys([...keys, getKey()]); | ||
}; | ||
|
||
/** Remove item at index. */ | ||
const remove = (index: number) => { | ||
const _keys = keys.filter((_, i) => i !== index); | ||
const val = value.filter((_, i) => i !== index); | ||
setKeys(_keys); | ||
setValue(path, val); | ||
}; | ||
|
||
/** Renders individual components utilizing <RenderComponent/>. */ | ||
const renderComponents = () => | ||
Array.from({length: keys.length}, (_, index) => { | ||
// Clone and adjust component to fit nested needs. | ||
const renderable: IRenderable = { | ||
...structuredClone(component), | ||
key: `${path}.${index}`, // Trigger Formik array values. | ||
multiple: false, // Handled by <Multiple/> | ||
description: '', // One description rendered for all components. | ||
label: '', // One label rendered for all components. | ||
}; | ||
|
||
return ( | ||
<tr key={keys[index]}> | ||
<td> | ||
<RenderComponent component={renderable} form={form} path={path} value={value[index]} /> | ||
</td> | ||
<td> | ||
<button onClick={() => remove(index)} aria-controls={renderable.key}> | ||
Remove item | ||
</button> | ||
</td> | ||
</tr> | ||
); | ||
}); | ||
|
||
return ( | ||
<Component {...props}> | ||
<Label {...props} /> | ||
<table> | ||
<tbody>{renderComponents()}</tbody> | ||
<tfoot> | ||
<tr> | ||
<td> | ||
<button type="button" onClick={() => add()}> | ||
Add another | ||
</button> | ||
</td> | ||
</tr> | ||
</tfoot> | ||
</table> | ||
<table /> | ||
<Description {...props} /> | ||
</Component> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import {DEFAULT_RENDER_CONFIGURATION, RenderForm} from '@lib/renderer'; | ||
import {expect} from '@storybook/jest'; | ||
import type {ComponentStory, Meta} from '@storybook/react'; | ||
import {userEvent, within} from '@storybook/testing-library'; | ||
|
||
import {Multiple} from './multiple.container'; | ||
|
||
const meta: Meta<typeof Multiple> = { | ||
title: 'Containers / Multiple', | ||
component: Multiple, | ||
decorators: [], | ||
parameters: {}, | ||
}; | ||
export default meta; | ||
|
||
export const multipleTextfields: ComponentStory<typeof RenderForm> = args => ( | ||
<RenderForm {...args} /> | ||
); | ||
multipleTextfields.args = { | ||
configuration: DEFAULT_RENDER_CONFIGURATION, | ||
form: { | ||
display: 'form', | ||
components: [ | ||
{ | ||
type: 'textfield', | ||
key: 'multiple-inputs', | ||
description: 'Array of strings instead of a single string value', | ||
label: 'Multiple inputs', | ||
multiple: true, | ||
}, | ||
], | ||
}, | ||
initialValues: { | ||
'multiple-inputs': ['first value'], | ||
}, | ||
onSubmit: () => {}, | ||
}; | ||
multipleTextfields.play = async ({canvasElement}) => { | ||
const canvas = within(canvasElement); | ||
|
||
// check that new items can be added | ||
await userEvent.click(canvas.getByRole('button', {name: 'Add another'})); | ||
const input1 = await canvas.getAllByRole('textbox')[0]; | ||
expect(input1).toHaveDisplayValue('first value'); | ||
await userEvent.clear(input1); | ||
await userEvent.type(input1, 'Foo'); | ||
expect(input1).toHaveDisplayValue('Foo'); | ||
|
||
const input2 = await canvas.getAllByRole('textbox')[1]; | ||
expect(input2).toHaveDisplayValue(''); | ||
|
||
// the label & description should be rendered only once, even with > 1 inputs | ||
expect(canvas.queryAllByText('Multiple inputs')).toHaveLength(1); | ||
expect(canvas.queryAllByText('Array of strings instead of a single string value')).toHaveLength( | ||
1 | ||
); | ||
|
||
// finally, it should be possible delete rows again | ||
const removeButtons = await canvas.findAllByRole('button', {name: 'Remove item'}); | ||
expect(removeButtons).toHaveLength(2); | ||
await userEvent.click(removeButtons[0]); | ||
expect(await canvas.getAllByRole('textbox')[0]).toHaveDisplayValue(''); | ||
expect(await canvas.getAllByRole('textbox')).toHaveLength(1); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters