Emily

Emily

How To Get Phoenix & VueJS working Together?

I have VueJS GUIs with the project generated using Webpack.

I have Elixir modules that will need to be used by the VueJS GUIs.

I foresee I will eventually have an umbrella Phoenix project. Right now it is just VuejS, Javascript, and Elixir modules.

I need to merge the files from VueJS, Elixir, & Phoenix into 1 project.

I need it to eventually be a compiled standalone app.

I have no clue where to go next doing this according to conventions.

Anyone have a guide or tutorial for the next steps?

Most Liked

maz

maz

building on @paulanthonywilson 's post, in case you weren’t aware you will need to do a couple other things to get Vue.js “working”

This is my naive implementation, there may be more steps for other features and nuances but this will get something showing up in a browser. Using Phoenix 1.4 RC0:

edit package.json:

{
  "repository": {},
  "license": "MIT",
  "scripts": {
    "deploy": "webpack --mode production",
    "watch": "webpack --mode development --watch"
  },
  "dependencies": {
    "phoenix": "file:../deps/phoenix",
    "phoenix_html": "file:../deps/phoenix_html"
  },
  "devDependencies": {
    "@babel/core": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "babel-loader": "^8.0.0",
    "copy-webpack-plugin": "^4.5.0",
    "css-loader": "^0.28.10",
    "mini-css-extract-plugin": "^0.4.0",
    "optimize-css-assets-webpack-plugin": "^4.0.0",
    "uglifyjs-webpack-plugin": "^1.2.4",
+    "vue": "^2.5.17",
+    "vue-loader": "^15.4.2",
    "webpack": "4.4.0",
    "webpack-cli": "^2.0.10"
  }
}

cd <project root>
cd assets && npm install && node node_modules/webpack/bin/webpack.js --mode development

^^ this should have npm add Vue as a dependency

in <project root>/assets/js/: add and save new file vue-hello-world.js:

import Vue from 'vue'

const helloWorldContainer = document.querySelector("#hello-world-container")

if (helloWorldContainer) {
  new Vue({
    el: "#hello-world-container",
    data() {
      return {
        msg: "Hello World from Vue!"
      }
    }
  });
}  

^^ we will eventually add a hello-world-container div tag

in <project root>/assets/js/: edit file app.js and import/glue your vue-hello-world.js to the global app imports at the bottom:

// We need to import the CSS so that webpack will load it.
// The MiniCssExtractPlugin is used to separate it out into
// its own CSS file.
import css from "../css/app.css"

// webpack automatically bundles all modules in your
// entry points. Those entry points can be configured
// in "webpack.config.js".
//
// Import dependencies
//
import "phoenix_html"

// Import local files
//
// Local files can be imported directly using relative paths, for example:
// import socket from "./socket"

+ import "./vue-hello-world"

Open <project root>/templates/page/index.html.eex and add your div:

+ <div id="hello-world-container" >
+       <h1>{{ msg }}</h1>
+ </div>
<section class="phx-hero">
  <h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
  <p>A productive web framework that<br/>does not compromise speed and maintainability.</p>
</section>

<section class="row">
  <article class="column">
    <h2>Resources</h2>
    <ul>
      <li>
        <a href="https://hexdocs.pm/phoenix/overview.html">Guides &amp; Docs</a>
      </li>
      <li>
        <a href="https://github.com/phoenixframework/phoenix">Source</a>
      </li>
      <li>
        <a href="https://github.com/phoenixframework/phoenix/blob/v1.4/CHANGELOG.md">v1.4 Changelog</a>
      </li>
    </ul>
  </article>
  <article class="column">
    <h2>Help</h2>
    <ul>
      <li>
        <a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
      </li>
      <li>
        <a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
      </li>
      <li>
        <a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
      </li>
    </ul>
  </article>
</section>

From the project root run the app: mix phx.server

You should see a webpack log similar to:
[./js/vue-hello-world.js] 275 bytes {./js/app.js} [built]

Open localhost:4000. You should see the “Hello World from Vue!” message.

Finally, get the Chrome Vue plugin:

peerreynders

peerreynders

Seems to me lots of blogs about Phoenix get into Brunch like it’s somehow an essential part of Phoenix - it isn’t. When it comes to the development workflow there is one loose “connection” to Brunch in my_app/config/dev.exs - the watchers configuration. It contains the command to start up the process responsible for building bundles whenever the primary files change. Phoenix LiveReload is then responsible for supplying the re-built files to the browser via a hidden <iframe> which coordinates the necessary updates in the browser.

Vue with Webpack (instead of Brunch) can make use of the same mechanism. To demonstrate, start with a simple Vue.js component project:

$ tree assets
assets
├── build
│   └── webpack.dev.conf.js
├── images
├── index.html
├── js
├── package.json
└── src
    ├── App.vue
    ├── assets
    │   └── phoenix.png
    └── main.js

assets/index.html:

<!DOCTYPE html>
<html>
  <head>
    <!-- assets/index.html -->
    <meta charset="utf-8">
    <title>Vue.js Demo Page</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="js/app.js"></script>
  </body>
</html>

assets/src/App.vue:

<template>
  <div class="foo">
    <img src="./assets/phoenix.png">
    <h1>{{ msg }}</h1>
    <button id="change-message" @click="changeMessage">Change message</button>
    <p>{{ passedProp }}</p>
  </div>
</template>

<script>
  export default {
    name: 'hello',
    data() {
      return {
        msg: 'Welcome to Your Vue.js App'
      };
    },
    props: ['passedProp'],
    methods: {
      changeMessage() {
        this.msg = 'new message';
      }
    }
  };
</script>

<style>
  .foo {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    margin-top: 60px;
  }
</style>

assets\src\main.js:

// src/main.js
import Vue from 'vue';
import App from './App';

Vue.config.productionTip = false;

new Vue({
  el: '#app',
  template: '<App passedProp="Greetings!" />',
  components: { App }
});

assets/build/webpack.dev.conf.js:

// build/web.config.dev.js
const path = require('path');
const webpack = require('webpack');
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');

const PATHS = {
  assetsRoot: '/',
  assetsSubDirectory: '/'
};

function assetsPath(filePath) {
  return path.posix.join(PATHS.assetsSubDirectory, filePath);
}

function resolve (dir) {
  return path.join(__dirname, '..', dir);
}

module.exports = {
  entry: {
    app: './src/main.js'
  },
  output: {
    filename: 'js/[name].js',
    path: resolve(PATHS.assetsRoot)
  },
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src')
    }
  },
  module: {
    rules :[
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        query: {
          presets: ['env']
        },
        include:[resolve('src')]
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 20000,
          name: assetsPath('images/[name].[hash:7].[ext]')
        }
      }
    ]
  },
  // cheap-module-eval-source-map is faster for development
  devtool: '#cheap-module-eval-source-map',
  plugins: [
    new webpack.NoEmitOnErrorsPlugin(),
    new FriendlyErrorsPlugin()
  ],
  watchOptions: {
    ignored: /node_modules/
  }
};

assets/package.json:

{
  "name": "assets",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "devbuild": "node ./node_modules/webpack/bin/webpack.js  --config ./build/webpack.dev.conf.js",
    "watch": "./node_modules/.bin/webpack --watch-stdin --config ./build/webpack.dev.conf.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-env": "^1.6.0",
    "css-loader": "^0.28.7",
    "file-loader": "^1.1.5",
    "friendly-errors-webpack-plugin": "^1.6.1",
    "url-loader": "^0.6.2",
    "vue-loader": "^13.0.5",
    "vue-style-loader": "^3.0.3",
    "vue-template-compiler": "^2.4.4",
    "webpack": "^3.6.0"
  },
  "dependencies": {
    "vue": "^2.4.4"
  }
}

Check this project with:

assets $ npm i

...
added 623 packages in 10.714s

assets $ npm run watch

> assets@1.0.0 watch /Users/wheatley/sbox/vue/phx/assets
> webpack --watch-stdin --config ./build/webpack.dev.conf.js


Webpack is watching the files…
...
^C assets $

At this point it should be possible to view assets/index.html through a browser from the filesystem.

Now create a new Phoenix 1.3 project:

assets $ cd ..
       $ mix phx.new my_app --no-brunch --no-ecto

and create a new directory my_app/assets and copy the following files into it:

my_app/assets
├── build
│   └── webpack.dev.conf.js
├── package.json
└── src
    ├── App.vue
    ├── assets
    │   └── phoenix.png
    └── main.js

Modify my_app/assets/build/webpack.dev.conf.js

// build/web.config.dev.js
const path = require('path');
const webpack = require('webpack');
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');

const PATHS = {
  assetsRoot: '../priv/static', // CHANGED
  assetsSubDirectory: '/'
};

function assetsPath(filePath) {
  return path.posix.join(PATHS.assetsSubDirectory, filePath);
}

function resolve (dir) {
  return path.join(__dirname, '..', dir);
}

module.exports = {
  entry: {
    app: './src/main.js'
  },
  output: {
    filename: 'js/[name].js',
    path: resolve(PATHS.assetsRoot)
  },
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'phoenix': resolve('../deps/phoenix/priv/static/phoenix.js'),                // ADDED
      'phoenix_html': resolve('../deps/phoenix_html/priv/static/phoenix_html.js'), // ADDED
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src')
    }
  },
  module: {
    rules :[
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        query: {
          presets: ['env']
        },
        include:[resolve('src')]
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 20000,
          name: assetsPath('images/[name].[hash:7].[ext]')
        }
      }
    ]
  },
  // cheap-module-eval-source-map is faster for development
  devtool: '#cheap-module-eval-source-map',
  plugins: [
    new webpack.NoEmitOnErrorsPlugin(),
    new FriendlyErrorsPlugin()
  ],
  watchOptions: {
    ignored: /node_modules/
  }
};

Note the modified PATHS.assetsRoot and the additions to resolve.alias.

Modify my_app/assets/src/main.js:

// src/main.js
import Vue from 'vue';
import App from './App';

import 'phoenix_html';            // ADDED
// import socket from "./socket";

Vue.config.productionTip = false;

new Vue({
  el: '#app',
  template: '<App passedProp="Greetings!" />',
  components: { App }
});

Install the supporting packages:

assets $ npm i
...
added 623 packages in 11.886s

Give it a quick check:

$ npm run watch

> assets@1.0.0 watch /Users/wheatley/sbox/vue/phx/my_app/assets
> webpack --watch-stdin --config ./build/webpack.dev.conf.js


Webpack is watching the files…
...
^C assets $ 

Now modify watchers in my_app/config/dev.exs:

config :my_app, MyAppWeb.Endpoint,
  http: [port: 4000],
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: [node: ["node_modules/webpack/bin/webpack.js", # CHANGED
                  "--watch-stdin",
                  "--config", "./build/webpack.dev.conf.js",
                  cd: Path.expand("../assets", __DIR__)]
            ]

to kick off Webpack watching the frontend source files.

The same thing can be accomplished by specifying a suitable npm run script, however this rather verbose incantantion is necessary for some MS Windows installations.

Replace my_app/lib/my_app_web/templates/layout/app.html.eex with

<!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">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Hello MyApp!</title>
  </head>

  <body>
    <%= render @view_module, @view_template, assigns %>
    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

and my_app/lib/my_app_web/templates/page/index.html.eex with

<div id="app"></div>

Finally start Phoenix

assets $ cd ..
my_app $ mix phx.server
Compiling 12 files (.ex)
Generated my_app app
[info] Running MyAppWeb.Endpoint with Cowboy using http://0.0.0.0:4000

Webpack is watching the files…

 DONE  Compiled successfully in 924ms20:40:49
...
  [14] ../deps/phoenix_html/priv/static/phoenix_html.js 1.23 kB {0} [built]
    + 6 hidden modules

Now open the browser at http://localhost:4000/ to see the “Hello MyApp!” page.

If you modify the message text in my_app/assets/src/App.vue Phoenix LiveReloader will update the page in the browser shortly after you save the file (and the bundle has been rebuilt).

With a basic setup like this it should be possible to go further by “scavenging” features from other non-Phoenix Vue “donor” projects like the ones generated by vue-cli, e.g. $ vue init webpack my-project.

bkolobara

bkolobara

I have converged to a similar setup in most of my web projects. If you can avoid server-side rendering having a SPA and treating the backend as an api makes things much simpler. Even better with GraphQL.

I like to keep my frontend and backend in different folders. Usually the fronted is a pure npm project and the backend is a mix project. This has multiple benefits:

  • Idiomatic Webpack, TypeScript and ReactRouter setup with hot reloading on the frontend. No need to worry about how to fit it into Elixir’s folder structure or introducing additional boilerplate into Elixir to make the hot reloading work.
  • The Webpack Dev Server proxies all /api call to the Elixir application running on another port.
  • In production all the static files are hosted on a CDN (404s are redirected to index.html and /api calls are proxied to the production Elixir instance).
  • If something doesn’t work googling becomes much easier. With already complex toolchains like JSX + TypeScript I prefer to stick to the simplest structure and avoid pushing it into another folder structure, in this case Elixir’s.

Where Next?

Popular in Questions Top

marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
jaysoifer
Is there a way to rollback a specific migration and only that one ("skipping" all the other ones)? Would mix ecto.rollback -v 2008090...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Other popular topics Top

josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39247 209
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35953 110
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I'm a nov...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

We're in Beta

About us Mission Statement