From Vue to Alpine.js on a legacy project
Published: 2024-03-23 07:31:24

I have been struggling with progressive enhancement on a legacy project. The requirements are simple: there is some PHP-generated HTML that could be nicely enhanced.

Our approach has been using Vue 2 inline templates. This feature neatly separated HTML code (server generated) from JS and CSS code, that lived on the client.

No JS? No problem: the HTML works anyway.

Unfortunately, Vue 3 removed support for inline templates. This left us in a tough spot, since Vue 2 is unsupported and EOL.

Enter Alpine.js. You know Vue? Alpine is the same sauce. Minus lots of complcations. It is just perfect for this use case!

Migrating from Vue 2 to Alpine.js is really, really straightforward, and you gain a lot of benefits:

- reactivity is super easy
- reactivity and events bubble outwards, to the outermost Alpine.js enabled element. No need to have props and weird questions about communicating between parents and children!
- data sharing among different components is easy: you can have a global reactive store and bind to that
- all is inline! but you can have some separation of concens and move JS code to a separate file, so that the HTML becomes lighter
- error reporting is stellar: look at your console and you will see a specific and well described error!

## From file.vue to Alpine.js

### Template (HTML)

If you have a component, it will have its own template, somewhere. 

If it is an inline-template, then replace `inline-template` with `x-data="customcomponent"`. 

```
<div is="custom-component" inline-template>...</div>
```
becomes
```
<div x-data="customcomponent">...</div>
```

Otherwise, you probably have some `<custom-component />` code in your HTML: copy and paste your HTML template that you have in your .vue file, and replace `<custom-component />` with the entire HTML template. Then add  and `x-data="customcomponent"` to the outermost tag.

What is `customcomponent`? It is the name of the JS component that we will create in the next step. The `customcomponent` will be registered at the last step of this tutorial, and will allow Alpine.js to connect this block of HTML to the correct script that automates it.

Note that you can (and should) inline x-data too, and have all your logic in one place. But I prefer to separate it as soon as it becomes even slightly complex.

Now that your HTML is mostly in place, you will need to replace the `v-` attributes with... `x-` attributes. You can just rename most of them: they are equivalent.

One exception is `x-if` and `x-show`: `x-if` works on a `<template>` tag only, so you should probably start by renaming `v-if` to `x-show`, unless you have special requirements.

### CSS

Your CSS code goes to the main CSS. No magic here, sorry.

### JS

1. Create a new `custom-component.js` file
2. Start like this:
```
export default () => ({
  // Code here
});
```
3. Paste the insides of your .vue `script` tag to where `// Code here` is. 
4. Promote all `data` to the outermost scope:

```
export default () => ({
  reactiveVar1 : 'hi!',
});
```

5. Promote all `computed` to the outermost scope:
```
export default () => ({
  reactiveVar1 : 'hi!',
  computeMe() : { return this.reactiveVar1; },
});
```

- Rename `mount()` to `init()`

```
export default () => ({
  reactiveVar1 : 'hi!',
  computeMe() : { return this.reactiveVar1; },
  init() : { reactiveVar1 = 'hello world'; }
});
```

- Have a `watch` block? Promote all the watching function to the main scope, and then connect them manually in your init() function, like this:

`this.$watch('watchedVariable', (newValue) => this.watchedVariableChanged(newValue))`

- Adjust special Vue features, if you have any

## Bring it all together now

You will need to bootstrap Alpine.js somewhere in your JS code, at the top level of your app, the same as you did with Vue.

You would do something like this:

```
import Alpine from 'alpinejs/dist/module.cjs';
import collapse from '@alpinejs/collapse'

import customcomponent from './custom-components.js';

window.Alpine = Alpine

document.addEventListener("DOMContentLoaded", function () {
    Alpine.plugin(collapse)

    Alpine.data('customcomponent', customcomponent)

    Alpine.start()
});
```

We have imported our JS, we introduced it to Alpine and we  gave it a name: now we can use it.

Go to your HTML file and replace `data='{ someReactiveVar = 1; }'` with `data='customcomponent'`.

Reload and enjoy!