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://forum.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

Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&amp;query=perfume&amp;page=2, I would like to get: ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New

Other popular topics Top

TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 41539 114
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
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
Lily
In templates/appointment/index.html.eex: &lt;%= for appointment &lt;- @appointments do %&gt; &lt;tr&gt; &lt;td&gt;&lt;%= appoi...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 126479 1222
New

We're in Beta

About us Mission Statement