Skip to content

Forms

Iridium's Forms allow you to use a DSL to define different form elements, how they appear, interact, hydrate, etc.

Getting Started

Each form and all it's dependent structs like fields, layouts, actions, etc. are typed with the struct you provide when instantiating them.

Essentially, you're letting Iridium know what the underlying model your form will operate over. It will then use reflection to automatically retrieve and set those fields during different phases of your form's lifecycle.

An example explains this best:

go
// Underlying model/struct for your form is `User`
type User struct {
    Name string
    Email string
    Active bool
}

// A form definition that will act over your `User` model/struct
// allowing you to create, view, and edit users.
Form(). 
    Schema(
        FormInput("Name"),    // Reads/Writes `Name` field on User struct
        FormInput("Email"),   // Reads/Writes `Email` field on User struct
        FormSwitch("Active"), // Reads/Writes `Active` field on User struct
    )

Form() is a method created through type generation. It's actually equivalent to form.NewForm[models.User](nil), but this is aliased to reduce visual noise.

Form components

A form's Schema allows you to list form components that will then be displayed. They are split into two categories:

Fields

Layouts

Naming Components

The names components receive, like FormInput("Name"), have special properties and cavets:

Binding

To bind a component name to a struct's field, their names must be exact matches:

go
type User struct {
    Name string
}

FormInput("Name") // <- Correct, this will bind.
FormInput("name") // <- Will not bind.

Orphaned Names

Form components do not need a matching struct field to exist. In fact, orphaned components are extremely useful for reactive forms and for use with your form's hooks.

For example orphaned components can:

  • Provide additional form data that is not directly saved to a model
  • Used to configure other form elements

Example:

While the 'Over25' field is never directly saved to your User model, it can be used to prevent ages under 25 from being entered if it is toggled. You'll want to see the rules & reactivity sections to understand how to do this.

go
type User struct {
    Age int
}

Form().
    Schema(
        FormSwitch("Over25"), // Temporary - Will not be saved
        FormInput("Age")      // Will be saved
    )

Relationships

Names can also represent relationships, which allow you to source data from embedded structs, or from database relationships.

go
type Person struct {
    Name string
    Car Car 
}
type Car struct {
    Make string
}

// A form for the Person struct
Form().
    Schema(
        FormInput("Name"),      // Name of the person
        FormInput("Car.Make")   // Make of their car
    )

You can learn more about form relationships here

Data Sources / Drivers

Forms are often registered with Iridium drivers in order to populate and persist data automatically and to different sources. Drivers are bridges between Iridium and a particular data source like a database, in-memory array, file, etc.

See our drivers section for a better understanding of how to create a driver for your form.

Registering a driver

You can register a driver with your form, by passing it directly in the Form() call:

go
// Get a GORM-based driver for the User struct
userDriver := gormdriver.Get[User]()

// Register that userDriver with your form
Form(userDriver).
    Schema()

Driverless forms

Forms do not require drivers.

go
Form() // This form is driverless

However, to get much use out of them, you'll still need to provide a Fill or FillModel methods directly on the form in order to fill fields with data. See hooks for more details.

Driverless forms can be useful when you don't need to use a dedicated driver for a particular form. This might be because Iridium's provided drivers, like GORM and Array, don't match your preferred data source, and you don't want to create a custom driver yourself, or because you don't need a data source at all behind a form (say you're just getting values to launch a process).



Common Methods

Schema

The Schema method defines the actual form's schema. It accepts a list of components to populate the form with:

go
Form().
    Schema(
        FormSwitch("Active"),
        FormTime("DOB"),
        FormGrid("my-grid").
           Schema(
                TextInput("Name")
           ),
    )

Fixed Columns

The FixedColumns method defines how many columns the form has.

go
// static
Form().
    FixedColumns(3).
    Schema(
        // All three will be side-by-side in the form
        FormInput("Name"),
        FormTime("DOB"),
        FormSwitch("Active"),
    )

// callback
Form().
    FixedColumns(func (ctx *ctxPage.FormSubmit[T]) int {
        return 3
    })

Columns

The Columns method defines how many columns the form has using tailwind's reactive grid-col class. The columns of the grid will react based on the view port.

go
// static
Form().
    Columns(map[string]int{
        "xs": 1,
        "sm": 2,
        "lg": 4,
    }).
    Schema(
        // one top of each other for xs view port
        // Name + DOB same line for sm view port
        // All three on same line for lg view port
        FormInput("Name"),
        FormTime("DOB"),
        FormSwitch("Active"),
    )
    
// callback
Form().
    ColumnsFn(func (ctx *ctxPage.FormSubmit[T]) map[string]int {
        return map[string]int{
            "xs": 1,
            "sm": 2,
            "lg": 4,
        }
    })

Driver

The Driver method allows you to set the driver of the form

go
// get a driver
myDriver := gormDriver.Get[MyModel]()

Form().
    Driver(myGormDriver)

Alternative

The driver is generally set not with the Driver call, but instead the Form call like so:

go
myDriver := gormDriver.Get[MyModel]()
Form(myDriver)

Disabled

The Disabled method will disable the form and apply disabled to all the form components:

go
// static
Form().
    Disabled()
    
// callback
Form().
    DisabledFn(func (ctx *ctxPage.FormSubmit[T]) bool {
        return true
    })



Form Lifecycle Hooks

Forms have a series of hooks that allow you to further control the form's different lifecycles.

Forms have three core lifecycles:

  1. Fill - Load the initial form and populate the fields
  2. Action - Submit the form
  3. Live - Perform a live request

Different lifecycles run different hooks sequentially:

LifecycleHooks
LoadFill / FillModel
MutateFormDataBeforeFill
AfterFill
ActionMutateFormDataBeforeValidation
BeforeAction
Action
AfterAction
LiveOnly runs field level lifecycle hooks

If any hook returns an error or fails, it short-circuits the execution and returns an error to the client

Data Hooks

Used to define where the form's initial data comes from.

Fill

Prefer FillModel when possible

Using the Fill method allows you to directly provide the url.Values used to populate your form's fields.

go
Form().
    Schema(
        FormInput("Name"),
    ).
    Fill(func(ctx *ctxPage.FormSubmit[T]) (url.Values, error) {
        // return a map[string][]string a.k.a url.Values here that directly matches your form elements
        return map[string][]string{
            "Name": []string{"Larry Smith"},
        }, nil
    })

FillModel

Fill model allows you to provide a model you create or source form elsewhere, that is then interpreted by Iridium into url.Values.

It's the preferred way to load data into your form from a struct since it lets Iridium handle the complex struct -> url.Values parsing for you.

go
Form().
    Schema(
        FormInput("Name"),
    ).
    FillModel(func(ctx *ctxPage.FormSubmit[T]) (*T, error) {
        return &T{
            Name: "Hello",
        }, nil
    })

Action Hooks

These hooks only run on a final submission (e.g., clicking Submit) and after your field's validation rules & form's validation hooks have passed.

BeforeAction

BeforeAction is a hook that runs just before the Action callback is called.

go
Form().
    BeforeAction(func(model *T, ctx *ctxPage.FormSubmit[T]) error {
		// You could log something out here, or one last change to the model!
	})

Action

The primary operation to perform once the form is submitted. Usually, you'd call your driver to persist the model here, but you can do anything.

go
// This action will create a model
Form(myDriver).
    Action(func(model *T, ctx *ctxPage.FormSubmit[T]) error {
        _, err := ctx.Driver.Create(model)
        return err
    }).

Drivers

Often you'll call directly on your driver from the ctxPage.FormSubmit[T] context here to save/update/upsert your model. The driver you instantiated your form with is passed all the way through to that ctx for that purpose.

AfterAction

Runs after Action, if Action did not return an error.

This is a good place to write a log statement, issue a redirect using the ctx, or send a notification.

go
Form(myDriver).
    AfterAction(func(model *T, ctx *ctxPage.FormSubmit[T]) error {
	    ctx.HxRedirect("/admin/")
        return nil
	})

Model mutations in Action

Be mindful of model state changes that occur during Action, especially when using a database driver.

The model instance passed to AfterAction references the Go struct as it existed before persistence, not necessarily the version after the database operation.

For example, GORM may mutate the model during save/update hooks (e.g., setting IDs, timestamps, defaults). The GORM driver returns an updated model reference after these operations.

If your AfterAction logic depends on the model’s final persisted state, you should use the model returned by the driver rather than the original struct passed into the hook.

Hydration Hooks

These hooks run relate to filling schema components with data.

MutateFormDataBeforeFill

Modify the raw url.Values before they are applied to the model (e.g., force a value, normalize input, format strings).

go
Form().
    MutateFormDataBeforeFill(func(data url.Values, ctx *ctxPage.FormSubmit[models.User]) error {
		
	})

AfterFill

Runs after the model struct has been hydrated with the form values.

go
Form().
    AfterFill(func(ctx *ctxPage.FormSubmit[models.User]) error {
		
	})

Validation Hooks

Validation hooks relate to validating each form's fields during submission. Fields contain their own rules to check their validity, but these are additional hooks that allow you to mutate all the fields togther prior to validation.

MutateFormDataBeforeValidation

MutateFormDataBeforeValidation allows you to change the form data before running your fields validation hooks.

go
Form().
    MutateFormDataBeforeValidation(func(data url.Values, ctx *ctxPage.FormSubmit[T]) error {})

Released under the MIT License.