Django Vue.js integration as a widget

2018-06-22

For a side project I am currently working on I was in the need of some interactive (JavaScript) widget. After reading a lot about Vue in the last time, I decided to give it a try. Turns out if you want to add Vue to an existing project and still keep the benefits of single file components, a hot-reloading server etc., you are leaving the common scenario of SPAs and have to work around a few things.

Because I could not find a good resource describing how to integrate Vue in a widget-like manner in Django, this is how you can do it:

Add webpack_loader to Django and be aware of it's issues

The webpack_loader package is a python lib which allows to dynamically load webpack generated bundles. After adding some basic stuff to your settings.py, you are nearly ready to go. There are some litte things you have to take care of. I have added my vue.config.js here so you can work around the problems.

In your settings.py:

WEBPACK_LOADER = {
    'DEFAULT': {
        'BUNDLE_DIR_NAME': 'vue/', 
        'STATS_FILE': os.path.join(BASE_DIR, 'static', 'vue', 'webpack-stats.json'),
    }
}

Also dont forget to add webpack_loader to you installed apps. A more detailed setup guide can be found here.

Add template loaders and tags to render generated JS files.

In the template you want to integrate the widget to:

{% load render_bundle from webpack_loader %}

<div class="vueapp" data-component="Hello2"></div>

{% render_bundle 'chunk-vendors' %}
{% render_bundle 'app' %}

The chunk-vendors bundle contains all vendor code whereas app contains your app code. I will describe in the following paragraph whats about the div.vueapp.

Add a Django.js entry point which is able to load Vue components based on a data-attribute.

To make all components available in your django template for loading, we make them all accessible from a newly created vue instance. To make this as dynamic as possible, we create a new vue instance/app for each element with a vueapp class. The component we want to load is passed via data-component attribute.

import Vue from "vue";
import store from "./store";

/* all components */
import Example1 from "./components/Example1.vue";
import Example2 from "./components/Example2.vue";

Vue.config.productionTip = false;

const components = {
  Example1,
  Example2
};

const apps = document.querySelectorAll(".vueapp");

for (let i = 0; i < apps.length; i++) {
  const el = apps[i];
  const compName = el.getAttribute("data-component");
  const app = components[compName];

  new Vue({
    el: el,
    store,
    components: { app },
    render: h => h(app)
  });
}

Bonus WTF: I ran into a strange issue when adding the for loop here. Every time I added more than one component, only every 2nd component was rendered. Wtf? When debugging, I saw that my loop was not executed enough times. I then noticed that I had used document.getElementsByClassName which is a LIVE-element (HTMLCollection). When you are looping over the result of this method and one of the elements its returned is changed/removed (vue will replace the 'app' element), the HTMLCollection changes and thus apps.length changes. What the.. Note to self: Always use .querySelectorAll.

The second thing you need to configure is a vue.config.js file, which defiens where the files should be written to when building:

const BundleTracker = require("webpack-bundle-tracker");

if (process.env.NODE_ENV === "production") {
  module.exports = {
    outputDir: "../static/vue",
    css: {
      extract: false
    },
    configureWebpack: {
      plugins: [
        new BundleTracker({ filename: "../static/vue/webpack-stats.json" })
      ],
      output: {
        publicPath: "" /* needs to be set because of a webpack_loader bug */
      }
    }
  };
}

You can start the build process now by executing vue build src/django.js.

Load Vue component with a div tag

You are now able to load a new vue component by using this tag:

<div class="vueapp" data-component="Example1"/>
<div class="vueapp" data-component="Example2"/>

The advantage of this approach is that you can still you the Vue cli/webpack stuff to develop your component and then start the build process to make it available in Django.

If you are aware of a better or simpler approach, do not hesitate to shoot me an email ;)