Skip to main content

Guide | Build Forms

Compose forms offer:

  • A streamlined way to group inputs and collect data as a single, sanitized data structure.
  • Built-in validation methods that will surface errors to the user based on your business logic.
  • Layout customization to control how inputs are arranged in the form.
  • End-to-end type safety with TypeScript.
function createUser() {
page.modal(({ resolve }) =>
ui.form(
"create-user-form",
[
ui.textInput("name"),
ui.emailInput("email"),
ui.radioGroup("role", ["Admin", "User", "Guest"]),
],
{
onSubmit: (form) => {
users = [insertUser(form.name, form.email, form.role), ...users]
page.toast("User created successfully", { appearance: "success" })
page.update() // refresh table with new user
resolve() // close modal
}
}
),
{ title: "Create User" }
)
}

page.add(() => ui.table("users", users))
page.add(() => ui.button("createUser", { onClick: createUser }))
tip

Place forms inside modals, then trigger the modal from a button or table action. The example above demonstrates this pattern. Also see the CRUD example app for a more in-depth example.

Validate forms

Forms include a validate hook that can be used to validate your forms using your own business logic.

page.add(() => 
ui.form(
"create-user-form",
[
ui.radioGroup("role", ["Admin", "User", "Guest"]),
ui.numberInput("age"),
],
{
validate: (form) => {
if (form.role === "Admin" && form.age < 18) {
// display error to user
return "Admins must be at least 18 years old"
}

return; // or return undefined to indicate no errors
},
onSubmit: (form) => createUser(form)
}
)
)

Many components also include built in validators. For example:

  • ui.emailInput and ui.urlInput will validate that the input is a valid email or URL.
  • ui.fileDrop includes parameters to limit the accepted file types, or control the min/max count of submitted files.

Set initial input values

You can specify the initial value of an input by setting the initialValue parameter for an input.

function editUser(user) {
page.modal(({ resolve }) =>
ui.form(
"edit-user",
[
ui.textInput("name", { initialValue: user.name }),
ui.emailInput("email", { initialValue: user.email }),
],
{
onSubmit: (form) => {
editUser(user.id, form.name, form.email)
resolve() // close modal
}
}
)
)
}

Change input values programmatically

You can change the current value of an input programmatically at any time using the page.setInputs method.

page.add(() =>
ui.selectBox(
"email-template",
["Invoice", "Welcome", "Order Confirmation"],
{
onChange: (value) => {
// Pass an object that maps input IDs to the new values.
page.setInputs({
"emailBody": emailTemplates[value]
})
}
}
)
)

page.add(() =>
ui.form(
"email-form",
[
ui.textInput("emailSubject"),
ui.textArea("emailBody"),
],
{
onSubmit: (form) => {
sendEmail(form.emailSubject, form.emailBody)
}
}
)
)
warning

Notice that ui.selectBox is not part of the form. This is intentional since inputs inside of forms will ignore their own callbacks such as onChange.

Customize form layout

Forms have no special requirements for customizing the layout. Feel free to nest layout components and leverage styling parameters to customize the layout. For example:

page.add(() => ui.form(
"form-id",
[
ui.textInput("addressLineOne"),
ui.textInput("addressLineTwo"),
// render inputs in a row
ui.row(
[
ui.textInput("city"),
ui.selectBox("state", LIST_OF_STATES),
ui.numberInput("zipCode"),
],
{
spacing: "8px" // reduce spacing between inputs from default 16px
}
),
ui.row(
ui.submitButton("save"),
{ justify: "end" } // align submit button to the right
)
],
{
spacing: "24px", // increase spacing between rows from default 16px
onSubmit: (form) => saveAddress(form)
}
))

Customize submit button

Forms render with a submit button at the bottom by default. You can override the default position and styling of the button by including your own ui.submitButton component anywhere inside the form.

Or you can use the properties.hideSubmitButton parameter to hide the submit button entirely.

page.add(() => ui.form(
"form-id",
[
// Render the submit button at the top of the form with custom styling.
ui.submitButton("submit", { label: "Save changes", appearance: "primary" }),
ui.textInput("firstName")
]
))

Submit buttons are a thin wrapper on top of the ui.button component, and include all of the same parameters.

Using inputs without a form

There are many situations where you may want to use an input without it being part of a form. For example, a custom dropdown that filters table values, or a persistent file drop that uploads files to a database as soon as they are dropped.

That's completely possible, and quite straightforward. Simple use the input component directly, and hook into the callback functions exposed directly by the input component.

let users = fetchUsers()

function filterUsers(filter) {
users = users.filter(user => user.role === filter)
page.update()
}

page.add(() =>
ui.selectBox(
"role",
["Admin", "User", "Guest"],
{
onChange: filterUsers
}
)
)

page.add(() => ui.table("users", users))

Notes and best practices

  • Inputs nested inside a form component will ignore their own direct callback functions such as onEnter and onChange.
  • Forms cannot be nested inside other forms.