Alpine.js
Shadleaf’s interactive components (dialog, dropdown menu, tabs and the like, from 0.4 on) use Alpine.js for what the native elements cannot do on their own. A page can only have one Alpine: two copies would both initialise the same elements. So you choose where it comes from, with one property:
shadleaf.assets.alpine=bundled # the default| Value | The assets fragment loads |
Choose it when |
|---|---|---|
bundled |
Alpine’s standard build, Shadleaf’s components, and starts Alpine | your application does not load Alpine itself |
csp |
Alpine’s CSP build, Shadleaf’s components, and starts Alpine | your Content-Security-Policy has no 'unsafe-eval' |
external |
only Shadleaf’s components, registered with your Alpine | your application already loads Alpine |
In every case the script is served from your own application under /shadleaf/**, so a policy with
script-src 'self' allows it. It is a module script, so it never blocks rendering.
bundled
Section titled “bundled”Nothing to do: include the assets fragment as shown in Getting started. You can use Alpine
in your own markup too (x-data, x-show, @click, …) without loading it yourself.
To register your own components or plugins, listen for alpine:init in a script that runs before the assets
fragment. Shadleaf starts Alpine as soon as its script runs, and alpine:init fires only once, at that moment:
<head> <th:block th:replace="~{sl/layout :: theme-script}"></th:block> <script type="module" th:src="@{/js/alpine-components.js}"></script> <th:block th:replace="~{sl/layout :: assets}"></th:block></head>document.addEventListener('alpine:init', () => { window.Alpine.data('orderLine', () => ({ quantity: 1, increment() { this.quantity++; } }));});Do not load another Alpine next to it. Shadleaf’s script refuses to start a second one when it finds
window.Alpine already on the page, and says so in the browser console. It cannot notice an Alpine that loads
after it, so if you see elements initialised twice, look for a second Alpine script tag.
Alpine’s standard build turns every attribute expression into code with new Function(...), which a policy without
'unsafe-eval' blocks. The CSP build parses the expressions itself instead. Shadleaf’s own templates only use
expressions it accepts, so the components work the same with either build.
The rules apply to your own Alpine markup as well. The CSP build supports property access, method calls, assignments,
ternaries and string concatenation. It does not support arrow functions, template literals, the spread operator,
assignments to nested properties, or globals such as Math, document and console. Move that logic into an
Alpine.data component, registered as shown above, and call its methods from the markup:
<!-- not with the CSP build --><button @click="items = items.filter(i => i.done)">Clear done</button>
<!-- works with both builds --><div x-data="todoList"> <button @click="clearDone">Clear done</button></div>The Content-Security-Policy guide covers the rest of a strict policy. samples/shadleaf-sample-02 in the
repository uses csp behind exactly that policy.
external
Section titled “external”Your application loads Alpine, and Shadleaf’s script only registers its components with it. Shadleaf’s script has
to run before your Alpine starts, so put the assets fragment before the script that loads Alpine. Deferred and
module scripts run in the order they appear:
<head> <th:block th:replace="~{sl/layout :: theme-script}"></th:block> <th:block th:replace="~{sl/layout :: assets}"></th:block> <script defer th:src="@{/js/alpine.min.js}"></script></head>The same goes for an Alpine you bundle yourself: the module that calls Alpine.start() goes after the fragment.
import Alpine from 'alpinejs';
window.Alpine = Alpine;Alpine.start();If Shadleaf’s script runs after Alpine has already initialised elements, the Shadleaf components on the page stay inert, and the browser console says: Shadleaf: shadleaf.js ran after Alpine started. Move the fragment up.
Some components need an Alpine plugin, such as @alpinejs/focus. With bundled and csp Shadleaf includes them;
with external your application registers them. Each component’s page lists the plugins it needs. No component
needs one yet: the dialog family, the dropdown menu, the popover and the tooltip use none. The dropdown menu, popover
and tooltip are placed with CSS anchor positioning, not with @alpinejs/anchor: every current browser supports it, and
for older ones their components set the position themselves.
The avatar’s broken-image fallback is in the same script but is no Alpine component: it works with every value of
shadleaf.assets.alpine, and with external also before, or without, your Alpine.
Pages without interactive components
Section titled “Pages without interactive components”The script is on every page that includes the assets fragment, whether that page uses an interactive component or
not. That keeps the setup to one fragment. bundled and csp add about 20 KB, compressed, and are cached across pages.