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
50 changes: 49 additions & 1 deletion packages/fresh/src/application/form/_internal/prop-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/** Props applied to the root form element. */
export interface FormElementProps {
/** Additional standard, ARIA, data, or framework attributes for the form element. */
readonly [attribute: string]: unknown;
/** DOM id for the form element. */
readonly id: string;
/** Submission URL. */
Expand All @@ -8,6 +10,40 @@ export interface FormElementProps {
readonly method: string;
/** Disable native browser validation when framework validation owns errors. */
readonly noValidate: boolean;
/** CSS class forwarded to the form element. */
readonly class?: string;
/** Submit handling is owned by the managed form runtime. */
readonly onSubmit?: never;
/** Blur capture handling is owned by progressive enhancement. */
readonly onBlurCapture?: never;
/** Input capture handling is owned by progressive enhancement. */
readonly onInputCapture?: never;
/** Form refs are owned by progressive enhancement. */
readonly ref?: never;
}

/** Caller overrides accepted by the managed form component's `formProps` bag. */
export interface FormElementOverrideProps {
/** Additional standard, ARIA, data, or framework attributes for the form element. */
readonly [attribute: string]: unknown;

@augmentcode augmentcode Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packages/fresh/src/application/form/_internal/prop-types.ts:28 — The open index signature on FormElementOverrideProps means callers can pass framework-only props like dangerouslySetInnerHTML/children, which can prevent the managed hidden inputs (submission id + CSRF) from rendering. Consider explicitly preventing those props in formProps similar to how handler/ref keys are blocked/stripped.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

/** DOM id override for the form element. */
readonly id?: string;
/** Submission URL override. */
readonly action?: string;
/** HTTP method override. */
readonly method?: string;
/** Native validation override. */
readonly noValidate?: boolean;
/** CSS class forwarded to the form element. */
readonly class?: string;
/** Submit handling is owned by the managed form runtime. */
readonly onSubmit?: never;
/** Blur capture handling is owned by progressive enhancement. */
readonly onBlurCapture?: never;
/** Input capture handling is owned by progressive enhancement. */
readonly onInputCapture?: never;
/** Form refs are owned by progressive enhancement. */
readonly ref?: never;
}

/** Props for the hidden CSRF token input. */
Expand Down Expand Up @@ -165,7 +201,19 @@ export interface IntentButtonProps {
}

/** Form element props after progressive enhancement handlers are attached. */
export interface EnhancedFormProps extends FormElementProps {
export interface EnhancedFormProps {
/** Additional standard, ARIA, data, or framework attributes for the form element. */
readonly [attribute: string]: unknown;
/** DOM id for the form element. */
readonly id: string;
/** Submission URL. */
readonly action: string;
/** HTTP method used by the form. */
readonly method: string;
/** Disable native browser validation when framework validation owns errors. */
readonly noValidate: boolean;
/** CSS class forwarded to the form element. */
readonly class?: string;
/** Fresh client-navigation flag. */
readonly 'f-client-nav'?: boolean;
/** Fresh partial route name. */
Expand Down
1 change: 1 addition & 0 deletions packages/fresh/src/application/form/_internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type {
EnhancedFormProps,
ErrorProps,
FormCsrfInputProps,
FormElementOverrideProps,
FormElementProps,
IntentButtonProps,
LabelProps,
Expand Down
41 changes: 41 additions & 0 deletions packages/fresh/src/application/form/components/form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,47 @@ Deno.test('Form renders framework-owned submission and csrf hidden inputs', () =
assert(html.includes('class="demo-form"'), `Expected forwarded class in ${html}`);
});

Deno.test('Form forwards formProps attrs to the rendered form without dropping hidden inputs', () => {
const runtime = resolveRuntimeFormState(
replyFor<{ name?: string }>().initial({
values: { name: 'Ada' },
initialValues: { name: 'Ada' },
submissionId: 'sub-attrs',
csrfToken: 'csrf-attrs',
}),
{
id: 'runtime-form',
action: '/dashboard/framework/forms',
},
);

const html = renderToString(
<Form
state={runtime}
formProps={{
id: 'managed-form',
class: 'managed-form',
target: '_self',
'aria-label': 'Profile form',
'data-form-kind': 'profile',
}}
>
<input type='text' name='name' defaultValue='Ada' />
</Form>,
);

assert(html.includes('<form'), `Expected form element in ${html}`);
assert(html.includes('id="managed-form"'), `Expected formProps id in ${html}`);
assert(html.includes('class="managed-form"'), `Expected formProps class in ${html}`);
assert(html.includes('target="_self"'), `Expected formProps target in ${html}`);
assert(html.includes('aria-label="Profile form"'), `Expected formProps aria attr in ${html}`);
assert(html.includes('data-form-kind="profile"'), `Expected formProps data attr in ${html}`);
assert(html.includes('name="__submission_id__"'), `Expected submission id input in ${html}`);
assert(html.includes('value="sub-attrs"'), `Expected submission id value in ${html}`);
assert(html.includes('name="__csrf__"'), `Expected csrf input in ${html}`);
assert(html.includes('value="csrf-attrs"'), `Expected csrf value in ${html}`);
});

Deno.test('createFormEnhancementSnapshot strips runtime descriptors to a serializable shape', () => {
const runtime = resolveRuntimeFormState(
replyFor<{ name?: string }>().initial({
Expand Down
13 changes: 10 additions & 3 deletions packages/fresh/src/application/form/components/form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SUBMISSION_ID_FIELD_NAME } from '../runtime/idempotency.ts';
import type {
FormElementProps,
FormElementOverrideProps,
FormEnhancementSnapshot,
FormEnhancementState,
FormValues,
Expand Down Expand Up @@ -31,7 +31,7 @@ export interface FormProps<TValues extends FormValues> extends Partial<Record<st
/** Form controls and content rendered inside the managed form element. */
readonly children: FormContent;
/** Optional override for framework-managed form attributes. */
readonly formProps?: Partial<FormElementProps>;
readonly formProps?: FormElementOverrideProps;
}

/** Render a managed form element with submission and CSRF hidden inputs. */
Expand All @@ -42,10 +42,17 @@ export function Form<TValues extends FormValues>({
formProps: formPropOverrides,
...props
}: FormProps<TValues>): object {
const {
onSubmit: _formPropsOnSubmit,
onBlurCapture: _formPropsOnBlurCapture,
onInputCapture: _formPropsOnInputCapture,
ref: _formPropsRef,
...formElementOverrides
} = formPropOverrides ?? {};
const formProps = {
...state.formProps,
...(enhancement?.formProps ?? {}),
...(formPropOverrides ?? {}),
...formElementOverrides,
...props,
};

Expand Down
1 change: 1 addition & 0 deletions packages/fresh/src/application/form/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export type {
FormCollectionStrategy,
FormCollectionStrategyMode,
FormCsrfInputProps,
FormElementOverrideProps,
FormElementProps,
FormEnhancementOptions,
FormEnhancementSnapshot,
Expand Down
1 change: 1 addition & 0 deletions packages/fresh/src/application/form/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type {
FormCollectionStrategy,
FormCollectionStrategyMode,
FormCsrfInputProps,
FormElementOverrideProps,
FormElementProps,
FormEnhancementOptions,
FormEnhancementSnapshot,
Expand Down
Loading