🎉  The Alpine Day 2021 videos are out! Watch Now

Extending

Alpine has a very open codebase that allows for extension in a number of ways. In fact, every available directive and magic in Alpine itself uses these exact APIs. In theory you could rebuild all of Alpine's functionality using them yourself.

Lifecycle concerns

Before we dive into each individual API, let's first talk about where in your codebase you should consume these APIs.

Because these APIs have an impact on how Alpine initializes the page, they must be registered AFTER Alpine is downloaded and available on the page, but BEFORE it has initialized the page itself.

There are two different techniques depending on if you are importing Alpine into a bundle, or including it directly via a <script> tag. Let's look at them both:

Via a script tag

If you are including Alpine via a script tag, you will need to register any custom extension code inside an alpine:init event listener.

Here's an example:

<html>
    <script src="/js/alpine.js" defer></script>

    <div x-data x-foo></div>

    <script>
        document.addEventListener('alpine:init', () => {
            Alpine.directive('foo', ...)
        })
    </script>
</html>

If you want to extract your extension code into an external file, you will need to make sure that file's <script> tag is located BEFORE Alpine's like so:

<html>
    <script src="/js/foo.js" defer></script>
    <script src="/js/alpine.js" defer></script>

    <div x-data x-foo></div>
</html>

Via an NPM module

If you imported Alpine into a bundle, you have to make sure you are registering any extension code IN BETWEEN when you import the Alpine global object, and when you initialize Alpine by calling Alpine.start(). For example:

import Alpine from 'alpinejs'

Alpine.directive('foo', ...)

window.Alpine = Alpine
window.Alpine.start()

Now that we know where to use these extension APIs, let's look more closely at how to use each one:

Custom directives

Alpine allows you to register your own custom directives using the Alpine.directive() API.

Method Signature

Alpine.directive('[name]', (el, { value, modifiers, expression }, { Alpine, effect, cleanup }) => {})
   
name The name of the directive. The name "foo" for example would be consumed as x-foo
el The DOM element the directive is added to
value If provided, the part of the directive after a colon. Ex: 'bar' in x-foo:bar
modifiers An array of dot-separated trailing additions to the directive. Ex: ['baz', 'lob'] from x-foo.baz.lob
expression The attribute value portion of the directive. Ex: law from x-foo="law"
Alpine The Alpine global object
effect A function to create reactive effects that will auto-cleanup after this directive is removed from the DOM
cleanup A function you can pass bespoke callbacks to that will run when this directive is removed from the DOM

Simple Example

Here's an example of a simple directive we're going to create called: x-uppercase:

Alpine.directive('uppercase', el => {
    el.textContent = el.textContent.toUpperCase()
})
<div x-data>
    <span x-uppercase>Hello World!</span>
</div>

Evaluating expressions

When registering a custom directive, you may want to evaluate a user-supplied JavaScript expression:

For example, let's say you wanted to create a custom directive as a shortcut to console.log(). Something like:

<div x-data="{ message: 'Hello World!' }">
    <div x-log="message"></div>
</div>

You need to retrieve the actual value of message by evaluating it as a JavaScript expression with the x-data scope.

Fortunately, Alpine exposes its system for evaluating JavaScript expressions with an evaluate() API. Here's an example:

Alpine.directive('log', (el, { expression }, { evaluate }) => {
    // expression === 'message'

    console.log(
        evaluate(expression)
    )
})

Now, when Alpine initializes the <div x-log...>, it will retrieve the expression passed into the directive ("message" in this case), and evaluate it in the context of the current element's Alpine component scope.

Introducing reactivity

Building on the x-log example from before, let's say we wanted x-log to log the value of message and also log it if the value changes.

Given the following template:

<div x-data="{ message: 'Hello World!' }">
    <div x-log="message"></div>

    <button @click="message = 'yolo'">Change</button>
</div>

We want "Hello World!" to be logged initially, then we want "yolo" to be logged after pressing the <button>.

We can adjust the implementation of x-log and introduce two new APIs to achieve this: evaluateLater() and effect():

Alpine.directive('log', (el, { expression }, { evaluateLater, effect }) => {
    let getThingToLog = evaluateLater(expression)

    effect(() => {
        getThingToLog(thingToLog => {
            console.log(thingToLog)
        })
    })
})

Let's walk through the above code, line by line.

let getThingToLog = evaluateLater(expression)

Here, instead of immediately evaluating message and retrieving the result, we will convert the string expression ("message") into an actual JavaScript function that we can run at any time. If you're going to evaluate a JavaScript expression more than once, it is highly recommended to first generate a JavaScript function and use that rather than calling evaluate() directly. The reason being that the process to interpret a plain string as a JavaScript function is expensive and should be avoided when unnecessary.

effect(() => {
    ...
})

By passing in a callback to effect(), we are telling Alpine to run the callback immediately, then track any dependencies it uses (x-data properties like message in our case). Now as soon as one of the dependencies changes, this callback will be re-run. This gives us our "reactivity".

You may recognize this functionality from x-effect. It is the same mechanism under the hood.

You may also notice that Alpine.effect() exists and wonder why we're not using it here. The reason is that the effect function provided via the method parameter has special functionality that cleans itself up when the directive is removed from the page for any reason.

For example, if for some reason the element with x-log on it got removed from the page, by using effect() instead of Alpine.effect() when the message property is changed, the value will no longer be logged to the console.

→ Read more about reactivity in Alpine

getThingToLog(thingToLog => {
    console.log(thingToLog)
})

Now we will call getThingToLog, which if you recall is the actual JavaScript function version of the string expression: "message".

You might expect getThingToCall() to return the result right away, but instead Alpine requires you to pass in a callback to receive the result.

The reason for this is to support async expressions like await getMessage(). By passing in a "receiver" callback instead of getting the result immediately, you are allowing your directive to work with async expressions as well.

→ Read more about async in Alpine

Cleaning Up

Let's say you needed to register an event listener from a custom directive. After that directive is removed from the page for any reason, you would want to remove the event listener as well.

Alpine makes this simple by providing you with a cleanup function when registering custom directives.

Here's an example:

Alpine.directive('...', (el, {}, { cleanup }) => {
    let handler = () => {}

    window.addEventListener('click', handler)

    cleanup(() => {
        window.removeEventListener('click', handler)
    })

})

Now if the directive is removed from this element or the element is removed itself, the event listener will be removed as well.

Custom magics

Alpine allows you to register custom "magics" (properties or methods) using Alpine.magic(). Any magic you register will be available to all your application's Alpine code with the $ prefix.

Method Signature

Alpine.magic('[name]', (el, { Alpine }) => {})
   
name The name of the magic. The name "foo" for example would be consumed as $foo
el The DOM element the magic was triggered from
Alpine The Alpine global object

Magic Properties

Here's a basic example of a "$now" magic helper to easily get the current time from anywhere in Alpine:

Alpine.magic('now', () => {
    return (new Date).toLocaleTimeString()
})
<span x-text="$now"></span>

Now the <span> tag will contain the current time, resembling something like "12:00:00 PM".

As you can see $now behaves like a static property, but under the hood is actually a getter that evaluates every time the property is accessed.

Because of this, you can implement magic "functions" by returning a function from the getter.

Magic Functions

For example, if we wanted to create a $clipboard() magic function that accepts a string to copy to clipboard, we could implement it like so:

Alpine.magic('clipboard', () => {
    return subject => navigator.clipboard.writeText(subject)
})
<button @click="$clipboard('hello world')">Copy "Hello World"</button>

Now that accessing $clipboard returns a function itself, we can immediately call it and pass it an argument like we see in the template with $clipboard('hello world').

You can use the more brief syntax (a double arrow function) for returning a function from a function if you'd prefer:

Alpine.magic('clipboard', () => subject => {
    navigator.clipboard.writeText(subject)
})

Writing and sharing plugins

By now you should see how friendly and simple it is to register your own custom directives and magics in your application, but what about sharing that functionality with others via an NPM package or something?

You can get started quickly with Alpine's official "plugin-blueprint" package. It's as simple as cloning the repository and running npm install && npm run build to get a plugin authored.

Otherwise, let's create a pretend Alpine plugin by hand called Foo that includes both a directive (x-foo) and a magic ($foo).

We'll start producing this plugin for consumption as a simple <script> tag alongside Alpine, then we'll level it up to a module for importing into a bundle:

Script include

Let's start in reverse by looking at how our plugin will be included into a project:

<html>
    <script src="/js/foo.js" defer></script>
    <script src="/js/alpine.js" defer></script>

    <div x-data x-init="$foo()">
        <span x-foo="'hello world'">
    </div>
</html>

Notice how our script is included BEFORE Alpine itself. This is important, otherwise, Alpine would have already been initialized by the time our plugin got loaded.

Now let's look inside of /js/foo.js's contents:

document.addEventListener('alpine:init', () => {
    window.Alpine.directive('foo', ...)

    window.Alpine.magic('foo', ...)
})

That's it! Authoring a plugin for inclusion via a script tag is extremely simple with Alpine.

Bundle module

Now let's say you wanted to author a plugin that someone could install via NPM and include into their bundle.

Like the last example, we'll walk through this in reverse, starting with what it will look like to consume this plugin:

import Alpine from 'alpinejs'

import foo from 'foo'
Alpine.plugin(foo)

window.Alpine = Alpine
window.Alpine.start()

You'll notice a new API here: Alpine.plugin(). This is a convenience method Alpine exposes to prevent consumers of your plugin from having to register multiple different directives and magics themselves.

Now let's look at the source of the plugin and what gets exported from foo:

export default function (Alpine) {
    Alpine.directive('foo', ...)
    Alpine.magic('foo', ...)
}

You'll see that Alpine.plugin is incredibly simple. It accepts a callback and immediately invokes it while providing the Alpine global as a parameter for use inside of it.

Then you can go about extending Alpine as you please.