DEV Community

Cover image for Build modular app with Alpine.js
Keuller Magalhães
Keuller Magalhães

Posted on

Build modular app with Alpine.js

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
Enter fullscreen mode Exit fullscreen mode

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--;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

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
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
    })
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 (5)

Collapse
 
marzelin profile image
Marc Ziel
<script defer type="module">
Enter fullscreen mode Exit fullscreen mode

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 this loader 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:

<script type="module">
    import alpinejs from 'https://cdn.skypack.dev/alpinejs';
    import * as comps from '/dist/demo/index.js';
    Object.keys(comps).forEach(comp => {
      let data = comps[comp]();
      alpinejs.data(comp, () => data);
    });
    alpinejs.start();
</script>
Enter fullscreen mode Exit fullscreen mode

Also, you're awaiting for alpine before starting components loading. You might start loading it in parallel and then continue component initialization when it's ready:

export async function loader(moduleURLs) {
    const alpineP = import('https://cdn.skypack.dev/alpinejs');
    const modules = await Promise.all(moduleURLs.map(async (modURL) => {
      const module = await import(modURL);
      const { default: alpinejs } = await alpineP;
      Object.keys(module).forEach(attr => {
        let data = module[attr]();
        alpinejs.data(attr, () => data);
      })
      return module;
    }));
    alpinejs.start();
    return modules;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mikaelalfort profile image
Mikael Alfort • Edited

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)

Collapse
 
keuller profile image
Keuller Magalhães

Hey @marzelin thanks for your observations.

Collapse
 
jackzhoumine profile image
jackzhoumine

this article just show to reuse js,
if i wannt reuse html template ,I have to copy it like this:

  <nav x-data="menu" class="nav-header">
      ...
    </nav>

  <nav x-data="menu" class="nav-header">
      ...
    </nav>
Enter fullscreen mode Exit fullscreen mode

this way is ugly ,Is there a better way?

Collapse
 
colinj profile image
Colin Johnsun

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.