Events and forms

Bind form controls

Use x-model with text, checkbox, radio, select, and modifiers while preserving labels and native form behavior.

x-model keeps a form control and a piece of state in sync, in both directions. Type in the input and the state changes. Change the state and the input updates.

It works on every native control. That’s the point: you keep the real <input>, <select>, and <label> elements, and Alpine binds to them.

Text, select, checkbox, radio

Here is the filter panel with every control type bound:

<form x-data="{ query: '', status: '', mine: false, sort: 'newest' }"
  action="/issues" method="get">

  <label for="query">Search</label>
  <input id="query" name="query" type="search" x-model="query">

  <label for="status">Status</label>
  <select id="status" name="status" x-model="status">
    <option value="">All</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

  <label>
    <input type="checkbox" name="mine" x-model="mine"> Only mine
  </label>

  <label><input type="radio" name="sort" value="newest" x-model="sort"> Newest</label>
  <label><input type="radio" name="sort" value="oldest" x-model="sort"> Oldest</label>

  <button>Filter</button>
</form>

A single checkbox gives you a boolean. Radios bound to the same property give you the value of the checked one. A <select> gives you the selected option’s value.

Notice every control still has a name. Turn JavaScript off and the form submits ?query=safari&status=open&sort=newest to the server. Alpine adds to the form. It doesn’t replace it.

Value types

x-model stores strings by default. For the issue editor’s priority field, I want a number:

<input type="number" name="priority" x-model.number="priority">

Without .number, priority is "2", and priority > 1 works by accident until it doesn’t.

.boolean does the same for "true" and "false" strings coming from a select.

Timing modifiers

By default the state updates on every keystroke. For a search box that triggers work, that’s too eager. .debounce waits until typing pauses:

<input type="search" name="query" x-model.debounce.300ms="query">

.lazy waits until the field loses focus. I use it for validation messages, so an error doesn’t appear after the first letter.

Keep the controls real

The tempting shortcut is replacing controls with <div>s because the binding looks shorter. A <div @click="mine = !mine"> has no label, no keyboard support, no name, and sends nothing when the form submits.

Native controls give you all of that for free. Bind to them.

Try this: bind every control on your filter panel, open the console, and inspect the state with Alpine.$data($0). Then disable JavaScript and submit the form. Both should give you the same values.

Lesson completed