Reusable Alpine.js components?

Viewed 9426

How do I use Alpine.js to create a reusable component and display it? E.g., maybe I want to define an generic Alpine.js button component that changes text and color from parameters, then have my Alpine.js navbar component use the button component to show a login button.

Can I do this in pure client-side code, without relying on a server templating out all of the button HTML everywhere the button component is used?

4 Answers

Can I do this in pure client-side code, without relying on a server templating?

Yes, you can.

Alpine.js always will try to persuade you to use a server side templating engine.

But just like you, I don't let myself be persuaded:

<template x-component="dropdown">
    <div x-data="{ ...dropdown(), ...$el.parentElement.data() }">
        <button x-on:click="open">Open</button>

        <div x-show="isOpen()" x-on:click.away="close" x-text="content"></div>
    </div>
</template>

<x-dropdown content="Content for my first dropdown"></x-dropdown>

<div> Random stuff... </div>

<x-dropdown content="Content for my second dropdown"></x-dropdown>

<x-dropdown></x-dropdown>

<script>
    function dropdown() {
        return {
            show: false,
            open() { this.show = true },
            close() { this.show = false },
            isOpen() { return this.show === true },
            content: 'Default content'
        }
    }

    // The pure client-side code
    document.querySelectorAll('[x-component]').forEach(component => {
        const componentName = `x-${component.getAttribute('x-component')}`
        class Component extends HTMLElement {
            connectedCallback() {
                this.append(component.content.cloneNode(true))
            }
 
            data() {
                const attributes = this.getAttributeNames()
                const data = {}
                attributes.forEach(attribute => {
                    data[attribute] = this.getAttribute(attribute)
                })
                return data
            }
        }
        customElements.define(componentName, Component)
    })
</script>

Alpine.js contributer @ryangjchandler remarks that reusable templates are out of scope for Alpine.js:

The proposed [Alpine.js version 3] x-component directive will NOT have anything to do with templating or the markup for your component. Instead it will provide a way of writing more immediately reusable data sets & functions, whilst reducing the amount of directives you need to define in your markup.

If you need re-usable templates, I would consider using a server-side template engine or a more monolithic front end framework such as Vue or React. (link)

and

The functionality you are looking for is far out of the scope of Alpine. It's designed to work alongside your existing markup from the server or static files, not replace / component-ise your markup. (link)

use alpinejs-component

one same page by cdn:

<div
  x-data="{
    people: [
      { name: 'John', age: '25', skills: ['JavaScript', 'CSS'] },
      { name: 'Jane', age: '30', skills: ['Laravel', 'MySQL', 'jQuery'] }
    ]
  }"
>
  <ul>
    <template x-for="person in people">
      <!-- use the person template to find the <template id="person"> element. -->
      <x-component-wrapper x-component template="person" x-data="{ item: person }"></x-component-wrapper>
    </template>
  </ul>
</div>

<template id="person">
  <li class="user-card">
    <h2 x-text="item.name"></h2>
    <p x-text="item.age"></p>
    <ul>
      <template x-for="skill in item.skills">
        <li x-text="skill"></li>
      </template>
    </ul>
  </li>
</template>

<script src="https://unpkg.com/alpinejs-component@1.x.x/dist/component.min.js"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>

use url import html template:

<div
  x-data="{
    people: [
      { name: 'John', age: '25', skills: ['JavaScript', 'CSS'] },
      { name: 'Jane', age: '30', skills: ['Laravel', 'MySQL', 'jQuery'] }
    ]
  }"
>
  <ul>
    <template x-for="person in people">
      <x-component-wrapper x-component url="/public/person.html" x-data="{ item: person }"></x-component-wrapper>
    </template>
  </ul>
</div>

<script src="https://unpkg.com/alpinejs-component@1.x.x/dist/component.min.js"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>

person.html:

<li class="user-card">
  <h2 x-text="item.name"></h2>

  <p x-text="item.age"></p>

  <ul>
    <template x-for="skill in item.skills">
      <li x-text="skill"></li>
    </template>
  </ul>
</li>

install by npm :

npm i -D alpinejs-component

yarn add -D alpinejs-component

register plugin:

import Alpine from "alpinejs";
import component from "alpinejs-component";

Alpine.plugin(component);

window.Alpine = Alpine;

Alpine.start();

or use module in browser:

<x-component-wrapper x-component template="dropdown" x-data="dropdown"></x-component-wrapper>
<x-component-wrapper x-component template="dropdown" x-data="dropdown"></x-component-wrapper>

<template id="dropdown">
  <div @click="close" class="dropdown-toggle">
    <button x-on:click="open">Open</button>
    <div x-show="show" x-text="content"></div>
  </div>
</template>

<script type="module">
  import { default as Alpine } from 'https://cdn.skypack.dev/alpinejs'
  import alpinejsComponent from 'https://cdn.skypack.dev/alpinejs-component'
  function dropdown() {
    return {
      show: false,
      open() {
        console.log('open')
        this.show = true
        console.log(this.show)
      },
      close(event) {
        const button = this.$el.querySelector('button')
        const target = event.target
        if (this.$el.contains(target) && !button.contains(target)) {
          this.show = false
        }
      },
      get isOpen() {
        return this.show === true
      },
      content: 'Default content',
      init() {
        console.log(this.$el.parentElement)
        console.log('dropdown --- init')
      },
    }
  }
  Alpine.data('dropdown', dropdown)
  Alpine.plugin(alpinejsComponent)
  Alpine.start()
</script>

work well.

more info alpinejs-component

With Alpine.js v3 and Global Alpine Components, you can use Alpine.component() to encapsulate this functionality.

<div x-data="dropdown">
...
</div>

<script>
    Alpine.component('dropdown', () => ({
        open: false,
        toggle() { this.open = !this.open }
    }))
</script>
Related