Skip to content

Commit

Permalink
♻️ #43 - Refactor validation to suit multiple.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sven van de Scheur committed Jun 6, 2023
1 parent 2d6f8cc commit dfa63b5
Show file tree
Hide file tree
Showing 15 changed files with 157 additions and 99 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
},
"dependencies": {
"clsx": "^1.2.1",
"formik": "^2.2.9"
"formik": "^2.2.9",
"lodash": "^4.17.21"
},
"resolutions": {
"react-formio/formiojs": "~4.13.0"
Expand Down
2 changes: 1 addition & 1 deletion src/components/formio/textfield/textfield.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const TextField: React.FC<ITextFieldProps> = ({callbacks, component, valu
{..._callbacks}
/>

{!pristineState && component.showCharCount && <CharCount value={value} />}
{!pristineState && component.showCharCount && value && <CharCount value={value} />}
{component.description && <Description description={component.description} />}
{!pristineState && errors?.length > 0 && <Errors componentId={componentId} errors={errors} />}
</Component>
Expand Down
7 changes: 4 additions & 3 deletions src/components/utils/errors/errors.component.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {ValidationError} from '@lib/validation';
import clsx from 'clsx';
import React from 'react';

export interface IErrorsProps {
componentId: string;
errors: string[];
errors: ValidationError[];
}

/**
Expand All @@ -16,11 +17,11 @@ export const Errors: React.FC<IErrorsProps> = ({componentId, errors}) => {

return (
<ul className={className} aria-describedby={componentId}>
{errors?.map((error: string, index: number) => {
{errors?.map((error, index: number) => {
return (
<li key={index} className={listItemClassName}>
<label className={labelClassName} htmlFor={componentId} role="alert">
{error}
{error.message}
</label>
</li>
);
Expand Down
6 changes: 5 additions & 1 deletion src/components/utils/errors/errors.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export default meta;

export const errors: ComponentStory<typeof Errors> = args => <Errors {...args} />;
errors.args = {
errors: ['Postcode is required', 'Postcode does not match the pattern ^\\d{4}\\s?[a-zA-Z]{2}$'],
// errors: [{message: 'Postcode is required'}, {message: 'Postcode does not match the pattern ^\\d{4}\\s?[a-zA-Z]{2}$'}],
errors: [
{name: 'required', message: 'Postcode is required'},
{name: 'pattern', message: 'Postcode does not match the pattern ^\\d{4}\\s?[a-zA-Z]{2}$'},
],
};
errors.play = async ({canvasElement}) => {
const canvas = within(canvasElement);
Expand Down
6 changes: 4 additions & 2 deletions src/containers/multiple/multiple.container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ export const Multiple: React.FC<IMultipleProps> = props => {

return (
<Component {...props}>
<Label {...props} />
{props.component.label && (
<Label label={props.component.label} componentId={props.component.key as string} />
)}
<table>
<tbody>{renderComponents()}</tbody>
<tfoot>
Expand All @@ -87,7 +89,7 @@ export const Multiple: React.FC<IMultipleProps> = props => {
</tfoot>
</table>
<table />
<Description {...props} />
{props.component.description && <Description description={props.component.description} />}
</Component>
);
};
54 changes: 52 additions & 2 deletions src/containers/multiple/multiple.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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 {userEvent, waitFor, within} from '@storybook/testing-library';

import {Multiple} from './multiple.container';

Expand All @@ -27,13 +27,18 @@ multipleTextfields.args = {
description: 'Array of strings instead of a single string value',
label: 'Multiple inputs',
multiple: true,
showCharCount: true,
validate: {
required: true,
maxLength: 3,
minLength: 1,
},
},
],
},
initialValues: {
'multiple-inputs': ['first value'],
},
onSubmit: () => {},
};
multipleTextfields.play = async ({canvasElement}) => {
const canvas = within(canvasElement);
Expand Down Expand Up @@ -62,3 +67,48 @@ multipleTextfields.play = async ({canvasElement}) => {
expect(await canvas.getAllByRole('textbox')[0]).toHaveDisplayValue('');
expect(await canvas.getAllByRole('textbox')).toHaveLength(1);
};

export const multipleTextfieldsWithValidation: ComponentStory<typeof RenderForm> = args => (
<RenderForm {...args} />
);
multipleTextfieldsWithValidation.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,
showCharCount: true,
validate: {
required: true,
},
},
],
},
initialValues: {
'multiple-inputs': ['first value'],
},
};
multipleTextfieldsWithValidation.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];
const input2 = await canvas.getAllByRole('textbox')[1];

await userEvent.type(input1, 'foo', {delay: 300});
await userEvent.type(input2, 'bar', {delay: 300});

await userEvent.clear(input1);
expect(await canvas.findAllByText('Multiple inputs is required')).toHaveLength(1);

await userEvent.clear(input2);
waitFor(async () => {
expect(await canvas.findAllByText('Multiple inputs is required')).toHaveLength(2);
});
};
8 changes: 4 additions & 4 deletions src/lib/renderer/renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Column, Columns, Content, IColumnProps, TextField} from '@components';
import {Multiple} from '@containers';
import {DEFAULT_VALIDATORS, getFormErrors} from '@lib/validation';
import {DEFAULT_VALIDATORS, ValidationError, getFormErrors} from '@lib/validation';
import {IComponentProps, IFormioForm, IRenderConfiguration, IValues, Value, Values} from '@types';
import {Formik, useField, useFormikContext} from 'formik';
import {FormikHelpers} from 'formik/dist/types';
Expand Down Expand Up @@ -109,7 +109,7 @@ export const RenderForm: React.FC<IRenderFormProps> = ({
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
validate={values => getFormErrors(form, values, configuration.validators)}
validate={async values => await getFormErrors(form, values, configuration.validators)}
>
{props => {
return (
Expand Down Expand Up @@ -182,12 +182,12 @@ export const RenderComponent: React.FC<IRenderComponentProps> = ({
}

const [{onBlur, onChange}, {error}] = field;
const errors = error || [];

// Allow the value to be overriden.
const _value = value !== undefined ? value : field[0].value;

const callbacks = {onBlur, onChange};
const errors = error?.split('\n') || []; // Reconstruct array.

// In certain cases a component (is not defined as) a component but something else (e.g. a column)
// We deal with these edge cases by extending the schema with a custom (component) type allowing
Expand Down Expand Up @@ -223,7 +223,7 @@ export const RenderComponent: React.FC<IRenderComponentProps> = ({
callbacks={callbacks}
component={component}
form={form}
errors={errors}
errors={errors as unknown as ValidationError[]} // Formik typing issue.
path={path}
value={_value}
setValue={setFieldValue}
Expand Down
Loading

0 comments on commit dfa63b5

Please sign in to comment.