Skip to main content

Guide | Build Forms

Forms are the preferred method for working with multiple input components. They make it easy to add advanced validation, use submit buttons, and output form values as a single, sanitized data structure.

You can use forms by nesting inputs inside a ui.form component.

import { Page, UI } from "@composehq/sdk"

async function handler({ page, ui }: { page: Page, ui: UI }) {
function validateAge(age) {
if (age < 13) {
return "Minimum required age is 13.";
}
return true
}

function validateForm(form) {
if (form.files.length === 0 && form.age < 18) {
return "Minors are required to upload a parental consent form.";
}
return true;
}

page.add(() =>
ui.form(
"form-id",
[
ui.textInput("firstName"),
ui.textInput("lastName", { required: false }),
ui.numberInput("age", { validate: validateAge }),
ui.fileDrop("files", { label: "Drop consent form here.", maxCount: 1, required: false }),
],
{
validate: validateForm,
onSubmit: form => console.log(form) // form is a dict of form values keyed by input ID.
}
)
)
}

Custom validation

You can validate forms at both the input and form level. The code above gives examples of both.

In addition to custom validation, many components also expose built in validators such as the maxCount property exposed by the ui.fileDrop component. When possible, it's better to use these as we can validate inputs against them client side without requiring a network call to the server.

When a form is invalid, Compose will display the error to the user. Once a form submission is valid, Compose will call the onSubmit callback with the sanitized form object.

Submitting forms

Forms include a submit button at the bottom by default, but you can override the default position by including your own ui.submitButton component anywhere inside the form. You can also 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.
ui.submitButton("submit"),
ui.textInput("firstName")
]
))

Controlled input values

Compose inputs are typically uncontrolled, meaning values are managed by Compose and passed back to your app via hooks like onSubmit and onChange. This is intentional, given Compose's architecture.

If necessary, input values can be set anytime using the page.setInputs method, which accepts a dictionary of input ids to values.

import { Page, UI } from "@composehq/sdk"
import { faker } from "@faker-js/faker"

async function handler({ page, ui }: { page: Page, ui: UI }) {
page.add(() => ui.form(
"form-id",
ui.textInput("username"),
))

page.add(() => ui.button("randomize", {
label: "Randomize username",
onClick: () => {
page.setInputs({
"username": faker.internet.userName()
})
}
}))
}

Using inputs outside of forms

If you only need to use a single input component, there's usually no need for nesting that input inside a form. Instead, use the individual callbacks such as onEnter and onChange exposed by the components themselves.

You can still use the validate parameter to validate input values. Similar to forms, the output callbacks (e.g. onEnter) won't be called until an input is valid.

Notes

  • Inputs nested inside a form component will not trigger individual callbacks such as onEnter and onChange.
  • Inputs can be deeply nested inside a form component (e.g. a ui.textInput inside a ui.stack inside a ui.form) while still being part of the form.
  • Forms cannot be nested inside other forms.