Tywyn, Wales (2015)

Tywyn, Wales (2015)

A Comprehensive Introduction to using Vue with Django Templates

How to use Vue as a replacement for jQuery.


Just to be perfectly clear — this is a guide on how to use Vue.js within Django templates.

This is not a guide on how to use Vue in a separate, decoupled frontend app connected by DRF. This is about using Vue directly inside of ordinary Django templates. There are plenty of excellent guides on the former, fewer on the later (but we’ll give a shout out to each and every one of them!).

This article will give you the step-by-step rundown on how to add new Vue components to your Django app. We’re also going to touch on some of the JavaScript tooling that makes this happen. The examples in this article use vite & django-vite, but another bundler setup like webpack & django-webpack-loader will work just as well.

In future articles, we’ll dive into some more interesting examples of what you can do with Vue in Django. This article is just for laying the technical groundwork that will make that possible.

Let’s get to it!

Table of Contents:

Why would anyone do this?

Even when people are writing their frontends using plain Django templates, they’re still going to need to write JavaScript from time to time.

At it’s most simple, Vue can act as a drop-in replacement for all of those standalone JavaScript/jQuery scripts. There comes a point where your patchwork of JavaScript code will be easier to read, write, and maintain when you use a cleaner interface, type-checking, and a proper bunder for node packages. The workflow outlined in this article can help with all of that.

But Vue can also offer more than that. You can drop complete Vue components right into your Django templates, creating little islands of interactivity. You can scale up all the way up and create mini DRF-powered apps within your templates, or directly enhance your existing Django template HTML with Vue markup.

Why use Vue instead of React?

Aside from one’s personal perference of one framework’s API over the other, there are structural reasons why Vue works better than React for this use case. Vue was designed to be able to drop into existing Sever-Side Rendered (SSR) HTML templates like Django. You can directly augment the HTML within your Django templates with Vue markup. React can’t augment an existing HTML template — it has to stand completely on its own as its own app. Vue can also stand on its own, using Single File Components (SFC). But SFCs are lighter weight than full React apps, and a lot easier to iteratively create and inject into Django templates. You can read more about the differences between the two frameworks in Vue’s own comparison breakdown here.

So then why Vue instead of Svelte/htmx/Mithril/…? Because I don’t know what any of those are and I’m only capable of learning a maximum of 1 new JavaScript framework every year. Next Section.

Experiment 0: A Counter written in Vanilla JS + jQuery + Django

To help give us some context around what Vue and Vite can offer for us, I’d like to first walk through a super bare-bones implementation of a JavaScript app in a Django template. If you’re not interested in this exercise, feel free to skip ahead to Experiment 1: Vue SFC in Django.

Below, we have a Django page rendered within a nicely framed iframe. It includes a simple Counter app implemented with vanilla JS and jQuery.

Let’s step through this code line by line and see how it works.

The Django Template

Here’s the Django template for Experiment 0.

000_vanilla_js.html
{% extends "_experiment.html" %}
{% load static %}
{% block experiment %}
<script src="{% static 'js/vendor/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'js/000_vanilla_js.js' %}"></script>
<div
class="relative flex items-center justify-center space-x-3 rounded-lg border-4 border-amber-600 bg-yellow-100 px-6 py-5 shadow-sm max-w-lg"
>
<div class="my-5 text-base leading-7 space-y-3 flex flex-col">
<p class="pb-5 text-center">
This counter was made with jQuery and vanilla JavaScript.
</p>
<div class="flex flex-row mx-auto">
<button
class="text-white font-bold py-2 px-4 button rounded-l bg-red-500 hover:bg-red-700"
id="decrement"
>
-
</button>
<div
class="px-4 py-2 text-lg font-semibold text-gray-700"
id="counter"
>
0
</div>
<button
class="text-white font-bold py-2 px-4 button rounded-r bg-green-500 hover:bg-green-700"
id="increment"
>
+
</button>
</div>
</div>
</div>
{% endblock experiment %}

It’s super bare-bones. Tailwind aside, this is probably how you would’ve implemented this feature 20 years old.

1. We extend our existing base template.

{% extends "_experiment.html" %}

2. We load the built-in “static” templatetag that allow us to load our JS files.

{% load static %}

3. We place the content for this page within the “experiment” block that was defined in our base template _experiment.html

{% block experiment %}

4. We load in our static JS files using the static tag.

<script src="{% static 'js/vendor/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'js/000_vanilla_js.js' %}"></script>

I just want to take a second to highlight how we’re accomplishing this without any external JavaScript tooling:

  • We’re not using npm to install jQuery, we’re directly loading in the vendor file.
  • And we’re not using any JavaScript bundling or compilation, we’re just loading in 000_vanilla_js.js as a static JavaScript file.

And that works perfectly fine, up to a point. Plenty of legacy Django projects still follow this approach.

5. And then we build the actual counter.

<div>
...
<button> - </button>
...
<button> + </button>
...
</div>

The JavaScript

Great. Now let’s take a quick look at the JavaScript code.

static/js/000_vanilla_js.js
jQuery(function () {
var getCounterValue = function () {
return parseInt($("#counter").text(), 10);
};
var setCounterValue = function (value) {
$("#counter").text(value);
};
$("#decrement").on("click", function () {
var oldValue = getCounterValue();
var newValue = (oldValue -= 1);
setCounterValue(newValue);
});
$("#increment").on("click", function () {
var oldValue = getCounterValue();
var newValue = (oldValue += 1);
setCounterValue(newValue);
});
});

It’s jQuery. Pretty simple. Nothing to really point out here.

Now that we have a clear understanding of how this thing works in jQuery + vanilla JS, let’s take a look at what goes into implementing this same feature with Vue.

Experiment 1: A Counter written in Vue + Django

Here we have the exact same app, still embedded within a Django template, but this time it’s been written in Vue:

For those of you new to Vue.js programming, this section is also going to be a super crash course for you.

Definitely take a look at the official Vue docs at some point. In the meantime, let’s clarify a couple pieces of important Vue terminology:

  • A Vue app is the root container for all of your other vue stuff. This is where you define all of the components that you want to use in your HTML. And if there is shared state data between your Vue Components, it’ll be managed through the app instance. (Read more)
  • A Vue component is a little reuseable chunk of Vue code. It has it’s own self-contained logic, template rendering, and css styles. (Read more)

Let’s walk through all of the code that went into rendering this page and see how it differs from our vanilla JS implementation. We’ll start by looking at the Django template.

The Django Template

001_vue_mvp.html
{% extends "_experiment.html" %}
{% load django_vite %}
{% block experiment %}
{% vite_asset 'js/apps/001_vue_mvp.ts' %}
<div id="vue-experiment-1">
<demo-counter></demo-counter>
</div>
{% endblock experiment %}

We’re still extending the same base “_experiment.html” Django template. But now we’re loading the django-vite template tag instead of static.

Once django-vite is loaded, we can then use the vite_asset tag to load in our JavaScript file, . The vite_asset tag generates a <script> tag for us that links to our vite development server. Fundamentally, it’s doing the same thing as Experiment 0’s In this case, it’s a TypeScript file named 001_vue_mvp.ts which contains our Vue app.

That Vue app is instructed to bind to the element with id="vue-experiment-1". Any child of that element will be parsed by Vue, which allows Vue to swap out <demo-counter></demo-counter> for the actual <DemoCounter/> component.

The Vue App

Let’s look at that 001_vue_mvp.ts file.

js/apps/001_vue_mvp.ts
import "vite/modulepreload-polyfill"; // required for vite entrypoints
import { createApp, defineComponent } from "vue";
import DemoCounter from "@/js/components/DemoCounter.vue";
const RootComponent = defineComponent({
delimiters: ["[[", "]]"],
components: {
demoCounter: DemoCounter,
},
});
const app = createApp(RootComponent);
app.mount("#vue-experiment-1");
export {};

One setting we’re changing from the default Vue configuration is switching our delimiters from the default ["{{", "}}"] to ["[[", "]]"]. This is to avoid conflicts with Django template’s own {{ }} variable expansion delimeter. Any variable enclosed in {{ }} will be parsed by Django as usual, but any value enclosed in [[ ]] will be parsed by Vue.

On this line, we instruct Vue to bind the app instance to the HTML element with the id “vue-experiment-1”:

app.mount("#vue-experiment-1");

Now, every child element inside of that div can be manipulated by Vue! And we can even inject Vue components as children of that root div.

Within our app definition, we had defined demoCounter as a component:

components: {
demoCounter: DemoCounter,
},

Which allows us to use it in our Django template (as long as it’s a child of that root <div id="vue-experiment-1"> element).

<div id="vue-experiment-1">
<demo-counter></demo-counter>
</div>

How Vue parsing works in Django Templates

You might have some questions about how we got from DemoCounter to <demo-counter></demo-counter>.

The part of the Vue documentation that is going to be very relevant to us is in-DOM Template Parsing Caveats. In a Vue.js SFC, you’d be able to use a <DemoCounter/> component directly in your template. But our Django template is not being parsed by Vue, it’s being parsed by the browser’s DOM parser like any other plain HTML file. So we have to follow the DOM parser’s rules.

That means we can’t have self-closing tags (<DemoCounter/> becomes <DemoCounter></DemoCounter>). And since the DOM parser is case insensitive, we need to convert our multi-word components to kebab-case (<DemoCounter></DemoCounter> becomes <demo-counter></demo-counter>).

It’s considered a best practice to give your Vue components names that are at least 2 words long (ex: “DemoCounter” instead of “Counter”). 1 word components could potentially conflict with future reserved element tags that could be added to the HTML spec in the future. See rules/multi-word-component-names.

When your page first loads in the browser, <demo-counter></demo-counter> is just a placeholder. It doesn’t do anything. But once Vue processes it (when app.mount('#vue-experiment-1'); is actually run) it’ll be replaced by the actual HTML defined in your DemoCounter Component.

Before:

<div id="vue-experiment-1">
<demo-counter></demo-counter>
</div>

After 🎉:

<div id="vue-experiment-1">
<div>
...
<button> - </button>
...
<button> + </button>
...
</div>
</div>

Let’s look at that component now.

The Vue Component

js/components/DemoCounter.vue
<template>
<div
class="relative flex items-center justify-center space-x-3 rounded-lg border-4 border-amber-600 bg-yellow-100 px-6 py-5 shadow-sm max-w-lg"
>
<div class="my-5 text-base leading-7 space-y-3 flex flex-col">
<p class="pb-5 text-center">
This counter was made with a Vue component loaded into a Django Template.
</p>
<div class="flex flex-row mx-auto">
<button class="button rounded-l bg-red-500 hover:bg-red-700" @click="decrement">
-
</button>
<div class="count-display">{{ count }}</div>
<button
class="button rounded-r bg-green-500 hover:bg-green-700"
@click="increment"
>
+
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
const decrement = () => {
count.value -= 1;
};
const increment = () => {
count.value += 1;
};
</script>
<style>
.button {
@apply text-white font-bold py-2 px-4;
}
.count-display {
@apply px-4 py-2 text-lg font-semibold text-gray-700;
}
</style>

Nothing really special to point out here. This is a standard Single File Component (SFC). It should give you a decent idea of how to replicate jQuery logic within Vue. There are plenty of other resources online to teach you Vue development, so we’re not going to get too deep into it here.

Step-by-step: adding a new Vue component to your Django project

Okay, let’s wrap up what we’ve learned. Here are the steps we need to add a Vue component to a Django template.

  1. Create a Vue app
    Terminal window
    js/apps/001_vue_mvp.ts
  2. Load that app into your Django template.
    001_vue_mvp.html
    {% load django_vite %}
    {% vite_asset 'js/apps/001_vue_mvp.ts' %}
  3. Mount your Vue app to an element in your Django template.
    js/apps/001_vue_mvp.ts
    app.mount("#vue-experiment-1");
    001_vue_mvp.html
    <div id="vue-experiment-1">
  4. Use any components or Vue syntax within that element.
    001_vue_mvp.html
    <div id="vue-experiment-1">
    <demo-counter></demo-counter>
    </div>
  5. Finally, remember to add the Vue app as an entrypoint in your Vite config. (See django-vite docs for more details.)
    vite.config.js
    export default defineConfig({
    ...
    base: "/static/",
    build: {
    ...
    manifest: "manifest.json",
    outDir: resolve("./assets"),
    rollupOptions: {
    input: {
    '001_vue_mvp': './src/vite_assets/js/apps/001_vue_mvp.ts'
    }
    }
    }
    });

Conclusion

Now that we’ve learned how to integrate Vue with Django templates, we can now look at more complex (and useful!) applications of this technology. We’ll do that in future articles in this series.

If you want to look at some more of my examples to get you started, you can check out these repos:

  • django-vite-examples: Very minimal Django projects that utilize django-vite to load in Vue apps.
  • django-vue-experiments: Examples of more complex Vue + Django integrations.
  • supergood-reads: An experimental demo app where I’m trying out bleeding edge (and possibly ill-advised) Vue + Django integration strategies.

Further Reading

Here are all the other resources I’ve found that deal with Vue in Django templates.