io.select.single

One choice from a list you can search.

io.select.single asks to pick one option from a drop-down you can search. The handler gets the chosen option itself, not a string. Options can be records straight from your database. They stay on the server, and the browser gets only their labels.

The simplest one

The whole action file, and roughly what your team sees when it runs.

src/actions/users/changeRoleAction.ts
import { app } from '../../app';
export const changeRoleAction = app.defineAction({
name: 'Change role',
handler: async ({ io }) => {
const role = await io.select.single('New role', {
options: [
{ value: 'admin', label: 'Admin' },
{ value: 'editor', label: 'Editor' },
{ value: 'viewer', label: 'Viewer' },
],
defaultValue: 'editor',
});
},
});
The code. One call in an action, the whole file.
tools.example.com/actions/users/change_role

Change role

New roleEditor▾
Admin
Editor
Viewer
Continue
In the browser. Roughly what your team sees. Your theme decides the details.

What you can set

The settings you will reach for most, each with the line of code that turns it on.

io.select.single('Country', { options })

Search as you type. Type a few letters and the list narrows to the options that match. Long lists stay easy to use.

label: (account) => account.name

Records from your database. Pass rows as your query returns them. key says what identifies each one and label what the list shows.

const account = await io.select.single(…)

The option itself comes back. The handler gets the same object you passed, typed by options. Relations and dates included.

options: Object.values(BillingType)

Enums name themselves. Strings and numbers need no key or label. The members of an enum work as they are.

defaultValue: user.accountId

A starting choice by id. defaultValue takes the option or just its key. The id stored on a record is enough.

clearable: true

A × to start over. clearable shows a × that empties the field again. It goes best with .optional().

io.select.single(…).optional()

Optional when you say so. The field is required by default. Chain .optional() and an empty field gives undefined.

// Choose New role.

Checked on the server. The browser answers only with the position of the choice. An empty field or a value that was not offered is sent back.

.validate((plan) => message)

Checks of your own. .validate() runs on the server after the built-in checks. Return a message and it shows under the field.

Write your first tool today.

An async function, a few await calls, and the form is in the browser.

AwaitIO is free for small teams, made possible by companies and people who sponsor it.