This is a demo app illustrating an approach to integrating Vue into a Django app. Vue Single File Components are cross-compiled to native Web Components and used by custom element tag name in Django Templates.
A variety of techniques are demonstrated, which are described in the Annotated Code section.
To incorporate Vue into your own project, see Integrating Vue into your own Project
- Python 3.12 (or compatible Python 3.x)
- Node.js 18+ and npm (required for the Vue dev server and optional CSS rebuilds)
If you have not already cloned this project, do so:
- cd annotated_django_vue
- python3 -m venv .venv
- source .venv/bin/activate # Windows: .venv\Scripts\activate
- pip install -r requirements.txt
- npm install
- python manage.py migrate
- python manage.py runserver
The Vue app lives under vue/frontend
and is served by Vite during development.
In a separate terminal, start the Vite dev server:
- cd vue/frontend
- npm install
- npm run dev
Then open http://127.0.0.1:8000/ in your browser. The admin site is at http://127.0.0.1:8000/admin/ (log in with admin/admin).
- The CSS is generated from PicoCSS Sass files located in
scss/
- If you modify the sass, you'll need to recompile the sass files to generate the new main.css:
- Install Node.js and npm
- Run
npm install
- Run
npm run build-css
for a one-off build - Or run
npm run watch-css
to auto-rebuild on changes
The following steps will add a very simple Vue project to your Django project, allowing you to use Vue components by custom-tag name in your Django templates.
For advanced techniques, see Annotated Code.
From your Django root directory
mkdir -p vue
cd vue
npm create vue@latest
Answer the questions.
- Name your project, e.g.
frontend
- I recommend enabling at the following features
- TypeScript
- Pinia
- ESLint
- Include example code or not as you prefer.
cd frontend
npm install
npm run dev
In the entrypoint main.ts/js
, define Vue Single File Components (SFCs) as native web components.
For example, to define the 'App' element, use the following:
import {defineCustomElement} from 'vue'
customElements.define('my-app',
defineCustomElement(App, {
shadowRoot: false,
configureApp: (app) => {
app.use(pinia) // if using pinia
}
}),
);
This will create a custom element my-app
that can be used in your HTML.
See Annotated Code for additional strategies.
Somewhere in the Django template tree where the custom element is used, import the entrypoint.
<script type="module" src="http://localhost:5173/src/main.js"></script>
Note this links to the Vue dev server. For production configuration, see Annotated Code.
In your django templates, use the custom element by tag name, providing any property values as simple HTML attributes.
<my-app welcome-message="Hello World!"></my-app>
First, create a new Vue component (1). Next, create a Web Component in an entrypoint file (2). Ensure the entrypoint is included in the Django template tree (3). Finally, use the component by custom tag name (4), passing property values as simple HTML attributes.
- vue/frontend/src/components/DiceIcon.vue:1
- vue/frontend/src/main.ts:14
- rpgdice/templates/base.html:76
- rpgdice/templates/index.html:11
Components can make AJAX requests to Django views and render the partial pre-rendered HTML responses. First, create a view that will return partial HTML (1). Next, a component will listen for an event (2) and then make the AJAX request (3). The response HTML is displayed directly in an element using v-html (4).
- rpgdice/views.py:62
- vue/frontend/src/components/DiceKey.vue:26
- vue/frontend/src/components/DiceKey.vue:12
- vue/frontend/src/components/DiceKey.vue:34
Components consume AJAX responses that include JSON, and this can be done without needing a full REST API. Simply return a JSONResponse (1) in the view. A component makes the AJAX request and consumes the JSON payload (2).
Components can share state information with other components using Pinia. First, create a Pinia instance in an entrypoint (1). Next, designate which components need access to Pinia (2). Then, create a store with reactive state variables (3). In components, access the store (4), then use the state variables within the component (5, 6).
- vue/frontend/src/main.ts:7
- vue/frontend/src/main.ts:19
- vue/frontend/src/store/dice.ts:4
- vue/frontend/src/components/settings/DiceColorChoice.vue:5
- vue/frontend/src/components/settings/DiceColorChoice.vue:13
- vue/frontend/src/components/DiceIcon.vue:17
Unless persisted, Pinia state is lost when the page is reloaded. State can be persisted across page loads by using pinia-plugin-persistedstate (1), which uses the browser's native storage to serialize and deserialize state. To persist a store, use the persist: true option in the store config (2) to persist the entire store. It is also possible to persist only specific state variables.
A Vue component can provide one or more slots (1). This is very useful for passing in content that includes Django content (2), such as static and url template tags. Slots also allow different content to be passed to the same component (3).
- vue/frontend/src/components/DicePresetCard.vue:54
- rpgdice/templates/rpgdice/dice_preset_list.html:21
- rpgdice/templates/rpgdice/roll_dice_detail.html:8
Vue components can teleport content anywhere in the DOM. This can even be done conditionally (e.g., with v-if or :disabled). Teleported content can include any normal Vue logic, despite appearing anywhere on the page (2,3).
- vue/frontend/src/components/DicePresetCard.vue:57
- vue/frontend/src/components/DiceKey.vue:38
- vue/frontend/src/components/settings/DicePresetNameFilter.vue:29
Configure multiple entry points in Vite (1) and load components only on pages that need them (2, 3). If dynamic imports are used within the registration function, then those components (along with their dependencies) will only be loaded on pages that need them.
- vue/frontend/vite.config.ts:26
- rpgdice/templates/index.html:42
- rpgdice/templates/rpgdice/dice_preset_list.html:41
Instead of directly registering custom elements in the entrypoint, use a helper function (1) to allow the calling page to selectively register components (2).
Vue will coerce values for string, boolean, and number properties from the HTML attributes. However, anything more complex will simply be treated as a string. If a component needs to receive more complex data, the simplest approach is usually with an AJAX request. However, if avoiding an AJAX request is desired, complex data can still be passed in other ways. One way is to expose the Pinia store in an entrypoint (1). Then call a method or use a variable from the Django template (2) to set the state in conjunction with the Django json_script template tag (3). Components can then use the store normally to access the data (4).
- vue/frontend/src/packages.ts:18
- rpgdice/templates/index.html:49
- rpgdice/templates/index.html:46
- vue/frontend/src/components/PackageInfo.vue:5
Even when using Django Template, the Vue development server features such as Hot Module Replacement (HMR) and Vue Dev Tools work as expected. The Vue dev server frontend can also be accessed from a browser, usually on http://localhost:5173. This feature can often be useful for larger teams, where the JavaScript crew can develop components without having to run a Django server.
Vue components can be styled with SCSS (1). If the Django project is using an SCSS framework, those project files can be imported (2) as needed to refer to variables and mixins (3).
- vue/frontend/src/components/settings/DicePresetNameFilter.vue:34
- vue/frontend/src/components/settings/DicePresetNameFilter.vue:37
- vue/frontend/src/components/settings/DicePresetNameFilter.vue:43
Normally, the content inside a custom tag is displayed even if the tag is not defined. Since components are registered near the end of the body, this means there can be a brief flash of improperly rendered slot content before the tag becomes defined. To prevent this, add CSS styles (1) that hide content inside of undefined tags. Exceptions to this rule can be made if needed (2).
During a production build, Vite will normally produce separate JavaScript and CSS files. This means both would normally need to be loaded in the Django template. This is cumbersome at best, but can be avoided with a plugin that will automatically inject the CSS when the JavaScript entrypoint is loaded (1).
Vite will normally strip any exports from entrypoints that are unused in the JavaScript project, as part of its optimization. However, if calling exports from Django is needed, Vite must be configured to preserve the entrypoint exports (1).
In a production build, Vite will produce JavaScript files. Vite can output these to a Django static directory (1) and from that point they can be treated as normal static files. To prevent having to manually swap URLs between static production and Vue devserver, set up settings based on the Django DEBUG setting (2). Use a context processor to add this to the template context (3), and then use the new context variable to refer to the correct entrypoint URL throughout templates (4).