Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Components with Alpine.data

When the object in x-data grows to a dozen lines, the markup becomes hard to read. Alpine.data() registers a component under a name: data and methods live in JavaScript, and the HTML keeps a short x-data="todos". The same component can be placed on the page several times.

Registering with the CDN build

The component has to be registered before Alpine walks the page. That is what the alpine:init event is for — Alpine sends it right before starting. The code goes into a regular <script> tag in index.html:

<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
 
<script>
  document.addEventListener('alpine:init', () => {
    Alpine.data('todos', () => ({
      title: '',
      tasks: [],
      add() {
        if (this.title === '') return;
        this.tasks.push(this.title);
        this.title = '';
      }
    }));
  });
</script>
 
<div x-data="todos">
  <input x-model="title" @keydown.enter="add()" placeholder="New task">
  <ul>
    <template x-for="task in tasks">
      <li x-text="task"></li>
    </template>
  </ul>
</div>

It works as before, but the markup is shorter. The second argument of Alpine.data() is a function that returns an object — every component gets its own copy of the data.

Parameters

The function can take arguments. They are passed in x-data like a call:

<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
 
<script>
  document.addEventListener('alpine:init', () => {
    Alpine.data('todos', (initial = []) => ({
      tasks: initial,
      get count() {
        return this.tasks.length;
      }
    }));
  });
</script>
 
<div x-data="todos(['Buy milk', 'Call Oleg'])">
  <p x-text="'Home: ' + count"></p>
</div>
<div x-data="todos(['Report'])">
  <p x-text="'Work: ' + count"></p>
</div>

The page has two independent lists: "Home: 2" and "Work: 1".

Registering in an npm project

In a Vite project it is convenient to keep the component in its own module. Create src/todos.js:

// src/todos.js
export default (initial = []) => ({
  title: '',
  tasks: initial,
  add() {
    if (this.title === '') return;
    this.tasks.push(this.title);
    this.title = '';
  }
});

Register it in src/main.js before calling Alpine.start():

// src/main.js
import Alpine from 'alpinejs';
import todos from './todos.js';
 
Alpine.data('todos', todos);
 
window.Alpine = Alpine;
Alpine.start();

In index.html the component is used the same way: x-data="todos".

Common mistake

Registering the component after Alpine has started: in src/main.js after Alpine.start(), or with the CDN build outside an alpine:init handler, for example in DOMContentLoaded. Alpine has already walked the page, did not find todos and logged the error todos is not defined. Register components inside alpine:init or before Alpine.start().

Thanks for reading!

Donate to the site — we work hard for you.