Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions apps/docs/src/remix-hook-form/hidden-field.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { HiddenField } from '@lambdacurry/forms/remix-hook-form/hidden-field';
import { TextField } from '@lambdacurry/forms/remix-hook-form/text-field';
import { Button } from '@lambdacurry/forms/ui/button';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';

const formSchema = z.object({
// Hidden field still participates in validation and submission
lineItemId: z.string().min(1, 'Missing line item id'),
note: z.string().optional(),
});

type FormData = z.infer<typeof formSchema>;

const DEFAULT_ID = 'abc-123';

const HiddenFieldExample = () => {
const fetcher = useFetcher<{ message: string; submittedId: string }>();
const methods = useRemixForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
lineItemId: DEFAULT_ID,
note: '',
},
fetcher,
submitConfig: {
action: '/',
method: 'post',
},
});

return (
<RemixFormProvider {...methods}>
<fetcher.Form onSubmit={methods.handleSubmit}>
{/* This registers the field without rendering any extra DOM */}
<HiddenField name="lineItemId" />

{/* A visible field just to demonstrate normal layout around the hidden input */}
<div className="space-y-4">
<TextField name="note" label="Note" placeholder="Optional note" />
<Button type="submit">Submit</Button>
{fetcher.data?.message && (
<p className="mt-2 text-green-600">
{fetcher.data.message} – submittedId: {fetcher.data.submittedId}
</p>
)}
</div>
</fetcher.Form>
</RemixFormProvider>
);
};

const handleFormSubmission = async (request: Request) => {
const { data, errors } = await getValidatedFormData<FormData>(request, zodResolver(formSchema));

if (errors) {
return { errors };
}

return { message: 'Form submitted successfully', submittedId: data.lineItemId };
};

const meta: Meta<typeof HiddenField> = {
title: 'RemixHookForm/HiddenField',
component: HiddenField,
parameters: { layout: 'centered' },
tags: ['autodocs'],
decorators: [
withReactRouterStubDecorator({
routes: [
{
path: '/',
Component: HiddenFieldExample,
action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request),
},
],
}),
],
} satisfies Meta<typeof HiddenField>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
parameters: {
docs: {
description: {
story:
'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.',
},
},
},
};
6 changes: 2 additions & 4 deletions packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lambdacurry/forms",
"version": "0.20.1",
"version": "0.21.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand All @@ -26,9 +26,7 @@
"import": "./dist/ui/*.js"
}
},
"files": [
"dist"
],
"files": ["dist"],
"scripts": {
"prepublishOnly": "yarn run build",
"build": "vite build",
Expand Down
35 changes: 35 additions & 0 deletions packages/components/src/remix-hook-form/hidden-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type * as React from 'react';
import type { FieldPath, FieldValues, RegisterOptions } from 'react-hook-form';
import { useRemixFormContext } from 'remix-hook-form';

export interface HiddenFieldProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name' | 'type'> {
/**
* The field name to register with react-hook-form
*/
name: FieldPath<FieldValues>;
/**
* Optional register options to pass to react-hook-form's register
*/
registerOptions?: RegisterOptions;
}

/**
* HiddenField
*
* A minimal field that only renders a native hidden input and registers it with remix-hook-form.
* This avoids any extra DOM wrappers so it won't affect page layout.
*/
export const HiddenField = function HiddenField({
name,
registerOptions,
ref,
...props
}: HiddenFieldProps & { ref?: React.Ref<HTMLInputElement> }) {
const { register } = useRemixFormContext();

// Intentionally render a plain input to avoid any additional wrappers or styling
return <input type="hidden" {...register(name, registerOptions)} ref={ref} {...props} />;
};

HiddenField.displayName = 'HiddenField';
export type { HiddenFieldProps as HiddenInputProps }; // legacy-friendly alias
1 change: 1 addition & 0 deletions packages/components/src/remix-hook-form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export * from './use-data-table-url-state';
export * from './select';
export * from './us-state-select';
export * from './canada-province-select';
export * from './hidden-field';