Repository navigation
v0.32.0
Typed forms: tug gen types what a form sends, the struct its route's handler binds. A route says it with Takes, as the auth starter's login does, and routes.ts gets the struct, Inputs, each route that takes one by its name, and form(), a route's path and method as a form's action. React's <Form> takes the type of what it sends, and then only its keys, for errors, resetOnError and clearErrors, so a field misspelled, or renamed in Go and not in the form, is a type error, where it was a form that sent what its handler didn't read, and an error never shown.
type LoginInput struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
Remember bool `json:"remember"`
}
app.Post("/login", a.guestsOnly(a.login)).Name("login.store").Takes(LoginInput{})export interface LoginInput {
email: string
password: string
remember: boolean
}
export interface Inputs {
'login.store': LoginInput
// …
}<Form<Inputs['login.store']> action={form('login.store')} resetOnError={['password']}>
{({ errors }) => <InputError message={errors.email} />}
</Form>The same form in an app made with v0.32.0, with a slip in each name:
resources/js/pages/Auth/Login.tsx(61,24): error TS2820: Type '"passwrd"' is not assignable to type 'keyof LoginInput'. Did you mean '"password"'?
resources/js/pages/Auth/Login.tsx(78,43): error TS2551: Property 'emial' does not exist on type 'FormDataErrors<LoginInput>'. Did you mean 'email'?
What's new:
Route.Takes: declares the struct a route's handler binds, by a value of it, which a wrapper around the handler, as the auth starter'sa.guestsOnly(a.login), keeps tug from seeing. The route needs its name first,Name(...).Takes(...), as the types are by name:Takespanics on a route with none, and on a value that isn't a struct, as the app adds its routes.- Checked as it's bound:
Bind, andBindValid, fail, as a 500 whose error names both, a handler that binds a struct of body fields other than the one its route takes, so aTakesthe handler has moved on from shows at its first request, in the app's tests:tug: route "posts.store" takes main.PostInput, by Takes, but its handler binds main.DraftInput. A struct of query or path fields alone, as a list's filters, or a post's ID from its path, is let through. Inputsandform(), inroutes.ts, which tug gen writes. An input's keys are the namesvalidategives its errors: a field's json name, else its form tag's, else its own, which is howBindreads a form too. A field whose form or query tag names it otherwise stops tug gen, as the form would send it by one name and its errors would come back by another. A path field is the route's, inform()'s params, and an upload is aFile, orFile[], as the auth starter'sPhotoInputhasphoto: File | null.form()is the route's method as it's declared,'put'for aPut, which a form can't now come apart from.- The starters' forms: their 16 routes that bind input take it, every form's action is
form()'s, and React's forms are typed by their route's input, as<Form<Inputs['login.store']>>. Vue's and Svelte's<Form>take no type, in Inertia 3.7.1, so theirs takeform(), and their errors are any string's;useForm<Inputs['login.store']>types a form's data in all three.examples/inertia's post form is typed too. - The guide: TypeScript, a section on forms; Forms, its example with
Takesandform(); Routing,Takes; and the README.
Upgrading: nothing an app calls changed, and a route that takes nothing is as it was: go get github.com/cuonggt/tug@v0.32.0. tug gen writes Inputs and form() into routes.ts from then on, which the app commits as before. To type a form, its route Takes what its handler binds, after its name, and the form's action is form()'s, in place of route() and a method; in React, <Form<Inputs['the.route']>>. A route at a time does, and the starters' forms show each kind. A struct whose json and form tags name a field two ways is the one change tug gen can ask of an app, once the struct is taken.
The guide is in docs/. tug needs Go 1.26.