Skip to content

Reactivity

When you define a form with components & layouts, Iridium uses your definition to create a navigable component tree at run time to handle your form's request lifecycles.

This allows you to write reactive callbacks on your form components that can navigate and mutate different form elements when a user interacts with them.

Users then will see these changes live as they interact with your form. On a reasonably fast connection, it will appear like field validation & reactivity is happening client-side, while it actually happens server-side.

Benefits

  • A server-side based reactive form requires you the developer to only write field level validation and reactivity one time (on the server in Go), rather than twice (once in JS on the client for reactivity and validation, and then once in Go for validation).
    • Since you only write field validation on the server, there can be no mismatch between client/server side validation which helps prevent regressions & bugs.

Drawbacks

  • Server-side reactivity is generally limited by how fast your network connection is. On slow connections, your form can take a bit to update after a user has interacted with it.

What can you do?

  • Validate a field's data as a user interacts with it to immediately notify them if they have made an invalid choice.
  • Display/Hide certain form elements based on the state of others.
  • Mutate the current and other form values based on the state of your form.

AfterStateUpdated

Form fields contain an AfterStateUpdated hook that allows you to write a custom callback that uses the current request to mutate it's and other form's values.

Live

After writing a AfterStateUpdated method, if you want user input on that form element to trigger its callback in a live manner, you'll need to include the Live() method on your field. See more about live fields here.

Mutate the current value

Here we've setup the this FormInput to always upper case whatever text is written into it:

go
// Will run `AfterStateUpdated` if a user changes the `name`
TextInput("name").
    Live().
    AfterStateUpdated(func(state string, ctx FieldContext) (string, error) {
        return strings.ToUpper(state), nil
    },
),

Mutate another fields value

A basic example is as follows. When the user updates the field "name", it will set another form field called "First Name" with the first word in the name.

go
// Will run `AfterStateUpdated` if a user changes the `name`
TextInput("name").
    Live().
    AfterStateUpdated(func(state string, ctx FieldContext) (string, error) {
        names := strings.Split(state, " ")
        ctx.Set("first_name", names[0])
        return state, nil
    },
),

// The value of `first_name` is updated via 
// the `ctx.Set` function inside name's AfterStateUpdated callback
TextInput("first_name").
    Label("First Name"),

Nesting

Navigation is done by relative string paths. Here's an example with a layout, which nests fields inside itself

go
TextInput("name").
    Live().
    AfterStateHydrated(func (value string, ctx FieldContext) (string, error){
        names := strings.Split(value, " ")
        if len(names) < 2 {
            return value, errors.New("Name does not have a last name")
        }
        ctx.Set("./details/last_name", names[1])
        return value, nil
    }),
FormGrid("details").
    Schema(
        TextInput("first_name").
            LabelFn(func(ctx FieldContext) string {
                val, err := ctx.Get("../name")
                if err != nil {
                    return "", err
                }
                names := strings.Split(val, " ")
                return names[0]
            }
        ),
        TextInput("last_name")
    )

This examples shows how to navigate your tree to access different fields.

  • Name -> navigates into the details grid to find the last_name via ./details/last_name
  • First Name -> navigates up & out of the grid to the name component to get it's value

Note: If last_name wanted to access the first_name field, it could do so with ctx.Get("first_name") since it's pathing is relative to itself.

Tip

Notice how the last_name field inside the grid has it's data pushed to it via the name's Set call, and the first_name field fetches the name via it's Get function? You're able to pass data around in different ways within your form.

Note: first_name's LabelFn function is re-run on each render, hence it will always check the name field before formatting it's own value.

Chains

When using ctx.Set, inside one form element to set the value of another, it will also run any AfterStateUpdated methods attached to that form element. This chain will continue for as many fields exist with AfterStateUpdated methods that set each other's values

Example

go
TextInput("name").
    Live().
    AfterStateUpdated(func(state string, ctx FieldContext) (string, error) {
        ctx.Set("city", "hi")
        return state, nil
    },
),
TextInput("city").
    Live().
    AfterStateUpdated(func(state string, ctx FieldContext) (string, error) {
        // This `AfterStateUpdated` method was fired when you changed the 
        // value of this field in `AfterStateUpdated` method inside 
        // of the name field
        fmt.Print(state) // state is now "hi". 
        return state, nil
    },
),

Infinite Loops

You're probably wondering, and yes, you absolutely can chain together AfterStateUpdated functions forever, if two fields continually update each other, or some cycle appears.

This will blow up your stack and panic your app, so please be mindful of this pitfall. Please test your forms before shipping them!

Chain Validation

When interacting with a form component via a live request, that & only that fields validation is run. However, if that field affects other fields as well by setting their values, the validation callbacks on those fields will run as well.

Iridium does not validate the entire form during live requests to avoid blasting the users with errors, if they start editing only one field.

Live

Released under the MIT License.