Skip to content

Commit 19c1adf

Browse files
authored
feat(SubmitError): add component CC-1392 (#227)
1 parent ab200ae commit 19c1adf

File tree

6 files changed

+117
-3
lines changed

6 files changed

+117
-3
lines changed

.changeset/wicked-wombats-prove.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@cube-dev/ui-kit": patch
3+
---
4+
5+
Add `SubmitError` component to display error that throws onSubmit callback.
6+
Allow to manually visualize a submit error.

src/components/forms/Form/ComplexForm.stories.tsx

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { StoryFn } from '@storybook/react';
22
import { linkTo } from '@storybook/addon-links';
3+
import { userEvent, waitFor, within } from '@storybook/testing-library';
4+
import { expect } from '@storybook/jest';
35

46
import {
7+
Alert,
58
Block,
69
Checkbox,
710
CheckboxGroup,
@@ -11,11 +14,12 @@ import {
1114
Item,
1215
PasswordInput,
1316
Radio,
17+
RangeSlider,
1418
Select,
1519
Submit,
20+
SubmitError,
1621
Switch,
1722
TextInput,
18-
RangeSlider,
1923
} from '../../../index';
2024
import { NumberInput } from '../NumberInput/NumberInput';
2125
import { baseProps } from '../../../stories/lists/baseProps';
@@ -28,6 +32,56 @@ export default {
2832
parameters: { controls: { exclude: baseProps } },
2933
};
3034

35+
const CustomSubmitErrorTemplate: StoryFn<typeof Form> = (args) => {
36+
const [form] = Form.useForm();
37+
38+
return (
39+
<Form
40+
form={form}
41+
{...args}
42+
onSubmit={(v) => {
43+
console.log('onSubmit:', v);
44+
45+
throw <>Submission failed. Sorry for that :/</>;
46+
}}
47+
onValuesChange={(v) => {
48+
console.log('onChange', v);
49+
}}
50+
>
51+
<Field name="text" label="Text input">
52+
<TextInput />
53+
</Field>
54+
<Submit>Submit</Submit>
55+
{form.submitError ? <Alert>{form.submitError}</Alert> : null}
56+
</Form>
57+
);
58+
};
59+
60+
const SubmitErrorTemplate: StoryFn<typeof Form> = (args) => {
61+
const [form] = Form.useForm();
62+
63+
return (
64+
<Form
65+
form={form}
66+
{...args}
67+
onSubmit={(v) => {
68+
console.log('onSubmit:', v);
69+
70+
throw <>Submission failed. Sorry for that :/</>;
71+
}}
72+
onValuesChange={(v) => {
73+
console.log('onChange', v);
74+
}}
75+
>
76+
<Field name="text" label="Text input">
77+
<TextInput />
78+
</Field>
79+
<Submit>Submit</Submit>
80+
<SubmitError />
81+
</Form>
82+
);
83+
};
84+
3185
const AsyncValidationTemplate: StoryFn<typeof Form> = (args) => {
3286
const [form] = Form.useForm();
3387

@@ -277,3 +331,16 @@ export const Default = Template.bind({});
277331
export const ComplexErrorMessage = ComplexErrorTemplate.bind({});
278332

279333
export const AsyncValidation = AsyncValidationTemplate.bind({});
334+
335+
export const ErrorMessage = SubmitErrorTemplate.bind({});
336+
337+
export const CustomErrorMessage = CustomSubmitErrorTemplate.bind({});
338+
339+
ErrorMessage.play = CustomErrorMessage.play = async ({ canvasElement }) => {
340+
const canvas = within(canvasElement);
341+
const button = await canvas.getByRole('button');
342+
343+
await userEvent.click(button);
344+
345+
await waitFor(() => expect(canvas.getByRole('alert')).toBeInTheDocument());
346+
};

src/components/forms/Form/Form.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import {
33
createContext,
44
FormHTMLAttributes,
55
forwardRef,
6+
ReactNode,
67
useContext,
78
useEffect,
89
useRef,
10+
useState,
911
} from 'react';
1012
import { DOMRef } from '@react-types/shared';
1113

@@ -104,6 +106,7 @@ function Form<T extends FieldTypes>(
104106
...otherProps
105107
} = props;
106108
const firstRunRef = useRef(true);
109+
const [submitError, setSubmitError] = useState<ReactNode>(null);
107110

108111
ref = useCombinedRefs(ref);
109112

@@ -129,6 +132,8 @@ function Form<T extends FieldTypes>(
129132

130133
if (!form || form.isSubmitting) return;
131134

135+
setSubmitError(null);
136+
form.submitError = null;
132137
form.setSubmitting(true);
133138

134139
try {
@@ -137,10 +142,13 @@ function Form<T extends FieldTypes>(
137142
await onSubmit?.(form.getFormData());
138143
} catch (e) {
139144
await timeout();
145+
140146
if (e instanceof Error) {
141147
throw e;
142148
}
143149
// errors are shown
150+
setSubmitError(e as ReactNode);
151+
form.submitError = e as ReactNode;
144152
// transfer errors to the callback
145153
onSubmitFailed?.(e);
146154
} finally {
@@ -172,6 +180,7 @@ function Form<T extends FieldTypes>(
172180
validateTrigger,
173181
requiredMark,
174182
form,
183+
submitError,
175184
idPrefix: name,
176185
};
177186

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ReactNode, useContext } from 'react';
2+
3+
import { Alert, CubeAlertProps } from '../../content/Alert';
4+
5+
import { FormContext } from './Form';
6+
7+
type SubmitErrorProps = {
8+
submitError?: ReactNode;
9+
};
10+
11+
/**
12+
* An alert that shows a form error message received from the onSubmit callback.
13+
*/
14+
export function SubmitError(props: CubeAlertProps) {
15+
const { submitError } = useContext(FormContext) as SubmitErrorProps;
16+
17+
if (!submitError) {
18+
return null;
19+
}
20+
21+
return (
22+
<Alert theme="danger" {...props}>
23+
{submitError}
24+
</Alert>
25+
);
26+
}

src/components/forms/Form/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1+
import { CubeAlertProps } from '../../content/Alert';
2+
13
import { Field } from './Field';
4+
import { SubmitError } from './SubmitError';
25
import { useForm } from './useForm';
36
import { useFormProps, FormContext, Form as _Form } from './Form';
47

58
const Form = Object.assign(
69
_Form as typeof _Form & {
710
Item: typeof Field;
11+
SubmitError: typeof SubmitError;
812
useForm: typeof useForm;
913
},
10-
{ Item: Field, useForm },
14+
{ Item: Field, useForm, SubmitError },
1115
);
1216

13-
export { useFormProps, Form, Field, useForm, FormContext };
17+
export { useFormProps, Form, Field, useForm, FormContext, SubmitError };
1418
export type { CubeFormProps } from './Form';
1519
export type { CubeFormInstance } from './useForm';
1620
export type { FieldTypes, Fields } from './types';
21+
export type { CubeAlertProps as CubeSubmitErrorProps };

src/components/forms/Form/useForm.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class CubeFormInstance<
3434
private fields: TFormData = {} as TFormData;
3535
public ref = {};
3636
public isSubmitting = false;
37+
public submitError: ReactNode = null;
3738

3839
public onValuesChange: (data: T) => void | Promise<void> = () => {};
3940
public onSubmit: (data: T) => void | Promise<void> = () => {};

0 commit comments

Comments
 (0)