Integrating Alpine.js + Pre/Server-rendered content
Alpine.js is a great choice for adding some interactive feature to a server rendered, static or pre-rendered site.
In the Alpine.js GitHub Issues, we’ve seen a lot of questions around how to deal with content that’s pre-rendered or server-rendered in Alpine.js in order to use Alpine.js as a progressive enhancement.
From a poll I ran recently, 95.2% of Alpine.js users want to use it with a server-rendered page, static site generators or HTML pages.
I (want to) use @Alpine_JS in projects that are:
Server-rendered -> Rails/Laravel/Django/Express/WordPress/insert CMS
Static site (with generator) -> Hugo/Hexo/Jekyll/11ty
HTML pages -> hand-written HTML
SPAs (single page apps) -> in React/Angular/Ember/Vue, or with a routerPoll results (21 votes)
- Server rendered: 71.4%
- Static sites (with generator): 9.5%
- HTML pages: 14.3%
- SPAs: 4.8%
— Hugo (@hugo__df) March 27, 2020
This post details solutions to handling these cases. You can find all the examples at alpinejs.codewithhugo.com/pre-rendered-content/.
Table of Contents
Hiding rendered HTML in Alpine’s x-show=“false” or x-show=“0”
Alpine.js has a directive for showing/hiding content: x-show
. We can use it with an “always-falsy” expression (0, null, undefined, '', false, NaN
).
For example, we can set x-show="false"
(to be obvious) or x-show="0"
(to be compact) on any content that’s pre-rendered or server-rendered in order to hide it.
Note: we’re hiding elements (ie. style="display: none"
), therefore they still exist on the page, so it’s not suitable for things like option
s inside of a “select” (as the last example shows).
<div
x-data="{ options: ['hello', 'world'], selected: 'world' }"
>
<p>
<span x-show="true">pre-rendered content that won't be hidden</span>
</p>
<p>
the content in "<span x-show="false">pre-rendered to be hidden</span
>" will get hidden
</p>
<p>
the content in "<span x-show="0">pre-rendered to be hidden</span>"
will get hidden
</p>
<p>
Example with a select that doesn't quite work, we end up with 2
"hello" options, one is selected & hidden and the other is visible
and unselected:
</p>
<select x-model="selected">
<template x-for="o in options" :key="o">
<option :value="o" x-text="o" :selected="o === selected"></option>
</template>
<option x-show="false" value="hello" selected>hello</option>
</select>
</div>
You can see this example at work at Alpine.js Playground - example using x-show with pre-rendered content.
We’ve now seen how to use x-show
to hide server-rendered or pre-rendered content on your page.
Next we’ll see how to overwrite some content using x-text
/x-html
.
Overwriting text/HTML content using x-text/x-html
The next situation one might encounter is that some text is pre-populated on the server but should be driven by Alpine.js once it boots up.
I dealt with this situation in Integrating Alpine.js with Eleventy & YAML files to create Alpine Playground’s Collections.
The way to deal with it is to wrap relevant content in a span
(or other element) that will have the x-text
or x-html
property set. As in the following example.
Another thing we could do is store the initial HTML/text into a variable somewhere, that would be done using x-refs and $refs
/this.$refs
in x-init. Let me know on Twitter or the Alpine.js Discord if that’s something you need more guidance on.
<div
x-data="{ text: 'alpine-render', html: '<button>btn from alpine</button>' }"
x-init="setTimeout(() => {
text = 'alpine-render-2';
html = '<button>alpine-render-2</button>';
}, 1000);
"
>
<span x-text="text">pre-rendered text</span>
<div x-html="html">
<h3>Pre-rendered HTML</h3>
</div>
</div>
You can see this example at work at Alpine.js Playground - example overriding pre-render HTML or text with x-html/x-text.
We’ve now seen how to overwrite text and HTML content with x-text and x-html.
Next we’ll see how to remove DOM Nodes using x-ref and x-init.
Removing single nodes using x-ref + x-init
If we got back to the “select” situation in the first section. If we server/pre-render some options, we actually need to delete them.
In order to do this we can use x-ref
and then use $refs
/this.$refs
in x-init. Refs give us access to a DOM Node inside the Alpine.js Component. Once we’ve got the DOM Node, we can call .remove()
on the node. For example if we’ve got <span x-ref="remove1">Hello</span>
we can call $refs.remove1.remove()
in x-init to remove the whole Node.
<div
x-data="{ options: ['hello', 'world'], selected: 'world' }"
x-init="
$refs.remove1.remove();
$refs.remove2.remove();
$refs.remove3.remove();
"
>
<p>
The following text will get removed with x-ref approach "<span
x-ref="remove1"
>pre-rendered content</span
>"
</p>
<p>
As will the following text: "<span x-ref="remove2"
>pre-rendered content</span
>"
</p>
<p>
A more sensible example with the "select" that works... mostly
("world" should be selected)
</p>
<select x-model="selected">
<template x-for="o in options" :key="o">
<option :value="o" x-text="o" :selected="o === selected"></option>
</template>
<option x-ref="remove3" value="hello" selected>hello</option>
</select>
</div>
You can see this example at work at Alpine.js Playground - example removing pre-rendered nodes using x-refs and x-init.
We’ve now seen how to use x-ref
and $refs.refName.remove()
to delete nodes. However, keeping track of multiple refs is quite verbose.
In the next section we’ll look at how to introduce an x-remove
attribute to mark Nodes to be deleted on x-init.
Removing nodes using x-remove + x-init code
x-remove
is not an attribute that Alpine.js does anything with, we’ll implement our own functionality around it though.
In the previous section we used x-ref
and $refs
to find which DOM Nodes to call .remove()
on. That was tedious, for each Node to delete, we need a new ref and a new remove()
statement.
Instead we can mark elements to be deleted with the x-remove
attribute. Then in x-init
, we can use $el.querySelectorAll('[x-remove]')
(or this.$el.querySelectorAll
if defining x-init in JavaScript) to find all the Nodes that have this attribute.
Once we’ve got all the nodes, we can loop over them and call .remove()
on each of them.
In code that’s: $el.querySelectorAll('[x-remove]').forEach(e => e.remove())
.
Note: deleting options causes issues with
x-model
on theselect
, see the next section
<div
x-data="{ options: ['hello', 'world'], selected: 'world' }"
x-init="
$el.querySelectorAll('[x-remove]')
.forEach(e => e.remove());
"
>
<p>
The following text will get removed with x-ref approach "<span
x-remove
>pre-rendered content</span
>"
</p>
<p>
As will the following text: "<span x-remove
>pre-rendered content</span
>"
</p>
<p>
A more sensible example with the "select" that works... mostly
("world" should be selected)
</p>
<select x-model="selected">
<template x-for="o in options" :key="o">
<option :value="o" x-text="o" :selected="o === selected"></option>
</template>
<option x-remove value="hello" selected>hello</option>
</select>
</div>
You can see this example at work at Alpine.js Playground - example removing pre-rendered nodes using x-remove attribute and x-init.
We’ve now seen how to use x-remove
, x-init
and $el
to have a cleaner way to remove pre-rendered DOM Nodes.
That’s it for this post, you can check out Integrating Alpine.js with Eleventy & YAML files to create Alpine Playground’s Collections for more learnings working with Alpine.js + server/pre-rendered content.
If you’re interested in Alpine.js, Subscribe to Alpine.js Weekly. A free, once–weekly email roundup of Alpine.js news and articles.
Interested in Alpine.js?
Subscribe to Alpine.js Weekly. A free, once–weekly email roundup of Alpine.js news and articles