Events and forms
Handle events with intent
Use x-on and modifiers for a reason instead of attaching broad listeners everywhere.
x-on listens for an event on an element and runs an expression. The short form is @. @click="open = true" is the whole API.
Most of the power sits in the modifiers, the dot suffixes after the event name. Each one changes one thing about how the browser event is handled. Use them when the interaction needs them, not as a reflex.
Let’s wire up the issue editor. The contract: Escape closes it, clicking outside dismisses it, and the form owns submission.
Start from the browser’s event flow
A click on a button inside the editor fires on the button, then bubbles up to the editor, then to the document. A submit event fires on the form. Escape fires a keydown on whatever has focus.
Every modifier below changes one step of that flow. If you can’t say which step, you don’t need the modifier.
Escape closes the editor
Alpine has key modifiers. .escape filters keydown to that one key. .window moves the listener to the window, so it works no matter which field has focus:
<div x-data="{ editing: false }" @keydown.escape.window="editing = false">
Without .window, Escape only works while focus is inside the element. Since the editor might not have focus yet, window is the right target here.
Click outside dismisses it
.outside fires when a click lands anywhere except inside the element:
<form x-show="editing" @click.outside="editing = false">
I wrote about this in why I use Alpine.js. In vanilla JavaScript it’s a document listener plus a contains() check plus an id. Here it’s one attribute on the element it belongs to.
The form owns submission
Pressing Enter in a field submits the form. So does the Save button. Both fire one submit event on the form. Listen there, not on the button:
<form @submit.prevent="save()">
<input name="title" x-model="title">
<button>Save</button>
</form>
.prevent calls event.preventDefault(), which stops the browser from reloading the page. That’s the one place it belongs.
Don’t sprinkle .prevent and .stop
The common failure is adding .prevent and .stop to every handler until the weird behavior goes away. .stop calls stopPropagation(), and now a parent that needed that click never sees it. Analytics break. The outside-click handler breaks.
My rule: .prevent only where the browser default is wrong. .stop almost never.
Here’s the editor with exactly the modifiers it needs:
<div x-data="{ editing: false, title: '' }" @keydown.escape.window="editing = false">
<button @click="editing = true">Edit</button>
<form x-show="editing" @click.outside="editing = false" @submit.prevent="save()">
<input name="title" x-model="title">
<button>Save</button>
</form>
</div>
Read each modifier and say aloud what it changes. If you can do that for every one, the event handling is intentional.
Lesson completed