Recently I've created a POC involving some new frontend technologies and Alpine JS was one of them among others. In this article I will show an approach to create modular web apps with Alpine.
Context
Our context is create medium/large size web application totally modular. Each page is treated as module composed by many components and in the backend side we have Go
processing the page creation like SSR.
Alpine
AlpineJS is a new kids on the block on Javascript land and Its describe in their site as:
Your new, lightweight, Javascript framework
AlpineJS is very simple and easy to use. It has 3 pillars: Attributes, Properties and Methods. My goal is not to introduce Alpine, but show our strategy to modulize the application using Alpine.
Page and Components
A page is composed by many components, navbar, cards, box, menu, fields, graphs etc. In Alpine a component can be a simple div with x-data
attribute, simple ha!? To reuse component's logic we decide to create a single JS file that represents logic and state of each component. Let's see a simple example of a file with counter
.
export function counter() {
return {
count: 0,
reset() {
this.count = 0;
},
increment() {
this.count++;
},
decrement() {
this.count--;
}
}
}
In the example above we have created a counter
component with count
attribute and 3 operations: reset, increment and decrement. In HTML side we need to attach its function with our component, like:
<div x-data="counter" class="box-counter">
<span class="lbl-counter"
:class="{'lbl-counter-red': count < 0, 'lbl-counter-blue': count > 0}"
x-text="count">0</span>
<div class="">
<button type="button" class="btn-counter" @click="increment"> Increment </button>
<button type="button" class="btn-counter" @click="reset">Reset</button>
<button type="button" class="btn-counter" @click="decrement"> Decrement </button>
</div>
</div>
As you can see, our div
tag has an attribute x-data
that has value counter
. So Alpine does the magic here linking both (HTML and Javascript).
Very simple and scalable to create components like that. But let's imagine a page with 20 or 30 components like it, I think we will have a messy page and very hard to maintain.
Let's break down our problem into 2 parts: script composition and loading.
Script Composition
The structure of application is based on pages and each page has an index.ts
that will exports all components necessary to that page. On image below you can see POC structure:
According to the image, we have 4 pages: demo, home, login and prospects. We created a folder shared
that contains all shared components between the pages, like: menu, navbar, etc. Let's explore the demo page.
The demo page is composed by 3 components: menu, counter and todos. The index.ts
file for this page is shown below:
import menu from '../shared/menu'
import counter from './counter'
import todos from './todos'
export {
menu,
counter,
todos
}
The demo HTML page has 3 HTML elements referring to those components, let's see the snippet of the HTML page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
<link rel="stylesheet" href="assets/global.css" />
</head>
<body>
<nav x-data="menu" class="nav-header">
...
</nav>
<div x-data="counter" class="box-counter">
...
</div>
<div x-data="todos" class="todoapp">
...
</div>
</body>
</html>
Using this strategy we can build very sophisticated pages in a modular manner very easily. One problem was resolved, so we need to nail down the second one.
Script Loading
Script loading is very important issue to reduce boilerplate code. We have created a loader
function that solve it for us. The loader function is shown below:
export async function loader(modules) {
const { default: alpinejs } = await import('https://cdn.skypack.dev/alpinejs')
let promises = modules.map((mod) => import(mod))
return Promise.all(promises).then(values => {
console.debug('Alpine', alpinejs.version)
values.forEach(module => {
Object.keys(module).forEach(attr => {
let data = module[attr]();
alpinejs.data(attr, () => data);
})
})
alpinejs.start();
})
}
It is a naive example that loads Alpine's runtime dynamically from CDN and loads all modules passed by the argument and registers them into Alpine as components.
Now we just use it in our HTML page to load each page module.
<script defer type="module">
import { loader } from './assets/loader.js'
loader(['/dist/demo/index.js']).catch(err => console.error(err))
</script>
As you can see we put our compiled Javascript file inside /dist/demo/index.js
. Its a standard we decided to our application and works fine for us. We are using rollup
to transpile our Typescript code and bundle it.
Summarize
Alpine is a great player for us and its simplicity helps us to be more productive.
I hope this article can help you and suggestions are very welcome!
Top comments (6)
Adding
defer
attribute to inlined scripts has no effect. It only works here because modules are deferred by default.Why don't you just add alpine import to
index.ts
and do component initialization there? Using thisloader
here makes no sense to me.If you don't want to repeat initialization in every
index.ts
, you can do this in an inlined module:Also, you're
await
ing foralpine
before starting components loading. You might start loading it in parallel and then continue component initialization when it's ready:How would you extend your code/solution to import modules when needed, e.g. on a click? I have seen lazy loaded import but how can I access alpinejs?
I think your second example code tries to do just that, but I get error in console:
loader.js:12 Uncaught (in promise) ReferenceError: alpinejs is not defined
at loader (loader.js:12:2)
Hey @marzelin thanks for your observations.
this article just show to reuse js,
if i wannt reuse html template ,I have to copy it like this:
this way is ugly ,Is there a better way?
I recently used Alpine In a new project at work very successfully. As is the case with new projects, organising the project folders and making the code modular is an iterative process in itself. Your article is GOLD! Thank you for sharing your experience. I can’t wait to put it into practice.
Can you please share code pen or Github for this? Thanks.