Tips for real-world Alpine.js
Alpine Day 2021 Talk: tackling Alpine FAQs, common issues & web patterns with Alpine.js
Table of Contents
1. What even is Proxy {}
?
Demo: https://codepen.io/hugodf/pen/dyvqPbw
<div x-data="{ obj: { count: 1 } }">
<button @click="console.log(obj)">Proxy</button>
</div>
On click, console output:
Proxy { <target>: {}, <handler>: {} }
Proxy: a JavaScript object which enables you to wrap JS objects and intercept operations on it
This is core to Alpine’s reactivity, proxies allow Alpine to detect data updates.
How do I use the data in the Proxy?
Demo: https://codepen.io/hugodf/pen/vYxzEEw
<div x-data="{ obj: { count: 1 } }">
<button @click="obj.count++" x-text="obj.count"></button>
</div>
The same as you would use the data if it wasn’t in a proxy.
Note: a Proxy and the object it’s wrapping are indistinguishable, trust me I tried to do it to make alpine devtools perform better
How do I print the data in the Proxy?
Demos: https://codepen.io/hugodf/pen/yLMxyeo
Print it as a string
<button
@click="console.log(JSON.stringify(obj))"
>
stringified
</button>
Output: '{"count":1}'
(string)
Unfurl the proxy
<button
@click="console.log(JSON.parse(JSON.stringify(obj)))"
>
unfurled
</button>
Output: Object { count: 1 }
(JavaScript Object)
Note: “unfurl” fancy way of saying unwrap, we’re really doing a deep clone of the data using serialisation/deserialisation -> it’s actually another object instance
Debugging tip
<pre x-text="JSON.stringify(obj, null, 2)"></pre>
Don’t forget null, 2
it does the pretty-printing.
Output:
{
"count": 1
}
Note: null, 4
for 4-space indented output
2. Fetch data
How do I load data with Alpine?
The fetch
API is a native way to load data in modern browsers from JavaScript.
Note: you don’t “use Alpine” to load data, fetch
is a browser API
I want to load & list books that match “Alpine” from Google Books API
Let me initialise a books
array
<div
x-data="{
books: []
}"
>
</div>
Note: I know I’m going to want to store some books
On component startup (x-init
), load data from Google Book Search API & extract the response
<div
x-data="{
books: []
}"
x-init="
fetch('https://www.googleapis.com/books/v1/volumes?q=Alpine')
.then(res => res.json())
.then(res => console.log(res))
"
>
</div>
https://codepen.io/hugodf/pen/BaWOrMX
Note: fetch
returns a Promise and so does res.json()
, hence the use of .then
to access the result of the operation.
Output:
Note: JavaScript object with items array of obj that contains volumeInfo
Store the volumeInfo
of each items
as books
.
<div
x-data="{
books: []
}"
x-init="
fetch('https://www.googleapis.com/books/v1/volumes?q=Alpine')
.then(res => res.json())
.then(res => {
books = res.items.map(item => item.volumeInfo)
})
"
>
<div x-text="JSON.stringify(books)"></div>
</div>
https://codepen.io/hugodf/pen/QWpVwXQ
Note: as far as we’re concerned, volumeInfo is the book
Output:
We can clean up the output with x-for
+ x-text
instead of dumping the data.
<ul>
<template x-for="book in books">
<li x-text="book.title"></li>
</template>
</ul>
https://codepen.io/hugodf/pen/YzZOKgE
Output:
How about a loading state?
Pattern:
- action causes a data load
- set loading = true & start the data load
- receive/process the data
- loading = false, data = newData
<div
x-data="{
books: [],
isLoading: false
}"
x-init="
isLoading = true;
fetch('https://www.googleapis.com/books/v1/volumes?q=Alpine')
.then(res => res.json())
.then(res => {
isLoading = false;
books = res.items.map(item => item.volumeInfo)
})
"
Note: we initialised isLoading to false, before fetching set it to true and once we’ve got the relevant data set it to false again
<div x-show="isLoading">Loading...</div>
<ul>
<template x-for="book in books">
<li x-text="book.title"></li>
</template>
</ul>
https://codepen.io/hugodf/pen/dyvqPxY
Promises/data fetching in JavaScript can easily fill a whole other talk.
See codewithhugo.com/async-js for a deeper look at topics such as fetching in parallel & delaying execution of a Promise.
3. Send and handle events
One of the other key Alpine features: the ability to send and receive events using x-on
+ $dispatch
.
Alpine -> Alpine events
$dispatch('event-name', 'event-data')
Creates and sends an “event-name” event with “event-data” as the “detail”.
The 2nd parameter (“detail”) doesn’t need to be a string, it can be an object too.
$dispatch('event-name', { count: 1 })
Receiving events using x-on
.
<div
x-on:peer-message.window="msg = $event.detail"
x-data="{ msg: '' }"
>
<div x-text="msg"></div>
</div>
<button
x-data
@click="$dispatch('peer-message', 'from-peer')"
>
Send peer message
</button>
https://codepen.io/hugodf/pen/NWpLPXj
When to use .window
?
When the element dispatching the event is not a child/descendant of the one that should receive it.
Example of when .window
is not necessary
<div
x-on:child-message="msg = $event.detail"
x-data="{ msg: '' }"
>
<div x-text="msg"></div>
<button
x-data
@click="$dispatch('child-message', 'from-child')"
>
Send message to parent
</button>
</div>
https://codepen.io/hugodf/pen/NWpLPXj
The button
(element that dispatches “child-message”) is a descendant/child of the div
with x-on:child-message
.
The name for this is “event bubbling”
Bubbling: browser goes from the element on which an event was triggered up its ancestors in the DOM triggering the relevant event handler(s).
If the element with the listener (x-on
) is not an ancestor of the dispatching element, the event won’t bubble up to it.
Note: bring up event bubbling and why you need to use .window in certain cases. I feel like that gets confusing for people
JavaScript -> Alpine events
How is $dispatch
implemented? (See the source)
el.dispatchEvent(new CustomEvent(event, {
detail,
bubbles: true,
}))
We can do the same in our own JavaScript
<button id="trigger-event">Trigger from JS</button>
<script>
const btnTrigger = document.querySelector('#trigger-event')
btnTrigger.addEventListener('click', () => {
btnTrigger.dispatchEvent(
new CustomEvent('peer-message', {
detail: 'from-js-peer',
bubbles: true
})
)
})
</script>
https://codepen.io/hugodf/pen/NWpLPXj
Alpine -> JavaScript events
We can use document.addEventListener
and read from the event.detail
(same as when using x-on
).
<div id="listen">Listen target from JS</div>
<script>
const listenTarget = document.querySelector('#listen')
document.addEventListener('peer-message', (event) => {
listenTarget.innerText = event.detail
})
</script>
https://codepen.io/hugodf/pen/NWpLPXj
Event name x-on quirks
HTML is case-insensitive and x-on:event-name
is a HTML attribute.
To avoid surprises, use dash-cased or namespaced event names, eg. hello-world
or hello:world
instead of helloWorld
.
4. x-show
vs x-if
What can you do with x-show
that you can’t with x-if
and vice versa?
Concept: short-circuit/guard
Example:
function short(maybeGoodData) {
if (!maybeGoodData) return [];
return maybeGoodData.map(...)
}
If maybeGoodData
is null
we won’t get a “Cannot read property ‘map’ of null” because the .map
branch doesn’t get run.
The “if” is sometimes called a guard or guard clause & the whole pattern is sometimes called “short-circuiting return”, it avoids running code that could cause errors.
x-if
doesn’t evaluate anything inside the template if x-if
evaluates to false
.
Same as the guard in our short
function it helps us skip evaluations that could be dangerous.
x-show
keeps evaluating everything (x-show only toggles display: none
).
<div x-data="{ obj: null }">
<template x-if="obj">
<div x-text="obj.value"></div>
</template>
<span x-text="'not crashin'">crashed</span>
</div>
<div x-data="{ obj: null }">
<div x-show="obj">
<div x-text="obj.value"></div>
</div>
<span x-text="'not crashin'">crashed</span>
</div>
https://codepen.io/hugodf/pen/vYxzOze
x-if
doesn’t crash
x-show
does due to obj.value
Performance
Depending on what you’re doing you might find that x-show
or x-if
yields better performance.
x-show
toggles a single style
property.
x-if
inserts/removes DOM Nodes.
If you’re having performance issues with one, try the other.
Note: Kevin in devtools perf issue https://github.com/alpine-collective/alpinejs-devtools/pull/182/files
Useful links
- “slides”: codewithhugo.com/alpine-tips
- all the demos: codepen.io/collection/ZMMNgj
- Async JS: codewithhugo.com/async-js
Photo by Jeremy Bishop on Unsplash
Interested in Alpine.js?
Subscribe to Alpine.js Weekly. A free, once–weekly email roundup of Alpine.js news and articles