Skip to content

Commit cc565a3

Browse files
authored
Merge pull request #126 from lambda-curry/codegen-bot/add-phone-input-component
2 parents 461abfe + 8cf0367 commit cc565a3

File tree

11 files changed

+1346
-530
lines changed

11 files changed

+1346
-530
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import { zodResolver } from '@hookform/resolvers/zod';
2+
import { PhoneInput } from '@lambdacurry/forms/remix-hook-form/phone-input';
3+
import { Button } from '@lambdacurry/forms/ui/button';
4+
import type { Meta, StoryObj } from '@storybook/react-vite';
5+
import { expect, userEvent, within } from '@storybook/test';
6+
import { type ActionFunctionArgs, useFetcher } from 'react-router';
7+
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
8+
import { z } from 'zod';
9+
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';
10+
11+
const successMessageRegex = /Form submitted successfully/;
12+
13+
// Define a schema for phone number validation
14+
const formSchema = z.object({
15+
usaPhone: z.string().min(1, 'USA phone number is required'),
16+
internationalPhone: z.string().min(1, 'International phone number is required'),
17+
});
18+
19+
type FormData = z.infer<typeof formSchema>;
20+
21+
const ControlledPhoneInputExample = () => {
22+
const fetcher = useFetcher<{ message: string }>();
23+
const methods = useRemixForm<FormData>({
24+
resolver: zodResolver(formSchema),
25+
defaultValues: {
26+
usaPhone: '',
27+
internationalPhone: '',
28+
},
29+
fetcher,
30+
submitConfig: {
31+
action: '/',
32+
method: 'post',
33+
},
34+
});
35+
36+
return (
37+
<RemixFormProvider {...methods}>
38+
<fetcher.Form onSubmit={methods.handleSubmit}>
39+
<div className="grid gap-8">
40+
<PhoneInput
41+
name="usaPhone"
42+
label="Phone Number"
43+
description="Enter a US phone number"
44+
/>
45+
<PhoneInput
46+
name="internationalPhone"
47+
label="International Phone Number"
48+
description="Enter an international phone number"
49+
isInternational={true}
50+
/>
51+
</div>
52+
<Button type="submit" className="mt-8">
53+
Submit
54+
</Button>
55+
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
56+
</fetcher.Form>
57+
</RemixFormProvider>
58+
);
59+
};
60+
61+
const handleFormSubmission = async (request: Request) => {
62+
const { data, errors } = await getValidatedFormData<FormData>(request, zodResolver(formSchema));
63+
64+
if (errors) {
65+
return { errors };
66+
}
67+
68+
return {
69+
message: `Form submitted successfully! USA: ${data.usaPhone}, International: ${data.internationalPhone}`,
70+
};
71+
};
72+
73+
const meta: Meta<typeof PhoneInput> = {
74+
title: 'RemixHookForm/PhoneInput',
75+
component: PhoneInput,
76+
parameters: { layout: 'centered' },
77+
tags: ['autodocs'],
78+
} satisfies Meta<typeof PhoneInput>;
79+
80+
export default meta;
81+
type Story = StoryObj<typeof meta>;
82+
83+
export const Default: Story = {
84+
decorators: [
85+
withReactRouterStubDecorator({
86+
routes: [
87+
{
88+
path: '/',
89+
Component: ControlledPhoneInputExample,
90+
action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request),
91+
},
92+
],
93+
}),
94+
],
95+
parameters: {
96+
docs: {
97+
description: {
98+
story: 'Phone input component with US and international number support.',
99+
},
100+
source: {
101+
code: `
102+
const formSchema = z.object({
103+
usaPhone: z.string().min(1, 'USA phone number is required'),
104+
internationalPhone: z.string().min(1, 'International phone number is required'),
105+
});
106+
107+
const ControlledPhoneInputExample = () => {
108+
const fetcher = useFetcher<{ message: string }>();
109+
const methods = useRemixForm<FormData>({
110+
resolver: zodResolver(formSchema),
111+
defaultValues: {
112+
usaPhone: '',
113+
internationalPhone: '',
114+
},
115+
fetcher,
116+
submitConfig: {
117+
action: '/',
118+
method: 'post',
119+
},
120+
});
121+
122+
return (
123+
<RemixFormProvider {...methods}>
124+
<fetcher.Form onSubmit={methods.handleSubmit}>
125+
<div className="grid gap-8">
126+
<PhoneInput
127+
name="usaPhone"
128+
label="Phone Number"
129+
description="Enter a US phone number"
130+
/>
131+
<PhoneInput
132+
name="internationalPhone"
133+
label="International Phone Number"
134+
description="Enter an international phone number"
135+
isInternational
136+
/>
137+
</div>
138+
<Button type="submit" className="mt-8">
139+
Submit
140+
</Button>
141+
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
142+
</fetcher.Form>
143+
</RemixFormProvider>
144+
);
145+
};`,
146+
},
147+
},
148+
},
149+
play: async ({ canvasElement, step }) => {
150+
const canvas = within(canvasElement);
151+
152+
await step('Verify initial state', async () => {
153+
// Wait for inputs to be mounted and associated with their labels
154+
const usaPhoneLabel = await canvas.findByLabelText('Phone Number');
155+
const internationalPhoneLabel = await canvas.findByLabelText('International Phone Number');
156+
157+
expect(usaPhoneLabel).toBeInTheDocument();
158+
expect(internationalPhoneLabel).toBeInTheDocument();
159+
160+
// Wait for submit button to be present
161+
const submitButton = await canvas.findByRole('button', { name: 'Submit' });
162+
expect(submitButton).toBeInTheDocument();
163+
});
164+
165+
await step('Test validation errors on invalid submission', async () => {
166+
// Submit form without entering phone numbers
167+
const submitButton = await canvas.findByRole('button', { name: 'Submit' });
168+
await userEvent.click(submitButton);
169+
170+
// Verify validation error messages appear
171+
await expect(canvas.findByText('USA phone number is required')).resolves.toBeInTheDocument();
172+
await expect(canvas.findByText('International phone number is required')).resolves.toBeInTheDocument();
173+
});
174+
175+
await step('Test successful form submission with valid phone numbers', async () => {
176+
// Enter valid phone numbers (await the inputs before typing)
177+
const usaPhoneInput = await canvas.findByLabelText('Phone Number');
178+
const internationalPhoneInput = await canvas.findByLabelText('International Phone Number');
179+
180+
// Enter a US phone number (should format to (202) 555-0123)
181+
await userEvent.type(usaPhoneInput, '2025550123');
182+
183+
// Enter an international phone number (UK example digits; component will normalize & format with + and spaces)
184+
await userEvent.type(internationalPhoneInput, '7911123456');
185+
186+
// Submit form
187+
const submitButton = await canvas.findByRole('button', { name: 'Submit' });
188+
await userEvent.click(submitButton);
189+
190+
// Verify success message (regex matches the prefix of the success text)
191+
await expect(canvas.findByText(successMessageRegex)).resolves.toBeInTheDocument();
192+
});
193+
},
194+
};
195+
196+
export const WithCustomStyling: Story = {
197+
decorators: [
198+
withReactRouterStubDecorator({
199+
routes: [
200+
{
201+
path: '/',
202+
},
203+
],
204+
}),
205+
],
206+
render: () => {
207+
const fetcher = useFetcher<{ message: string }>();
208+
const methods = useRemixForm<FormData>({
209+
resolver: zodResolver(formSchema),
210+
defaultValues: {
211+
usaPhone: '',
212+
internationalPhone: '',
213+
},
214+
fetcher,
215+
submitConfig: {
216+
action: '/',
217+
method: 'post',
218+
},
219+
});
220+
221+
return (
222+
<RemixFormProvider {...methods}>
223+
<fetcher.Form onSubmit={methods.handleSubmit}>
224+
<div className="grid gap-8">
225+
<PhoneInput
226+
name="usaPhone"
227+
label="Custom Styled Phone Input"
228+
description="With custom styling applied"
229+
className="border-2 border-blue-500 p-4 rounded-lg"
230+
inputClassName="bg-gray-100"
231+
/>
232+
<PhoneInput
233+
name="internationalPhone"
234+
label="Custom Styled Intl Phone Input"
235+
description="With custom styling applied"
236+
isInternational
237+
className="border-2 border-blue-500 p-4 rounded-lg"
238+
inputClassName="bg-gray-100"
239+
/>
240+
</div>
241+
<Button type="submit" className="mt-8">
242+
Submit
243+
</Button>
244+
</fetcher.Form>
245+
</RemixFormProvider>
246+
);
247+
},
248+
parameters: {
249+
docs: {
250+
description: {
251+
story: 'Phone input with custom styling applied for US and International modes.',
252+
},
253+
},
254+
},
255+
};

0 commit comments

Comments
 (0)