Django and Vue3
In the previous articles of this series, I’ve demonstrated a method of integrating Django templates and Vue in such a way that preserves the strengths of both frontend frameworks.
Since those articles were published, Vue3 has been released, bringing a variety of improvements and a list of breaking changes. With these numerous changes, the code examples from the prior articles in this series no longer work directly with Vue3. But happily, with only a few isolated changes, the same general approach works well with Vue3 and, moreover, the code becomes, in my opinion, simpler. In fact, we need not alter our components, our Vuex stores, or even our vue.config.js to work with Vue3. Rather, the required changes are limited to the app initialization logic.
Instead of rehashing the entire series of articles, which explain the ideas and approach in detail, this article will instead enumerate the changes needed to adapt the Vue2 solution to Vue3. At the same time, I’ll introduce a couple of additional changes that, while not strictly necessary for Vue3, improve the overall quality of the Vue + Django integration.
If you’re just starting, I suggest reading from the start of these articles to learn more about the general approach utilized in this article. Or, to get started straight away with Vue3 + Django, check out the sample application source code.
Continued from Part 3
Overview of changes
In Vue2, app initialization was generally done with new Vue()
constructor. However, this approach was eliminated in Vue3, so instead we will adapt the code to use the new createApp()
method. Similarly, we no longer can instantiate the Vuex store with new Vuex.store()
but will instead use createStore()
.
Both these changes are straightforward adaptations based on the Vue migration guide.
However, our usage with Django necessitates a couple additional changes. First, we must provide for passing of properties from Django template, as our previous approach, which relied on new Vue()
, no longer works. Second, as we may potentially be adding several Vue apps to a single page, it behooves us to extract our app/store creation logic into a callable function.
String Property Passing
The new app initialization mechanism renders our old strategy of property passing obsolete. Luckily, Vue3 allows us to pass root properties as the second argument of the createApp
method. Thus, we simply need to some way to extract them from our rendered Django template HTML. A simple and intuitive way is to use our root element’s dataset (i.e. its data-
attributes) as properties for our app, an approach I first saw described by Michal Levý. An example:
const selector = "#my_app_element";
const mountEl = document.querySelector(selector);
const app = createApp(AppComponent, {...mountEl.dataset});
With this approach, we can adjust our Django templates to pass properties as data attributes.
<div id="hello_world_b" data-msg="">
<hello-world></hello-world>
</div>
Unfortunately, with this method all properties are passed as strings, as there is no typing data associated with data attributes. This means if a property is intended to represent a different data type, e.g. a number, then the Vue application must explicitly handle this conversion. This is not ideal, as it means we must make special allowances in our Vue application for the limitations of our Django templates.
As the theme of this series of articles is using Django and Vue without sacrificing the strengths of either, let’s eliminate this shortcoming so that we may use our Vue SFCs without this compensation.
Property Passing with Datatypes
We can again utilize the root element’s dataset, this time to pass along the intended property datatype. For example, data-counter-datatype="Number"
attribute and value might be associated with a data-counter
attribute. We can then update our initialization code above to create a conversion function that checks for the existence these associated datatype attributes and performs custom data conversion as needed.
const datasetDatatypePostfix = "Datatype";
const convertDatasetToTyped = (dataset) => {
const keys = Object.keys(dataset);
keys.forEach(function(key){
let datatypeKey = key + datasetDatatypePostfix;
if (datatypeKey in dataset) {
let datatype = dataset[datatypeKey];
switch (datatype) {
case "String": //already string, do nothing
break;
case "Number":
dataset[key] = Number(dataset[key])
break;
case "Boolean":
dataset[key] = dataset[key] === 'true'
break;
// TODO: Add additional datatype conversions
default: //do nothing
}
delete dataset[datatypeKey];
}
});
return dataset;
}
The function accepts an Object representing our root element dataset, and checks for each key if an associated key with Datatype
appended exists. If so, the function performs the specified data conversion in situ and removes that datatype description entry, finally returning the resulting dataset. Note I’ve included logic for converting Number
and Boolean
types only; if your app requires other datatypes, you’ll need to supply your custom conversion logic.
We update our initialization code above then to use this function:
const app = createApp(options, convertDatasetToTyped({...mountEl.dataset}));
Now, we may pass typing information in our Django templates such that our Vue components receive properly typed properties.
Factory Initialization
To avoid repeating the same initialization code throughout our app for each App instance we need to instantiate, we can follow the Vue3 recommendation by creating a utility factory function that will init our apps. Below is a simple implementation that allows us to pass a Vuex store and HTML selector, and will take care of the creating the app, using the store, and mounting to the element described by the selector.
export const createAppInEl = (options, store, selector) => {
const mountEl = document.querySelector(selector);
const app = createApp(options, {...mountEl.dataset});
app.use(store);
// additional configurations here
app.mount(selector);
return app;
}
Any custom configuration, e.g. directives or components, you might have shared among all your apps may be included here.
Store Initialization
Our factory initialization function allows us to pass a Vuex store for use with an application. We’ll create that store using another factory initializer createSharedStore
which replaces our old new Vuex.store()
code with the new Vue3 createStore()
initialization.
Remember in our previous article we leveraged the “vuex-persistedstate” plugin to preserve our state between page loads. We can again use this plugin with Vue3 in an identical manner. However, to prevent this section from being too short, I will take this opportunity to remedy a shortcoming with my prior code.
In our previous implementation, the persisted properties were specified by the initialization code. However, this requires that the initialization code be aware of module and path names. It would cleaner if instead a store could specify if its own persisted state paths¹.
Ideally, we’d like to specify in our Vuex module itself which paths should be persisted, e.g.
export default {
state: {count: 0},
mutations: {
increment: state => state.count++,
decrement: state => state.count--
},
persistentPaths: ["count", ]
}
We can indeed use just this setup with the following update to our persistedstate
plugin initialization
createPersistedState({
paths: Object.entries(modules).map(
([mName , m]) => 'persistentPaths' in m
? m.persistentPaths.map(path => mName + "." + path)
: []
).flat(),
})
where modules
is a list of zero or more Vuex modules as defined above. This code will check for the existence of the persistentPaths
list property in each module, and if it exists it will prepend the module name to each specified property and flatten into a single combined list of fully qualified paths, which will then be persisted in our application.
Putting all this together to create a reusable store initialization factory function:
export const createSharedStore = (modules) => {
return new createStore({
plugins: [
createPersistedState({
paths: Object.entries(modules).map(
([mName , m]) => 'persistentPaths' in m
? m.persistentPaths.map(path => mName + "." + path)
: []
).flat(),
})
],
modules: modules,
strict: process.env.NODE_ENV !== "production",
});
}
¹: The argument can also be made that the initializing code should be precisely where the specification of persistent paths should be, in that a module itself should not concern itself with whether its paths are persisted or not. Conceptually, I agree, but in this case I find that the convenience of placing the entire description of a module (including its persisted condition) in a single tidy file outweighs the self-satisfaction of a inviolable separation of concern.
Conclusion
That’s the extent of the changes to needed to put your Django and Vue3 frontends in beautiful harmony. The primary changes necessitated by Vue3 are a result of its new app initialization mechanism, but in the spirit of Vue3, we’ve taken the opportunity to improve and streamline our setup.
Source
View the full example Django/Vue app or compare the changes from Vue2 to Vue3.