We apologize for the inconvenience. Please contact us for further assistance.
diff --git a/src/components/Form/ErrorMessage.tsx b/src/components/Form/ErrorMessage.tsx
index 8d2a09cd..30de0914 100644
--- a/src/components/Form/ErrorMessage.tsx
+++ b/src/components/Form/ErrorMessage.tsx
@@ -2,13 +2,15 @@ import * as React from 'react';
import { CircleAlertIcon } from 'lucide-react';
-export const ErrorMessage: React.FC<{ error?: null | string[] }> = ({ error }) => {
+import { cn } from '@/utils';
+
+export const ErrorMessage: React.FC<{ className?: string; error?: null | string[] }> = ({ className, error }) => {
return error ? (
{error.map((message) => (
-
+
- {message}
+ {message}
)) ?? null}
diff --git a/src/components/Form/Form.stories.tsx b/src/components/Form/Form.stories.tsx
index 1a0b6272..ec7cdb5e 100644
--- a/src/components/Form/Form.stories.tsx
+++ b/src/components/Form/Form.stories.tsx
@@ -530,3 +530,27 @@ export const WithSuspend: StoryObj
, { dela
})
}
};
+
+export const WithError: StoryObj = {
+ args: {
+ content: {
+ name: {
+ kind: 'string',
+ label: 'Name',
+ variant: 'input'
+ }
+ },
+ beforeSubmit: (data) => {
+ if (data.name === 'Winston') {
+ return { success: true };
+ }
+ return { success: false, errorMessage: "Name must be 'Winston'" };
+ },
+ onSubmit: () => {
+ alert('Success!');
+ },
+ validationSchema: $SimpleExampleFormData.extend({
+ name: z.string().min(3)
+ })
+ }
+};
diff --git a/src/components/Form/Form.test.tsx b/src/components/Form/Form.test.tsx
index dc3239b0..fb1c93f6 100644
--- a/src/components/Form/Form.test.tsx
+++ b/src/components/Form/Form.test.tsx
@@ -1,6 +1,7 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Mock } from 'vitest';
import { z } from 'zod';
import { Form } from './Form';
@@ -114,4 +115,68 @@ describe('Form', () => {
expect(onSubmit.mock.lastCall?.[0].b).toBeUndefined();
});
});
+
+ describe('custom beforeSubmit error', () => {
+ let beforeSubmit: Mock;
+
+ beforeEach(() => {
+ beforeSubmit = vi.fn();
+ render(
+
+ );
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should render', () => {
+ expect(screen.getByTestId(testid)).toBeInTheDocument();
+ });
+
+ it('should not allow submitting the form with a zod error', async () => {
+ fireEvent.submit(screen.getByTestId(testid));
+ await waitFor(() =>
+ expect(screen.getAllByTestId('error-message-text').map((e) => e.innerHTML)).toMatchObject([
+ 'Please enter a number'
+ ])
+ );
+ expect(beforeSubmit).not.toHaveBeenCalled();
+ expect(onSubmit).not.toHaveBeenCalled();
+ });
+
+ it('should not allow submitting the form with the beforeSubmit error', async () => {
+ beforeSubmit.mockResolvedValueOnce({ errorMessage: 'Invalid!', success: false });
+ const field: HTMLInputElement = screen.getByLabelText('Value');
+ await userEvent.type(field, '-1');
+ fireEvent.submit(screen.getByTestId(testid));
+ await waitFor(() =>
+ expect(screen.getAllByTestId('error-message-text').map((e) => e.innerHTML)).toMatchObject(['Invalid!'])
+ );
+ expect(onSubmit).not.toHaveBeenCalled();
+ });
+
+ it('should allow submitting the form if beforeSubmit returns true', async () => {
+ beforeSubmit.mockResolvedValueOnce({ success: true });
+ const field: HTMLInputElement = screen.getByLabelText('Value');
+ await userEvent.type(field, '-1');
+ fireEvent.submit(screen.getByTestId(testid));
+ await waitFor(() => expect(onSubmit).toHaveBeenCalledOnce());
+ });
+ });
});
diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx
index b014df68..cf42703b 100644
--- a/src/components/Form/Form.tsx
+++ b/src/components/Form/Form.tsx
@@ -29,8 +29,13 @@ type FormProps, TData extends z.TypeOf) => Promisable<{ errorMessage: string; success: false } | { success: true }>;
className?: string;
content: FormContent;
+ customStyles?: {
+ resetBtn?: string;
+ submitBtn?: string;
+ };
fieldsFooter?: React.ReactNode;
id?: string;
initialValues?: PartialNullableFormDataType>;
@@ -47,8 +52,10 @@ type FormProps, TData extends z.TypeOf, TData extends z.TypeOf = z.TypeOf>({
additionalButtons,
+ beforeSubmit,
className,
content,
+ customStyles,
fieldsFooter,
id,
initialValues,
@@ -73,6 +80,7 @@ const Form = , TData extends z.TypeOf) => {
const fieldErrors: FormErrors = {};
+ const rootErrors: string[] = [];
for (const issue of error.issues) {
if (issue.path.length > 0) {
const current = get(fieldErrors, issue.path) as string[] | undefined;
@@ -82,10 +90,11 @@ const Form = , TData extends z.TypeOf [...prevErrors, issue.message]);
+ rootErrors.push(issue.message);
}
}
setErrors(fieldErrors);
+ setRootErrors(rootErrors);
if (onError) {
onError(error);
}
@@ -107,6 +116,15 @@ const Form = , TData extends z.TypeOf, TData extends z.TypeOf
)}
{fieldGroup.description && (
- {fieldGroup.description}
+ {fieldGroup.description}
)}
, TData extends z.TypeOf
)}
+ {Boolean(rootErrors.length) && }
{fieldsFooter}
{additionalButtons?.left}
{/** Note - aria-label is used for testing in downstream packages */}