Skip to content

Commit ee9891f

Browse files
authored
Merge pull request #145 from lambda-curry/codegen-bot/add-hidden-field-component-1758302032
2 parents 443f20e + 6533d4b commit ee9891f

File tree

4 files changed

+136
-4
lines changed

4 files changed

+136
-4
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { zodResolver } from '@hookform/resolvers/zod';
2+
import { HiddenField } from '@lambdacurry/forms/remix-hook-form/hidden-field';
3+
import { TextField } from '@lambdacurry/forms/remix-hook-form/text-field';
4+
import { Button } from '@lambdacurry/forms/ui/button';
5+
import type { Meta, StoryObj } from '@storybook/react-vite';
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 formSchema = z.object({
12+
// Hidden field still participates in validation and submission
13+
lineItemId: z.string().min(1, 'Missing line item id'),
14+
note: z.string().optional(),
15+
});
16+
17+
type FormData = z.infer<typeof formSchema>;
18+
19+
const DEFAULT_ID = 'abc-123';
20+
21+
const HiddenFieldExample = () => {
22+
const fetcher = useFetcher<{ message: string; submittedId: string }>();
23+
const methods = useRemixForm<FormData>({
24+
resolver: zodResolver(formSchema),
25+
defaultValues: {
26+
lineItemId: DEFAULT_ID,
27+
note: '',
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+
{/* This registers the field without rendering any extra DOM */}
40+
<HiddenField name="lineItemId" />
41+
42+
{/* A visible field just to demonstrate normal layout around the hidden input */}
43+
<div className="space-y-4">
44+
<TextField name="note" label="Note" placeholder="Optional note" />
45+
<Button type="submit">Submit</Button>
46+
{fetcher.data?.message && (
47+
<p className="mt-2 text-green-600">
48+
{fetcher.data.message} – submittedId: {fetcher.data.submittedId}
49+
</p>
50+
)}
51+
</div>
52+
</fetcher.Form>
53+
</RemixFormProvider>
54+
);
55+
};
56+
57+
const handleFormSubmission = async (request: Request) => {
58+
const { data, errors } = await getValidatedFormData<FormData>(request, zodResolver(formSchema));
59+
60+
if (errors) {
61+
return { errors };
62+
}
63+
64+
return { message: 'Form submitted successfully', submittedId: data.lineItemId };
65+
};
66+
67+
const meta: Meta<typeof HiddenField> = {
68+
title: 'RemixHookForm/HiddenField',
69+
component: HiddenField,
70+
parameters: { layout: 'centered' },
71+
tags: ['autodocs'],
72+
decorators: [
73+
withReactRouterStubDecorator({
74+
routes: [
75+
{
76+
path: '/',
77+
Component: HiddenFieldExample,
78+
action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request),
79+
},
80+
],
81+
}),
82+
],
83+
} satisfies Meta<typeof HiddenField>;
84+
85+
export default meta;
86+
87+
type Story = StoryObj<typeof meta>;
88+
89+
export const Default: Story = {
90+
parameters: {
91+
docs: {
92+
description: {
93+
story:
94+
'HiddenField renders a plain <input type="hidden" /> registered with remix-hook-form. Use it to submit values that should not impact layout or be visible to users.',
95+
},
96+
},
97+
},
98+
};

packages/components/package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lambdacurry/forms",
3-
"version": "0.20.1",
3+
"version": "0.21.0",
44
"type": "module",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",
@@ -26,9 +26,7 @@
2626
"import": "./dist/ui/*.js"
2727
}
2828
},
29-
"files": [
30-
"dist"
31-
],
29+
"files": ["dist"],
3230
"scripts": {
3331
"prepublishOnly": "yarn run build",
3432
"build": "vite build",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type * as React from 'react';
2+
import type { FieldPath, FieldValues, RegisterOptions } from 'react-hook-form';
3+
import { useRemixFormContext } from 'remix-hook-form';
4+
5+
export interface HiddenFieldProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type'> {
6+
/**
7+
* The field name to register with react-hook-form
8+
*/
9+
name: FieldPath<FieldValues>;
10+
/**
11+
* Optional register options to pass to react-hook-form's register
12+
*/
13+
registerOptions?: RegisterOptions;
14+
}
15+
16+
/**
17+
* HiddenField
18+
*
19+
* A minimal field that only renders a native hidden input and registers it with remix-hook-form.
20+
* This avoids any extra DOM wrappers so it won't affect page layout.
21+
*/
22+
export const HiddenField = function HiddenField({
23+
name,
24+
registerOptions,
25+
ref,
26+
...props
27+
}: HiddenFieldProps & { ref?: React.Ref<HTMLInputElement> }) {
28+
const { register } = useRemixFormContext();
29+
30+
// Intentionally render a plain input to avoid any additional wrappers or styling
31+
return <input type="hidden" {...register(name, registerOptions)} ref={ref} {...props} />;
32+
};
33+
34+
HiddenField.displayName = 'HiddenField';
35+
export type { HiddenFieldProps as HiddenInputProps }; // legacy-friendly alias

packages/components/src/remix-hook-form/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ export * from './use-data-table-url-state';
2222
export * from './select';
2323
export * from './us-state-select';
2424
export * from './canada-province-select';
25+
export * from './hidden-field';

0 commit comments

Comments
 (0)