Build modular app with Alpine.js
Tags: javascript, alpinejs, esm
Published: 2021-07-05 09:35:56

Recently I've created a POC involving some new frontend technologies and [Alpine JS](https://alpinejs.dev) 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`.

```javascript
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:

```html
<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:

![structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oth9df7bti037uhop1a3.png)

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:

```javascript
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:

```html
<!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:

```javascript
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.

```html
<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!