Skip to content

v0.32.0

Choose a tag to compare

@cuonggt cuonggt released this 01 Oct 10:12
· 33 commits to main since this release

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's a.guestsOnly(a.login), keeps tug from seeing. The route needs its name first, Name(...).Takes(...), as the types are by name: Takes panics 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, and BindValid, 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 a Takes the 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.
  • Inputs and form(), in routes.ts, which tug gen writes. An input's keys are the names validate gives its errors: a field's json name, else its form tag's, else its own, which is how Bind reads 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, in form()'s params, and an upload is a File, or File[], as the auth starter's PhotoInput has photo: File | null. form() is the route's method as it's declared, 'put' for a Put, 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 take form(), 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 Takes and form(); 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.