Local state and directives
Create an x-data scope
Declare local state and understand which descendants can read or change it.
x-data is where Alpine state lives. You put it on an element, give it an object, and every element inside can read and change that object.
That “every element inside” part is the whole lesson. Where you put x-data decides who owns the state.
Add Alpine to the page
Alpine is one script tag. Put it in the <head> with defer, so it runs after the HTML is parsed:
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
This loads the latest 3.x release. No build step, no bundler.
The filter panel owns the filter state
The filter panel needs two values: the search text and the selected status. So the x-data goes on the panel:
<form x-data="{ query: '', status: '' }" action="/issues" method="get">
<input name="query" type="search" x-model="query">
<select name="status" x-model="status">
<option value="">All</option>
<option value="open">Open</option>
</select>
<p x-text="query"></p>
</form>
Type in the search box and the paragraph updates. The <p> can read query because it’s inside the element that declared it.
Move that <p> outside the <form> and Alpine throws an error in the console: query is not defined. Nothing outside the scope can see it. That’s a feature.
Each row owns its own state
Every issue row can expand to show details. That’s a separate piece of state, one per row:
<li x-data="{ expanded: false }">
<button @click="expanded = !expanded">Login button unresponsive on Safari</button>
<div x-show="expanded">Reported by Marta, 3 days ago.</div>
</li>
Ten rows, ten independent expanded values. Clicking one row never touches another.
Nested scopes and shadowing
You can nest x-data. An inner scope sees everything the outer scope has, plus its own values. If both declare the same name, the inner one wins. Alpine calls this shadowing.
So if the board declares status for the filter, and a row also declares status for the issue itself, the row’s expressions silently read the row’s value. No error. Just a confusing bug.
My advice: name nested values so they can’t collide. filterStatus on the panel, issueStatus on the row.
Where to put it
Put x-data on the smallest element that contains everything that needs the state. Not on <body>. Not on every single element either.
For the board that means one scope on the filter panel and one scope on each row. Try it on your own page: take one value, move its x-data up and down the tree, and watch which controls can still reach it.
Lesson completed