AlpineComponentLoader is a robust, zero-dependency generic web component loader designed specifically for Alpine.js. It transforms standard HTML templates into reactive, encapsulated Custom Elements, bridging the gap between simple DOM sprinkling and full Single Page Application (SPA) architecture.
It supports Shadow DOM, Declarative Fetching, Global Store Binding, Lifecycle Hooks, Error Boundaries, and more without any build steps.
- Zero Build Step: No Webpack, No Vite, No Node.js. Just import and run.
- True Encapsulation: Optional Shadow DOM support keeps CSS/JS isolated.
- Dynamic Switching: Swap components programmatically with
<keep-alive>caching support. - Declarative Data: Auto-fetch APIs (
data-src) and bind Global Stores (bind-store) via attributes. - Resilience: Built-in Error Boundaries (
fallback) and Strict Prop Validation. - Performance: Strategies for Lazy (Viewport) and Idle (CPU Idle) loading.
- Type Safety: Attributes are automatically coerced into real JavaScript
Numbers,Booleans,Arrays, andObjects.
Import the module directly. No installation required.
<script type="module">
import AlpineComponentLoader from '/dist/AlpineComponentLoader.min.js';
// Optional: Global Configuration
AlpineComponentLoader.config({
basePath: '/components/',
errorCss: { color: 'red', fontWeight: 'bold' }
});
</script>Define components directly in your HTML using <template>.
Use the acl-props attribute to define prop types and defaults via JSON. This ensures attributes passed to your component (like count="5") are treated as Numbers, not Strings.
<template acl-component="inline-counter" acl-props='{ "count": "Number", "label": { "type": "String", "default": "Default Counter" } }'>
<div x-data="{ props: $el.$props }" style="border:1px solid #ccc; padding:10px;">
<h3 x-text="props.label || 'Default Counter'"></h3>
<button @click="props.count++">Count: <span x-text="props.count"></span></button>
</div>
</template>
<inline-counter></inline-counter>
<inline-counter label="My Widget" count="5"></inline-counter>Move components to separate HTML files and register them via JavaScript. This is ideal for production applications.
components/navbar.html
<style> :host { display: block; background: #333; color: white; } </style>
<nav x-data>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>index.html
AlpineComponentLoader.define('site-nav', 'navbar.html', {
shadow: true // Enable Shadow DOM style isolation
});<site-nav></site-nav>Use the <acl-component> element to load components entirely from HTML without writing any JavaScript registration code.
<acl-component
src="/components/card.html"
tag="my-card"
shadow="true"
loading="lazy"
title="Hello World"
></acl-component>AlpineComponentLoader automatically converts HTML string attributes into real JavaScript types (Numbers, Booleans, Arrays, Objects) based on your configuration. These props are made available as a reactive props object in your Alpine component.
You can define props in two ways: using JavaScript (define) or directly in HTML (acl-props).
A. External Components (JavaScript)
Pass an attributes object when registering the component. You can use simple types or detailed objects for defaults and validation.
AlpineComponentLoader.define('user-profile', 'user.html', {
attributes: {
'age': Number, // Basic type
'active': { // Default value
type: Boolean,
default: false
},
'status': { // Enum validation (Allowed values)
type: String,
options: ['online', 'offline', 'busy'],
default: 'online'
},
'config': { // Shape validation (Object structure)
type: Object,
schema: {
theme: String,
notifications: Boolean
}
}
}
});B. Inline Templates (acl-props)
Use the acl-props attribute on the <template> tag. The value must be a valid JSON string. This is useful for single-file demos or rapid prototyping.
<template acl-component="user-profile" acl-props='{
"age": "Number",
"active": { "type": "Boolean", "default": false },
"status": {
"type": "String",
"options": ["online", "offline", "busy"]
},
"config": {
"type": "Object",
"schema": { "theme": "String", "notifications": "Boolean" }
}
}'>
<div x-data="{ props: $el.$props }">
<span x-text="props.status"></span>
</div>
</template>Regardless of how they are defined, you pass props as standard HTML attributes. The loader handles the conversion before Alpine initializes.
<user-profile
age="25"
active="true"
status="busy"
config="{ 'theme': 'dark', 'notifications': true }"
></user-profile>When using AlpineComponentLoader.define, pass an attributes object:
AlpineComponentLoader.define('user-card', 'card.html', {
attributes: {
'age': Number,
'tags': Array,
'config': Object,
'active': Boolean
}
});When using inline templates, use the acl-props JSON attribute:
<template acl-component="user-card" acl-props='{ "age": "Number", "active": "Boolean" }'>
...
</template>Regardless of how it is defined, usage is the same:
<user-card
active="true"
age="25"
tags="['admin', 'editor']"
config="{ 'theme': 'dark' }"
></user-card>The $props object automatically includes several read-only state properties and helper methods useful for controlling the component.
| Property | Type | Description |
|---|---|---|
$data |
any |
Data fetched via data-src. null until loaded. |
$loading |
Boolean |
true while data is fetching or template is resolving. |
$error |
String |
Error message if something goes wrong. null otherwise. |
$lastUpdated |
Number |
Timestamp of the last reactive update. |
$emit(name, detail) |
Function |
Dispatches a custom event from the component root. |
$reload() |
Function |
Forces the component to reload (re-fetch data/template). |
$persistence |
Object |
If persist is used this contains further helpers. See 8. State Persistence below. |
Automatically fetch JSON data and inject it into $el.$props.$data. Requests are globally deduplicated via a shared data cache; if multiple components request the same final URL simultaneously, only one network request is made.
You can dynamically build the fetch URL using path placeholders and query parameters through dedicated data attributes.
data-fetch-keys: Replaces segments in thedata-srcstarting with a colon (e.g.,:userId).data-fetch-params: Appends a query string to the URL (e.g.,?limit=5).
These attributes support three types of values:
- JSON Objects:
data-fetch-params='{ "limit": 5 }'. - Dynamic Expressions:
data-fetch-keys='{ "userId": $el.id }'(accesses component props directly). - Functions:
data-fetch-keys='() => ({ "userId": ~~(Math.random() * 9) + 1 })'(supports sync or async arrow functions).
Use the data-fetch-poll attribute to automatically re-fetch data at set intervals (in milliseconds). Polling bypasses the shared cache to ensure the component always receives fresh data.
<user-stats
data-src="/api/stats"
data-fetch-poll="5000"
></user-stats>Fetch logic and polling intervals can also be defined globally or per-component using objects or functions in your JavaScript configuration.
AlpineComponentLoader.define('user-profile', 'user.html', {
// Maps :userId in data-src to props.id
dataFetchKeys: ({ props }) => ({
userId: props.id
}),
// Adds dynamic query parameters asynchronously
dataFetchParams: async () => ({
_t: Date.now()
}),
// Set a default polling interval for this component
dataFetchPoll: 10000
});The component provides reactive state properties to handle the lifecycle of the request.
<div x-data="{ props: $el.$props }">
<template x-if="props.$loading">
<div>Loading...</div>
</template>
<template x-if="props.$error">
<div style="color: red" x-text="props.$error"></div>
</template>
<template x-if="props.$data">
<div>
<h1 x-text="props.$data.name"></h1>
<p x-text="props.$data.bio"></p>
<button @click="props.$reload()">Refresh Data</button>
</div>
</template>
</div>Sync an Alpine Global Store to the component's props. Updates to the store automatically update the component.
// Define store
Alpine.store('theme', { mode: 'dark', color: 'blue' });<theme-widget bind-store="theme"></theme-widget>Dispatch custom events from your component using the $emit helper available on props. It automatically configures the event with bubbles: true and composed: true so it can be caught by parent components, even outside the Shadow DOM.
<div x-data="{ props: $el.$props }">
<button @click="props.$emit('save', { status: 'complete' })">
Save Progress
</button>
</div>
<my-component @save="console.log('Saved:', $event.detail.status)"></my-component>Dynamically switch between components using the <acl-dynamic> element. This is similar to Vue's <component :is="...">. Attributes placed on the loader are automatically forwarded to the rendered component.
<div x-data="{ currentView: 'user-profile' }">
<button @click="currentView = 'user-settings'">Edit Settings</button>
<acl-dynamic :is="currentView" theme="dark"></acl-dynamic>
</div>By default, switching components destroys the old instance (losing state). Add the keep-alive attribute to cache inactive components in memory. When you switch back, they retain their state (input values, scroll position, etc.).
<acl-dynamic :is="currentView" keep-alive></acl-dynamic>You can also use keep-alive on standard components to prevent them from being destroyed when removed from the DOM manually. This allows you to detach a component (e.g., el.remove()) and re-attach it later without losing its state or triggering a re-fetch.
<user-profile keep-alive id="my-profile"></user-profile>
<script>
const el = document.getElementById('my-profile');
// Remove from DOM
// The component is "Deactivated" but its state/Alpine scope is preserved in memory
el.remove();
// ... Later ...
// Re-append to DOM
// The component is "Activated". It resumes immediately without re-initializing.
document.body.appendChild(el);
</script>Automatically save and restore component props to localStorage or sessionStorage. This is useful for preserving form inputs, user preferences, or active tabs across page reloads.
persist: Set to"local"(for localStorage) or"session"(for sessionStorage).persist-key: (Optional) The unique key used in storage. Defaults toacl:tagName. If you have multiple instances of the same component, you must provide unique keys (or unique IDs on the elements).persist-debounce: (Optional) Number of milliseconds to delay writing to storage. Useful for high-frequency updates like typing. Includes safety checks to flush data before the page unloads.
<user-settings
persist="local"
persist-key="app-settings_v1"
persist-debounce="500"
theme="dark"
notifications="true"
></user-settings>The loader injects a helper object into props.$persistence to allow manual control over storage:
$save(): Forces a save immediately (respects debounce if set).$flush(): Immediately writes any pending debounced changes to storage.$clear(): Removes the key from storage.$get(): Returns the current raw value from storage.
<button @click="props.$persistence.$clear(); location.reload()">
Reset to Defaults
</button>loading="eager"(Default): Loads immediately.loading="lazy": Loads when the element enters the viewport (IntersectionObserver). Use this for "below the fold" content.loading="idle": Loads when the browser network/CPU is idle (requestIdleCallback). Great for analytics or non-critical UI.
<heavy-footer loading="idle"></heavy-footer>To prevent "waterfall" loading (where a child component waits for its parent to mount before fetching), you can manually prefetch templates. This warms the cache so the component renders instantly when needed.
// Register the component first (fetching is deferred)
AlpineComponentLoader.define('heavy-widget', 'widget.html');
// Prefetch when the browser is idle or on hover
AlpineComponentLoader.prefetch('heavy-widget');If a component fails to load (404, Network Error) or fails to fetch its data-src, the loader renders a fallback template instead of breaking the page.
<acl-component
src="dashboard.html"
tag="my-dashboard"
fallback="#tpl-error-skeleton"
></acl-component>
<template id="tpl-error-skeleton">
<div class="error-box">⚠️ Widget unavailable</div>
</template>Load CSS (Bootstrap/Icons) or JS (Chart.js) automatically.
- CSS: Injected into Global Head (for caching/fonts) AND Shadow Root (for scoping).
- JS: Deduplicated globally.
AlpineComponentLoader.define('chart-widget', 'chart.html', {
shadow: true,
externalCss: ['https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css'],
externalScripts: ['https://cdn.jsdelivr.net/npm/chart.js']
});Execute logic during specific phases of the component definition.
| Hook | Trigger | Context (this) |
|---|---|---|
beforeMount |
DOM created, props initialized, but not rendered. | Component Instance |
mounted |
DOM rendered, Alpine initialized. | Component Instance |
updated |
Observed attribute changed. | { name, oldVal, newVal } |
activated |
Component restored from `` cache. | Component Instance |
deactivated |
Component removed but cached by ``. | Component Instance |
unmounted |
Component removed from DOM. | Component Instance |
loaded |
Component has finished the loading step. | Component Instance |
AlpineComponentLoader.define('timer-comp', 'timer.html', {
hooks: {
mounted() {
console.log('Component mounted!');
this.timer = setInterval(() => console.log('Tick'), 1000);
},
unmounted() {
clearInterval(this.timer);
}
}
});AlpineComponentLoader includes an optional visual debugger to help you inspect component props, loading states, and layout boundaries. To keep the core library lightweight, the debugger is a separate module that must be injected.
Import ACLDebugger and inject it into the main loader class before starting the application.
import AlpineComponentLoader from '/dist/AlpineComponentLoader.min.js';
import ACLDebugger from '/dist/ACLDebugger.min.js';
// Inject the Debugger into the Loader
ACLDebugger.inject(AlpineComponentLoader);
// Expose AlpineComponentLoader to the global scope
window.AlpineComponentLoader = AlpineComponentLoader;
// ... Rest of your ACL code ...
// Toggle the debugger on/off
AlpineComponentLoader.toggleDebug();Once injected, you can toggle the debug overlay programmatically or via a button:
<button @click="AlpineComponentLoader.toggleDebug()">
🐞 Debug Mode
</button>- Component Inspector: Hover over any component to see a tooltip with its Tag Name, Loading Status, and real-time Props (JSON).
- Visual Overlays: Draws green borders around all detected components to visualize layout and nesting.
- Performance Optimized: The debugger only renders overlays for components currently visible in the viewport, ensuring smooth performance even with hundreds of components on the page.
| Option | Type | Default | Description |
|---|---|---|---|
attributes |
Object |
{} |
Prop definitions (type, default, required, options, schema). |
shadow |
Boolean |
false |
Enable Shadow DOM encapsulation. |
dataSrc |
String |
null |
Default API URL for fetching. |
dataFetchKeys |
`Object | Function` | null |
dataFetchOptions |
Object |
{} |
Custom options for fetch() (headers, method, etc). |
dataFetchParams |
`Object | Function` | null |
dataFetchPoll |
Number |
null |
Polling interval in milliseconds. |
dataFetchTimeout |
Number |
10000 |
Timeout for data-src requests (ms). |
bindStore |
String |
null |
Name of Alpine Store to bind to props. |
loading |
String |
'eager' |
'eager', 'lazy', or 'idle'. |
fallback |
String |
null |
URL/ID of template to show on error. |
hooks |
Object |
{} |
Lifecycle (mounted...) and Data (beforeFetch...) hooks. |
externalCss |
Array |
[] |
List of CSS URLs to inject. |
externalScripts |
Array |
[] |
List of JS URLs to inject. |
forwardEvents |
Array |
[] |
Events to bubble out of Shadow DOM. |
cacheTemplates |
Boolean |
true |
Enable template cache for external (HTTP(s)) templates for 15 minutes. |
Set global defaults for all components (e.g., basePath, errorCss).
Manually trigger the loader (runs automatically on DOMContentLoaded).
Manually clears all template caches (Current and Old versions) from the browser's Cache Storage. Useful for forcing a refresh of templates without changing the version number.
await AlpineComponentLoader.clearCache();Manually fetches and caches the template for a registered component tag.
- tagName: The hyphenated tag name of the component to prefetch.
- Returns: A
Promisethat resolves with the template string.
await AlpineComponentLoader.prefetch('my-component');Q: My fonts aren't loading in Shadow DOM.
A: The library handles this automatically via "Dual Injection". It injects the CSS into the Shadow Root (for classes) and the Global Head (for @font-face definitions). Ensure your externalCss URL is correct.
Q: beforeMount crashes saying props is undefined.
A: Ensure you define your attribute in the config: attributes: { 'myProp': Array }. The library auto-initializes Types (Arrays become [], Objects {}) so they are ready for use in hooks.
Q: Fetching data flickers when I change the attribute fast.
A: The library uses AbortController internally. Rapid changes to data-src automatically cancel the previous pending request, ensuring your UI always shows the data for the current attribute value.