π§© Alpine.js: Reusable Patterns
Build Once, Use Everywhere!
π― The Big Idea
Imagine you have a toy factory. Instead of building each toy from scratch every time, you create molds (templates). Pour in the material, and out comes a perfect toy!
Alpine.js reusable patterns work the same way:
Alpine.data()= Your toy mold (reusable component logic)Alpine.bind= Your sticker sheets (reusable attribute bundles)
Build once. Use everywhere. Save time. Stay happy! π
π Part 1: Components with Alpine.data
What Is Alpine.data?
Think of Alpine.data() as a recipe card for your components.
Without a recipe: Every time you bake cookies, you guess the ingredients. Sometimes good, sometimes disaster! πͺπ₯
With a recipe: Same delicious cookies every single time! πͺβ¨
<!-- Define the recipe once -->
<script>
Alpine.data('counter', () => ({
count: 0,
increment() {
this.count++
},
decrement() {
this.count--
}
}))
</script>
<!-- Use it anywhere! -->
<div x-data="counter">
<button @click="decrement">-</button>
<span x-text="count"></span>
<button @click="increment">+</button>
</div>
Why This Rocks
| Problem Without | Solution With Alpine.data |
|---|---|
| Copy-paste code everywhere | Write once, use many times |
| Fix bugs in 10 places | Fix once, works everywhere |
| Messy HTML | Clean, readable code |
π¦ Making Components Accept Options
Your recipe can have variations! Like vanilla OR chocolate cookies.
<script>
Alpine.data('counter', (start = 0) => ({
count: start,
increment() { this.count++ },
reset() { this.count = start }
}))
</script>
<!-- Vanilla: starts at 0 -->
<div x-data="counter()">...</div>
<!-- Chocolate: starts at 100! -->
<div x-data="counter(100)">...</div>
The start = 0 part means: βIf nobody tells me where to start, use 0.β
π Components with init()
Some components need to wake up and do something first. Like turning on the oven before baking!
<script>
Alpine.data('fetchUser', () => ({
user: null,
loading: true,
init() {
// This runs automatically when
// the component appears!
fetch('/api/user')
.then(r => r.json())
.then(data => {
this.user = data
this.loading = false
})
}
}))
</script>
<div x-data="fetchUser">
<p x-show="loading">Loading...</p>
<p x-show="user" x-text="user?.name"></p>
</div>
init() = The βget readyβ function. Runs once when your component starts.
π§Ή Cleanup with destroy()
Sometimes you need to clean up when leaving. Like turning off the lights when you leave a room!
<script>
Alpine.data('timer', () => ({
seconds: 0,
interval: null,
init() {
this.interval = setInterval(() => {
this.seconds++
}, 1000)
},
destroy() {
// Stop the timer when
// component is removed!
clearInterval(this.interval)
}
}))
</script>
π¨ Part 2: Alpine.bind Directive Bundles
The Problem: Repeating Yourself
Ever typed the same thing over and over?
<!-- Ugh! Same stuff everywhere! -->
<button type="button"
class="btn btn-primary"
:disabled="loading"
@click="submit">
Save
</button>
<!-- Again?! -->
<button type="button"
class="btn btn-primary"
:disabled="loading"
@click="submit">
Update
</button>
The Solution: Bundle It Up!
Alpine.bind() lets you package a group of attributes into one neat bundle.
<script>
Alpine.bind('primaryButton', () => ({
'type': 'button',
'class': 'btn btn-primary',
':disabled': 'loading',
}))
</script>
<!-- So clean! -->
<button x-bind="primaryButton" @click="save">
Save
</button>
<button x-bind="primaryButton" @click="update">
Update
</button>
π What Can You Bundle?
| Type | Example |
|---|---|
| Static attributes | type, class, id |
| Dynamic bindings | :disabled, :class |
| Event listeners | @click, @input |
| Alpine directives | x-show, x-text |
<script>
Alpine.bind('searchInput', () => ({
'type': 'search',
'placeholder': 'Search...',
'@input': 'search($event.target.value)',
'@keyup.escape': 'clear()',
':class': '{ active: hasResults }'
}))
</script>
<input x-bind="searchInput">
π Dynamic Bundles
Your bundles can be smart and change based on data!
<script>
Alpine.bind('dropdown', () => ({
'@click': 'open = !open',
':aria-expanded': 'open',
':class': '{
"dropdown-open": open,
"dropdown-closed": !open
}'
}))
</script>
π Combining Both Powers
The real magic happens when you use both together!
<script>
// The component logic (the brain)
Alpine.data('modal', () => ({
isOpen: false,
open() { this.isOpen = true },
close() { this.isOpen = false }
}))
// The attribute bundle (the appearance)
Alpine.bind('modalTrigger', () => ({
'@click': 'open()',
'type': 'button',
'class': 'btn-open-modal'
}))
Alpine.bind('modalBackdrop', () => ({
'x-show': 'isOpen',
'@click': 'close()',
'class': 'modal-backdrop'
}))
</script>
<div x-data="modal">
<button x-bind="modalTrigger">
Open Modal
</button>
<div x-bind="modalBackdrop">
<div class="modal-content">
Hello!
<button @click="close()">Γ</button>
</div>
</div>
</div>
π Quick Summary
graph TD A["π Reusable Patterns"] --> B["Alpine.data"] A --> C["Alpine.bind"] B --> D["Component Logic"] B --> E["init & destroy"] B --> F["Accept Parameters"] C --> G["Attribute Bundles"] C --> H["Group Directives"] C --> I["Stay DRY"]
| Feature | Purpose | Think of it as⦠|
|---|---|---|
Alpine.data() |
Reusable component logic | Recipe cards πͺ |
Alpine.bind() |
Reusable attribute bundles | Sticker sheets π·οΈ |
init() |
Setup when component starts | Turning on the oven |
destroy() |
Cleanup when component removed | Turning off lights |
π Pro Tips
-
Name things clearly -
dropdown,modal,searchInputare better thanthing1 -
Keep components focused - One job per component!
-
Use parameters - Make your components flexible with options
-
Clean up after yourself - Always use
destroy()for timers, listeners, etc. -
Combine wisely -
Alpine.datafor logic,Alpine.bindfor appearance
π You Did It!
You now know how to:
- β
Create reusable components with
Alpine.data() - β Pass options to customize components
- β
Use
init()anddestroy()lifecycle methods - β
Bundle attributes with
Alpine.bind() - β Keep your code DRY (Donβt Repeat Yourself)
Build once. Use everywhere. Be happy! π
