Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control field validation behaviour #74

Merged
merged 7 commits into from
Mar 11, 2025
5 changes: 5 additions & 0 deletions .storybook/decorators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {Decorator} from '@storybook/react';
import {fn} from '@storybook/test';
import {Form, Formik} from 'formik';
import {CSSProperties} from 'react';
import {toFormikValidationSchema} from 'zod-formik-adapter';

import RendererSettingsProvider from '../src/components/RendererSettingsProvider';

Expand Down Expand Up @@ -34,13 +35,17 @@ export const withFormik: Decorator = (Story, context) => {
const initialTouched = context.parameters?.formik?.initialTouched || {};
const wrapForm = context.parameters?.formik?.wrapForm ?? true;
const onSubmit = context.parameters?.formik?.onSubmit || fn();
const zodSchema = context.parameters?.formik?.zodSchema;
return (
<Formik
initialValues={initialValues}
initialErrors={initialErrors}
initialTouched={initialTouched}
enableReinitialize
onSubmit={async values => onSubmit(values)}
validateOnBlur={false}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not be set by the context parameters?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've hardcoded them to reflect the actual implementation because enabling this validates the whole form rather than just the field that's touched.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh check, got it

validateOnChange={false}
validationSchema={zodSchema ? toFormikValidationSchema(zodSchema) : undefined}
>
{wrapForm ? (
<Form id="storybook-withFormik-decorator-form" data-testid="storybook-formik-form">
Expand Down
33 changes: 32 additions & 1 deletion src/components/forms/TextField/TextField.stories.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Meta, StoryObj} from '@storybook/react';
import {expect, userEvent, within} from '@storybook/test';
import {z} from 'zod';

import {withFormik, withRenderSettingsProvider} from '@/sb-decorators';

Expand Down Expand Up @@ -68,7 +69,7 @@ export const ValidationError: Story = {
},
};

export const NoAsterisks = {
export const NoAsterisks: Story = {
name: 'No asterisk for required',
decorators: [withRenderSettingsProvider],
parameters: {
Expand All @@ -82,3 +83,33 @@ export const NoAsterisks = {
isRequired: true,
},
};

export const ValidateOnBlur: Story = {
args: {
name: 'validateOnBlur',
label: 'Validate on blur',
},
parameters: {
formik: {
initialValues: {
validateOnBlur: '',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it validateOnBlur: true?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formik doesn't seem involved with the validating. So, should this parameter just be removed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay - I understand the confusion!

  1. These are stories for the raw TextField component, which is rendered by the Formio textfield, email... etc.
  2. Any form field rendered with Formik requires at least a name parameter
  3. The name prop is specified as arg on L89. For this test/story purpose, the name is validateOnBlur. Effectively resulting in <input type="text" name="validateOnBlur" />
  4. So, the formik context/parameters from the story expect some initialValues, which is keyed by the field names. Hence, {validateOnBlur: ''} - the field has an empty string as value initially.
  5. So, it doesn't have anything to do with the actual validation, it's just an illustrative form field name.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah hahaha, okay yeah that makes sense 😅

},
zodSchema: z.object({
validateOnBlur: z.any().refine(() => false, {message: 'Always invalid'}),
}),
},
},
play: async ({canvasElement}) => {
const canvas = within(canvasElement);
const input = await canvas.findByLabelText('Validate on blur');
expect(input).not.toHaveAttribute('aria-invalid');

await userEvent.type(input, 'foo');
expect(input).toHaveFocus();
expect(input).not.toHaveAttribute('aria-invalid');

input.blur();
expect(await canvas.findByText('Always invalid')).toBeVisible();
expect(input).toHaveAttribute('aria-invalid', 'true');
},
};
3 changes: 0 additions & 3 deletions src/components/forms/TextField/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,8 @@ const TextField: React.FC<TextFieldProps & TextboxProps> = ({
<Textbox
{...props}
onBlur={async e => {
console.log('onBlur');
props.onBlur(e);
console.log('validating');
await validateField(name);
console.log('validation done');
}}
className="utrecht-textbox--openforms"
id={id}
Expand Down