Skip to content

Add model-backed form APIs - #11630

Draft
brophdawg11 wants to merge 19 commits into
mainfrom
brophdawg11/codex-data-model-forms
Draft

Add model-backed form APIs#11630
brophdawg11 wants to merge 19 commits into
mainfrom
brophdawg11/codex-data-model-forms

Conversation

@brophdawg11

Copy link
Copy Markdown
Contributor

Adds low-level model-backed form APIs that connect data-schema models to native HTML controls without taking ownership of application markup. The same form definition derives browser constraints, restores failed values, and validates submitted FormData on the server.

Remix UI progressively enhances the browser's Constraint Validation API with touched-state styling and accessible invalid state while preserving no-JavaScript behavior.

  • adds createForm() projections and getConstraints() schema introspection
  • returns serializable field/form failures while omitting submitted password values
  • adds the form() mixin plus invalid-state support for built-in input and checkbox styles
  • demonstrates the full flow with an in-memory SQLite data-model form demo
  • migrates the bookstore login form to the same model-backed validation path
import * as s from 'remix/data-schema'
import { email } from 'remix/data-schema/checks'
import { createForm } from 'remix/data-schema/form'
import { form } from 'remix/ui/form'
import input from 'remix/ui/input'

let Account = s.object({
  id: s.string(),
  email: s.string().pipe(email()),
})

let AccountForm = createForm(Account, {
  fields: {
    email: { label: 'Email', type: 'email' },
    terms: {
      label: 'Accept the terms',
      type: 'checkbox',
      schema: s.literal(true),
    },
  },
})

let submission = AccountForm.parse(await request.formData())

if (!submission.success) {
  return render(<AccountPage submission={submission} />, { status: 400 })
}

function AccountPage({ submission }) {
  return (
    <form method="post" mix={form()}>
      <label {...AccountForm.getLabelAttrs('email')}>
        {AccountForm.fields.email.label}
      </label>
      <input {...AccountForm.getInputAttrs('email', submission)} mix={input()} />
      <button type="submit">Create account</button>
    </form>
  )
}

@brophdawg11 brophdawg11 self-assigned this Jul 23, 2026
@sergiodxa

sergiodxa commented Jul 23, 2026

Copy link
Copy Markdown
Member

Since labels come from AccountForm, I think this needs to account for the possibility of i18n being used, this means the labels needs to be updatable, receive a locale, or it should be possible to create AccountForm per request and pass it to the UI, including hydrated components.

Since you already have AccountForm.getLabelAttrs I think a AccountForm.getLabel(locale, email) could be useful too.

@brophdawg11

Copy link
Copy Markdown
Contributor Author

Yeah I need to think more thoroughly about i18n but it crossed my mind briefly during implementation and my gut reaction was that if you needed to translate, you could put your slug and you would translate in the UI layer like all other translations

<label>{t(form.firstName.label)}</label>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants