Phoenix 1.6 + Vue (esbuild)
Tags: elixir, vue, esbuild, phoenix
Published: 2022-01-18 16:05:03

Phoenix 1.6 [dropped support for webpack](https://fly.io/blog/phoenix-moves-to-esbuild-for-assets/) in favor of esbuild. Since this is a relatively recent update, most tutorials about using a React or Vue with Phoenix require modifying a webpack config. I followed the instructions on [this page](https://hexdocs.pm/phoenix/asset_management.html) to get esbuild working. Here is a brief tutorial on how to get Vue working with esbuild in Phoenix 1.6.

The final working code can be found here:
[https://github.com/weeksseth/phoneix_vue_chat](https://github.com/weeksseth/phoneix_vue_chat)

## Create a Phoenix project
Assuming you have installed Elixir, Hex, and Phoenix (v 1.6+), create a new Phoenix project using `mix phx.new <project_name>`. I added the `--no-ecto` flag since I am not using a database at the moment.

## Configure esbuild
Change directory to the assets folder and install the required dependencies:
```sh
npm i esbuild esbuild-vue -D
npm i vue ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view
```

Create a `assets/build.js` file and add the following code to it:
```js
const esbuild = require('esbuild')
const vuePlugin = require("esbuild-vue")

const args = process.argv.slice(2)
const watch = args.includes('--watch')
const deploy = args.includes('--deploy')

const loader = {
  // Add loaders for images/fonts/etc, e.g. { '.svg': 'file' }
}

const plugins = [
  vuePlugin()
]

let opts = {
  entryPoints: ['js/app.js'],
  bundle: true,
  target: 'es2017',
  outdir: '../priv/static/assets',
  logLevel: 'info',
  loader,
  plugins
}

if (watch) {
  opts = {
    ...opts,
    watch,
    sourcemap: 'inline'
  }
}

if (deploy) {
  opts = {
    ...opts,
    minify: true
  }
}

const promise = esbuild.build(opts)

if (watch) {
  promise.then(_result => {
    process.stdin.on('close', () => {
      process.exit(0)
    })

    process.stdin.resume()
  })
}
```

Modify the watcher in `config/dev.exs` to use node:
```diff
config :hello, HelloWeb.Endpoint,
  ...
  watchers: [
-     esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
+     node: ["build.js", "--watch", cd: Path.expand("../assets", __DIR__)]
  ],
  ...
```

Modify the aliases in `mix.exs` to install npm packages during setup:
```diff
defp aliases do
    [
-     setup: ["deps.get", "ecto.setup"],
+     setup: ["deps.get", "ecto.setup", "cmd --cd assets npm install"],
      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
      "ecto.reset": ["ecto.drop", "ecto.setup"],
      test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
-     "assets.deploy": ["esbuild default --minify", "phx.digest"]
+     "assets.deploy": ["cmd --cd assets node build.js --deploy", "phx.digest"]
    ]
  end
```

Remove the esbuild configuration from 
`config/config.exs`:
```diff
- config :esbuild,
-   version: "0.14.0",
-   default: [
-     args:
-       ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
-     cd: Path.expand("../assets", __DIR__),
-     env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
-   ]
```

And finally remove the esbuild dependency from `mix.exs`:
```diff
  defp deps do
    [
      {:phoenix, "~> 1.6.6"},
      {:phoenix_html, "~> 3.0"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:phoenix_live_view, "~> 0.17.5"},
      {:floki, ">= 0.30.0", only: :test},
      {:phoenix_live_dashboard, "~> 0.6"},
-     {:esbuild, "~> 0.3", runtime: Mix.env() == :dev},
      {:swoosh, "~> 1.3"},
      {:telemetry_metrics, "~> 0.6"},
      {:telemetry_poller, "~> 1.0"},
      {:gettext, "~> 0.18"},
      {:jason, "~> 1.2"},
      {:plug_cowboy, "~> 2.5"}
    ]
  end
```

## Add Vue
Create a new Vue component in `assets/js/components/Component.vue` with the following content:
```vue
<template>
  <h1>Hello world!</h1>
</template>
```

Replace the code in `assets/js/app.js` with the following:
```js
import Component from "./components/Component.vue";
import Vue from "vue";

new Vue({
  el: "#app",
  render: (h) => h(Component),
});
```

Add the following code to the end of `lib/<project_name>_web/templates/page/index.html.heex`:
```html
<section id="app">
</section>
```

Finally, start up your Phoenix server with `mix phx.server` and you should see the default Phoenix app with a section at the end greeting the planet. If you modify the Vue component and save it, the page should automatically rerender with your changes.

## What now?
This is the bare minimum required just to get Vue working with Phoenix. The components folder probably shouldn't be in the `js` folder since they are Vue components. Phoenix also comes with templates and layouts that you can choose to mix with Vue if you want. You'll probably want to come up with a better folder structure and change the entry point to the application. I don't know the best practices for doing this, so have fun!