Summary
@stacksjs/components' <Form> publishes a prop surface it does not honour. FormProps and the runtime formPropSchema both declare initialValues, validateOnChange, validateOnBlur, onSubmit and onError; Form.stx reads none of them. FieldProps.touched is declared and never produced. FormHelpers promises setValues/resetForm; defineExpose ships setErrors/reset. Passing any of these is a silent no-op — prop validation accepts them, TypeScript accepts them, and nothing happens.
Impact (real app)
analyticshq validates only inside the submit handler, on every form, because there is no blur hook to hang anything on:
resources/views/register.stx:23-26— password length is checked after the button is pressed and nowhere else.resources/views/reset-password.stx:58-65— you can type two mismatched passwords, tab away, and only learn on submit.grep -rn "touched|dirty|pristine" resources/returns nothing.
When we went looking for an existing primitive, FormProps.validateOnBlur and FieldProps.touched in the components package read as exactly the answer. They are not; they are declarations with no implementation behind them, which costs more than their absence would have.
Evidence in stx
packages/components/src/ui/form/index.ts:31-40—FormPropsdeclaresinitialValues,validateOnChange,validateOnBlur,onSubmit,onError.packages/components/src/ui/form/index.ts:42-49—FieldPropsdeclarestouched?: boolean.packages/components/src/ui/form/index.ts:53-61—formPropSchemaregistersinitialValues: PropTypes.object,validateOnChange: PropTypes.boolean,validateOnBlur: PropTypes.boolean,onSubmit: PropTypes.func,onError: PropTypes.func, so prop validation actively accepts all five.packages/components/src/ui/form/Form.stx:2-5— the entire prop surface the component reads:export const action = $props.action || '' export const method = $props.method || 'POST' export const validationSchema = $props.validationSchema || {} export const className = $props.className || ''initialValues,validateOnChange,validateOnBlur,onSubmitandonErrorappear nowhere else in the file.packages/components/src/ui/form/Form.stx:15-17— the only state iserrors,isSubmitting,submitCount. Notouched, nodirty.packages/components/src/ui/form/Form.stx:49-70—onSubmitis a local handler bound to the<form>'s submit event; the declaredonSubmitprop is never consulted, and the component emits'submit'/'error'instead.packages/components/src/ui/form/Form.stx:84—defineExpose({ errors, isSubmitting, submitCount, setErrors, reset, validateForm })vsFormHelpersat index.ts:23-27 (setErrors,setValues,resetForm). Two of three names differ andsetValuesdoes not exist.
Prior art
- React Hook Form —
formState.touchedFields,formState.isDirty,mode: 'onBlur' | 'onChange' | 'onSubmit'. - VeeValidate —
meta.touched/meta.dirtyper field. - Angular —
ng-touched/ng-dirty/ng-pristineclasses andcontrol.touched.
Ask
- Implement the declared props in
Form.stx: seed values frominitialValues, track per-fieldtouched/dirty, and re-validate onchange/blurwhenvalidateOnChange/validateOnBlurare set. - Reconcile
defineExposewithFormHelpers— either rename tosetValues/resetFormor correct the interface, and add whichever is missing. - Honour or delete the
onSubmit/onErrorprops; today they are shadowed by theemit('submit')/emit('error')channel and a caller who passes them gets silence. - Until (1) lands, remove the unimplemented entries from
formPropSchemaso prop validation stops accepting props the component ignores — a hard error is better than a no-op. useFormin@stacksjs/composablesalready implements this exact model (touched, dirty,validateOn: 'change'|'blur'|'submit', per-fielderrorId/inputProps());<Form>should delegate to it rather than carry a second, weaker copy.
Related: stacksjs/stacks#1940