(Updated: )
/ #alpinejs #javascript 

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:

Google Books Response

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:

Books Array value

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:

Book list

How about a loading state?

Pattern:

  1. action causes a data load
  2. set loading = true & start the data load
  3. receive/process the data
  4. 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

Photo by Jeremy Bishop on Unsplash

Author

Hugo Di Francesco

Co-author of "Professional JavaScript", "Front-End Development Projects with Vue.js" with Packt, "The Jest Handbook" (self-published). Hugo runs the Code with Hugo website helping over 100,000 developers every month and holds an MEng in Mathematical Computation from University College London (UCL). He has used JavaScript extensively to create scalable and performant platforms at companies such as Canon, Elsevier and (currently) Eurostar.

Interested in Alpine.js?

Subscribe to Alpine.js Weekly. A free, once–weekly email roundup of Alpine.js news and articles