11

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?

spiffytech
  • 6,161
  • 7
  • 41
  • 57

6 Answers6

26

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>
lovedder1995
  • 369
  • 1
  • 4
  • 2
  • This is cool. It's even possible to set x-model on component and bind to it in the template: The usage: `` The implementation: ` – bubblez Sep 24 '22 at 16:42
  • This only works for me if the script is placed below the HTML. Why is that? – kenset Oct 08 '22 at 21:06
  • It's great that you're using WebComponents. Might be beneficial to add this to the answer specifically. – Jay Regal Feb 16 '23 at 15:33
14

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)

spiffytech
  • 6,161
  • 7
  • 41
  • 57
3

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

https://github.com/markmead/alpinejs-component

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

<script>
    Alpine.component('dropdown', () => ({
        open: false,
        toggle() { this.open = !this.open }
    }))
</script>
nektobit
  • 843
  • 7
  • 11
  • 4
    OK, so how do you use this in production? Can this be a single file somewhere that you include or import as needed into a page? and then you use it like `...`? – Moss Sep 18 '21 at 21:39
  • 1
    Sadly this way you can only encapsulate the functions, not the markup/template needed for the component. – László Kenéz Feb 18 '22 at 19:07
  • Can you provide a link to the documentation where `Alpine.component` is explained? Can't find it unfortunately under https://alpinejs.dev/start-here, there are just the Globals `data`, `store` and `bind` – Simon Ferndriger Mar 05 '23 at 10:47
  • Don't you have to listen to the `alpine:init` first before you can add a "component" to Alpine? – Simon Ferndriger Mar 05 '23 at 11:21
  • @SimonFerndriger my version updated with url to AlpineJS, but I am not sure about :init. Just try - maybe worked :). – nektobit Mar 06 '23 at 12:54
3

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

JackChouMine
  • 947
  • 8
  • 22
2

You can do this with Alpine.data and the documented approach for encapsulating directives with x-bind. The trick is to bind the x-html directive. In your HTML do this:

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

In your Javascript:

document.addEventListener('alpine:init', () => {
    Alpine.data('dropdown', () => ({
        show: false,

        bind: {
            ['x-html']() { return `
                <button @click="show = !show">Click me!</button>
                <div x-show="show">Hello World</div>
            `},
        },
    }));
})

JSFiddle here.

It's a little hacky as you are encapsulating all your nested content in a multiline HTML string bound in the x-html directive (though perhaps no more hacky than the alternative of cloning templates everywhere). Make sure you don't use the backtick character in the content. Nevertheless, the content can be nested as deeply as you like and can contain Alpine.js directives. You can initialise your component by declaring and passing parameters into Alpine.data. You could also bind x-modelable to expose any properties of your component as outputs.

If you prefer to use templates, maybe because your editor does better syntax highlighting when markup is not embedded in a string, you can combine this approach with templates. Here's an example that demonstrates x-modelable and the use of templates too. In effect, Alpine does your template cloning for you.

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.js"></script>

</head>

<body>

  <div x-data="{clicked: false}">

    <div>Clicked is <span x-text="clicked"></span></div>
  
    <div x-data="dropdown" x-bind="bind" x-model="clicked"></div>

  </div>
 
  <template id="dropdown">
    <button @click="show = !show">Click me!</button>
    <div x-show="show">Hello World</div>      
  </template>

</body>

<script type="text/javascript">

document.addEventListener('alpine:init', () => {
    Alpine.data('dropdown', () => ({
      show: false,

      bind: {
        ['x-modelable']: 'show',
        ['x-html']() { return document.querySelector('#dropdown').innerHTML},
      },
    }));
})

</script>

</html>

JSFiddle here.

Peter Swords
  • 489
  • 6
  • 17
1

The x-component with native custom element in Vimesh UI (https://github.com/vimeshjs/vimesh-ui) is a more complete reusable component implementation :

<head>
    <script src="https://unpkg.com/@vimesh/style" defer></script>
    <script src="https://unpkg.com/@vimesh/ui"></script>
    <script src="https://unpkg.com/alpinejs" defer></script>
</head>


<body x-cloak class="p-2" x-data="{name: 'Counter to rename', winner: 'Jacky'}">   
    Rename the 2nd counter : <input type="text" x-model="name" class="rounded-md border-2 border-blue-500">
    <vui-counter x-data="{step: 1}" :primary="true" title="First" x-init="console.log('This is the first one')" owner-name="Tom"></vui-counter>
    <vui-counter x-data="{step: 5}" :title="name + ' @ ' + $prop('owner-name')" owner-name="Frank"></vui-counter>
    <vui-counter x-data="{step: 10, value: 1000}" :owner-name="winner">
        <vui-counter-trigger></vui-counter-trigger>
    </vui-counter>

<template x-component.unwrap="counter" :class="$prop('primary') ? 'text-red-500' : 'text-blue-500'"
    x-data="{ step : 1, value: 0}" x-init="$api.init && $api.init()" title="Counter" owner-name="nobody">
    <div>
        <span x-text="$prop('title')"></span><br>
        Owner: <span x-text="$prop('owner-name')"></span><br>
        Step: <span x-text="step"></span><br>
        Value : <span x-text="value"></span><br>
        <button @click="$api.increase()"
            class="inline-block rounded-lg bg-indigo-600 px-4 py-1.5 text-white shadow ring-1 ring-indigo-600 hover:bg-indigo-700 hover:ring-indigo-700">
            Increase
        </button>
        <slot></slot>
    </div>
    <script>
        return {
            init() {
                console.log(`Value : ${this.value} , Step : ${this.step}`)
            },
            increase() {
                this.value += this.step
            }
        }
    </script>
</template>
<template x-component="counter-trigger">
    <button @click="$api.of('counter').increase()"
        class="inline-block rounded-lg mt-2 bg-green-600 px-4 py-1.5 text-white shadow ring-1 ring-green-600 hover:bg-green-700 hover:ring-green-700">
        Tigger from child element</button>
</template>
</body>
comforx
  • 106
  • 4