Build a modern web app using Django and Javascript
October 24, 2021 - by Themba Mahlangu - 22 min read
Table of Contents
Introduction
Overview of frontend architecture for Django Applications
Server-side rendered templates
SPA (single page app) frontend with Django as a Rest API
Hybrid of the first two approaches
Learn by doing
Project Requirements
Prerequisites
Structuring your project
Setup the base and landing page templates
Views, Models, and API
Create the model
Create the views and API
Wire up the routers and URLs
Create the dashboard templates and alpine component
Creating the dashboard layout and landing page templates
Other required templates
Creating the dashboard Alpine Component
Updating the dashboard Alpine component - CRUD methods for the Todo app
Adding a function to build chart options for the Chart App
Summary
Next Steps
Further reading
¶
Introduction
In this article, you will learn how to use modern javascript in your Django application without giving up the advantages of Django.
Client-side javascript is a core part of modern interactive web applications. Popular modern javascript frameworks you could use for your Django application include
React
,
Angular
, and
VueJS
.
There are also lighter javascript frameworks like
Alpine
.js (Alpine) that provide powerful tools for composing behavior directly in HTML without the need for larger frontend frameworks that may require build tools or resorting to jQuery.
¶
Overview of frontend architecture for Django Applications
Frontend architecture refers to how the frontend code is organized. There are three common approaches for organizing your Django frontend code. Each has its merits and disadvantages.
¶
Server-side rendered templates
*
HTML templates are rendered on the server, this is the traditional approach.
*
Django templates with small bits javascript added throughout your templates to make your app more interactive.
Leverages all of Django's built in features.
Great for small teams, allows for quick prototyping.
The potential downside is frontend code can be large and disorganized (but it does not have to be!).
¶
SPA (single page app) frontend with Django as a Rest API
*
Django is used purely as a backend with communication via an API.
*
Great for larger teams with specialized frontend and backend developers as different teams can work simultaneously without conflicting with each other's work.
Using a frontend framework simplifies the building of complex frontend applications.
One downside is that a lot of Django's built-in features cannot be used e.g. views, authentication, templates, and forms.
¶
Hybrid of the first two approaches
*
Javascript is used when needed, otherwise traditional django templates are used.
*
This approach aims to be the best of both worlds,
use server side Django pages when little javascript is required, for example landing pages
use client first javascript when more complex UI elements are required
The choice of approach depends on your project requirements, for example, team size or the how complex the UI is.
The hybrid approach will be the best for most apps where
80-90% of the app is ‘
*
CRUDy’
*
Only a few components require state to be stored on the client side, for example, a video editing component.
¶
Learn by doing
In this next section, we look at the important aspects of building an application using Django and modern javascript.
¶
Project Requirements
A minimum viable product (MVP) web application with a landing page and a dashboard. The dashboard will have a
**
Todos app
**
and
**
Chart App
**
Use javascript
**
*
when required,
*
**
for example to make the dashboard interactive. This means taking the hybrid approach,
use traditional Django templates for the landing page
use javascript to add interactivity to the dashboard
We will not use any build tools in this tutorial, instead, we will use an
[
AlpineJS
](https://alpinejs.dev/)
script embedded directly on the page
Our MVP needs to be beautiful, we will use TailwindCSS and DaisyUI.
¶
Prerequisites
Basic knowledge of Django &
[
Javascript
](https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/JavaScript_basics)
, specifically
[
Alpine
](https://alpinejs.dev/)
. If you are familiar with Vue you already know Alpine, if not check out the excellent alpine docs.
Django App -
[
The full code base is available on this link
](https://github.com/advantch/django-alpine-js-tutorial)
This will not be a step-by-step tutorial but we will cover the main concepts. We advise that you
*
download the code
*
if you want to follow along. The tutorial is fast-paced and assumes knowledge of Django and javascript.
The end product
Landing page - server-rendered page with no javascript, uses simple Django templates.
Dashboard using both Django templates to initially render the page and JS to add interactions
Main dashboard page is a Django template view
Routing for the todo and chart views frontend to be handled by Alpine
Data fetching for the Todo app will be also handled by Alpine
¶
Structuring your project
Clone the repo from
here
. In this first step, we will structure our project for maintainability and developer happiness. Place everything where it should be, with a clear separation of concerns. Let’s take a brief look at the project structure.
folder structure
text
`├── project
| └── config # Optional keeps config separate from apps
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── asgi.py
│ ├── api.py # not standard, will use for the api later
│ └── wsgi.py
| └── apps # All apps in this folder
| ├── dashboard
│ ├── apps.py
│ ├── urls.py
│ ├── views.py
│ ├── api.py # not standard, will use for the api later
│ ├── schema.py # not standard, will use for the api later
│ └── models.py
├─── templates
| ├── base.html
| ├── home.html
| └── dashboard
| ├── components
| ├── layout.html
| ├── home.html
| └── components
| ├── header.html
| ├── logo.html
├──── static # static files
| ├── js
| ├── css
| └── images
└─── manage.py`
Notes
apps folder
- the project will only have one app called ‘dashboard’, we can already generate this app and place it in our apps folder.
apps/dashboard
&
config
folders - you will notice additional files api.py and schema.py. These are not generated by Django. We have added them for completeness since we already know our project will need an API.
templates
base.html
- all the main pages will extend this template see below for more details
components folder
static
- static file including js, images, and styles.
js
- In this project, we will be creating javascript components. Separating out javascript from our markup (HTML) will ensure that our codebase is maintainable. It will also make it easier to switch to a front-end framework if necessary.
¶
Setup the base and landing page templates
¶
Creating the base template
The base.html template includes all the required global scripts that will be used across the app.
*
templates/base.html
*
text
`{% load static i18n %}
<!DOCTYPE html>
{% get_current_language as language_code %}
<html lang="{{ language_code }}">
<head>
<title>{% spaceless %}{% block title %}Home{% endblock %}{% endspaceless %} </title>
<meta charset="UTF-8">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{# site meta and seo meta tags, override on specific pages as necessary. #}
{% block site_meta %}{% endblock %}
{% block seo-meta %}
<meta name="robots" content="index,follow">
{% endblock %}
{% block extra_css %}{% endblock %}
{% block head_js %}{% endblock %}
<link href="{% static 'css/site.css' %}" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/npm/daisyui@1.16.1/dist/full.css" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2/dist/tailwind.min.css" rel="stylesheet" type="text/css" />
{# Alpine and app scripts #}
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body id="id-body" class="{{theme}}">
<div>
<div class="flex h-screen antialiased overflow-x-hidden bg-base-200 text-base-content">
<div class="{% block page_wrapper_class %}h-full w-full overflow-hidden fixed{% endblock %}">
{# content goes here #}
{% block content %}{% endblock %}
</div>
</div>
</div>
{% block footer_js %}
<script type="text/javascript" src="{% static 'js/cookie.js' %}"></script>
<script src="https://unpkg.com/swagger-client"></script>
<script>
// Cookie func
const csrftoken = getCookie('csrftoken');
const authHeaders = {'X-CSRFToken': csrftoken}
// See Note below on using SwaggerJS
const client = SwaggerClient('/api/openapi.json', {
requestInterceptor: (req) => {
req.headers['X-CSRFToken'] = csrftoken;
req.headers['Content-Type'] = 'application/json';
return req;
},
})
//uncomment to log api endpoints to console for debugging purposes
client.then(client => console.log(client.apis))
</script>
{% endblock %}
</body>
</html>`
Notes
<head>...</head>
- this section includes
TailwindCSS
,(styling)
DaisyUI
(pre-made UI components) AlpineJS required. In production, these files should be minified, in the next article, we will look at how to improve this setup and make it production ready.
const csrftoken
- add the csrftoken to the page, this will be required for Ajax requests to the backend
const client = SwaggerClient('/api/schema/',..
- set up a
swagger-js client
. This is a nice addition that will make interacting with the backend API more convenient.
agger is an open-source toolset that is centered around the OpenAPI Specification (OAS).
More on Swagger
Swagger is an open-source toolset that is centered around the OpenAPI specification or “OAS”.
OAS defines a standard, language-agnostic interface for both humans and computers to understand a service without access to source code.
Swagger provides tools that can interact with this specification such as
Swagger Codegen for generating server stubs, and client SDKs.
Swagger Editor for designing APIs with the OpenAPI spec
Swagger UI for visualizing & interacting with the OpenAPI specs.
In the next tutorial, we will look at Swagger in more detail.
¶
Create the home page template
The landing page extends the base.html template and only includes one, plain html header component. We expect this page to be static over time.
*
templates/home.html
*
text
`{% extends 'base.html' %}
{% block content %}
{% include "components/header.html" %}
{% endblock %}`
*
config/urls.py
*
text
`from django.contrib import admin
from django.urls import path, include
from django.views.generic import TemplateView
from django.views.decorators.cache import cache_page
urlpatterns = [
path("admin/", admin.site.urls),
path("", cache_page(60 * 15)(TemplateView.as_view(template_name="home.html")), name="home"),
]
`
Notes
templates/home.html
- our landing page only includes a header components/header.html section, for now, you can easily extend this to add a footer. For this article, we used an online tailwind page builder. You can use a ton of resources to build high-quality landing pages fast, for example,
shuffle.dev
,
devdojo
, and many more.
config/urls.py
- uses a simple Django
TemplateView
defined directly in
config/urls.py
. You will note we have added
cache_page
to avoid dynamically generating the page every time a request is made.
¶
Views, Models, and API
In this next step, we will set up a dashboard view and required api endpoints.
What we want to achieve:
use traditional Django templates to initially render the main dashboard landing.html page
include any additional data in the page context to avoid having to make extra API calls from the frontend
setup api endpoints for the Todo app
¶
Create the model
*
apps/dashboard/models.py
*
text
`from django.db import models
from django.utils import timezone
class TodoManager(models.Manager):
def to_list(self):
return [todo.to_json() for todo in self.get_queryset()]
class Todo(models.Model):
STATUS_CHOICES = [
("done", "done"),
("outstanding", "outstanding"),
]
detail = models.CharField(max_length=500)
created = models.DateField(default=timezone.now, null=False)
status = models.CharField(
max_length=100, null=True, blank=True, choices=STATUS_CHOICES
)
updated = models.DateField(default=timezone.now, null=False)
objects = TodoManager()
def to_json(self):
return {
"id": self.id,
"created": self.created.strftime("%Y-%m-%d"),
"detail": self.detail,
"status": self.status,
}`
Notes:
TodoManager
- extend manager to include business logic for our todos app in this case returns a list of formatted todos.
def to_json(self)
: - added a convenience method to format our todos for the front end.
¶
Create the views and API
Next, we will create our template view for the dashboard landing page.
*
apps/dashboard/views.py
*
text
`from django.utils import timezone
from django.views.generic import TemplateView
class DashboardView(TemplateView):
template_name="dashboard/landing.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({"chart_data": self.get_chart_data()})
return context
def get_chart_data(self):
"""return chart data"""
return [
{"value": 335, "name": "Direct"},
{"value": 310, "name": "Email"},
{"value": 274, "name": "Union Ads"},
{"value": 235, "name": "Video Ads"},
{"value": 400, "name": "Search Engine"},
]
`
Notes
DashboardView
- we modified the
get_context_data
method to add additional data to the Chart. This is embedded in the page context and saves us from having to make an extra API call on the frontend Chart App to fetch this data.
Let’s add our backend API. We are using the
Django Ninja library
as a replacement for the excellent
Django rest framework
library. The library uses
Pydantic
for data validation, and has async and type hints support.
*
apps/dashboard/schema.py
*
text
`from typing import Optional
from datetime import datetime
from django.utils import timezone
from ninja import ModelSchema
from .models import Todo
class TodoSchema(ModelSchema):
created: Optional[datetime] = timezone.now()
status: Optional[str] = "outstanding"
class Config:
model = Todo
model_fields = ["id", "detail", "created", "status"]
`
Notes:
TodoSchema
- the schema serializes (converts data to python types) and deserializes data (converts data to content types such as JSON or XML). This is the equivalent of a serializer class in the Django rest framework.
*
apps/dashboard/api.py
*
text
`from ninja import Router
from .models import Todo
from .schema import TodoSchema
router = Router(tags=["todos"])
@router.get("/", operation_id="getTodos")
def list_todos(request):
"""List todos"""
todos = Todo.objects.to_list()
return todos
@router.get("/{todo_id}", operation_id="getTodo")
def todo_details(request, todo_id: int):
"""Retrive todo"""
todo = Todo.objects.get(id=todo_id)
return todo.to_json()
@router.delete("/{todo_id}", operation_id="deleteTodo")
def delete_todo(request, todo_id: int):
"""Delete todo"""
obj = Todo.objects.filter(id=todo_id).first()
if obj is not None:
obj.delete()
return {"message": f"deleted to with id {todo_id}"}
return router.create_response(
request,
{"message": "Delete failed obj not found"},
status=404,
)
@router.post("/", operation_id="addTodo")
def post_todo(request, payload: TodoSchema):
"""Add todo"""
obj = Todo.objects.create(detail=payload.detail, status=payload.status)
obj.save()
return obj.to_json()
`
Notes:
router
- we created the todos router and added the tag ‘todos’. This will ensure the correct tag is included in our api docs view. We will refer back to this file when we wire up the routers and urls below.
added all the endpoints required to perform basic crud operations. You will notice that on the
`
post_todo
`
endpoint the payload should adhere TodoSchema data type. If you need more guidance on this, check out the
[
Django ninja docs
](https://django-ninja.rest-framework.com/tutorial/)
.
¶
Wire up the routers and URLs
In this section, we will wire up the
DashboardView
and
api
endpoints to our URL configuration.
*
apps/dashboard/urls.py
*
text
`from django.urls import path
from . import views
app_name = "dashboard"
urlpatterns = [
path(
"dashboard/",
views.DashboardView.as_view(),
name="landing",
),
]
`
*
apps/config/api.py
*
text
`from ninja import NinjaAPI
from apps.dashboard.api import router as todos_router
api = NinjaAPI()
api.add_router("/todos/", todos_router)`
*
apps/config/urls.py
*
text
`from django.contrib import admin
from django.urls import path, include
from django.views.generic import TemplateView
from django.views.decorators.cache import cache_page
from .api import api
urlpatterns = [
path("admin/", admin.site.urls),
path("", cache_page(60 * 15)(TemplateView.as_view(template_name="home.html")), name="home"),
path("", include("apps.dashboard.urls")),
path("api/", api.urls),
]`
Notes:
apps/dashboard/urls.py
- we added the
DashboardView
to the dashboard URL configuration file.
apps/config/api.py
- imported the todos router we created earlier and added it to our API.
apps/config/urls.py
- updated the main urls.py to include the dashboard app URL config and API endpoints.
The landing page and API docs should now be live. This UI is automatically generated by Swagger.
¶
Create the dashboard templates and alpine component
Next, we will create the templates for the dashboard landing page, as well as the dashboard javascript component.
Summary of the workflow
creates the dashboard layout template
add the landing page, and include AlpineJS attributes directly in the markup but do not inline javascript directly on the page.
create the templates for the Chart app and Todo app
¶
Creating the dashboard layout and landing page templates
*
templates/layout.html
*
text
`{% extends 'base.html' %}
{% block content %}
<div class="flex flex-col">
<section>
<nav class="relative">...</nav>
</section>
{% block dashboard_content %}{% endblock %}
</div>
{% endblock content %}`
Notes:
this is a normal Django template that extends
`
base.html
`
the template and includes a dashboard navbar which should appear on every dashboard page.
*
templates/landing.html
*
text
`{% extends 'dashboard/layout.html' %}{% load static %} {% block dashboard_content %}
<div x-data='dashboard()' class="flex flex-col">
<div class="w-full lg:max-w-7xl flex justify-between align-items-center mt-10 p-2 lg:px-6">
<div class="text-lg md:text-3xl font-bold breadcrumbs">
<ul>
<li>
<a @click="changeView('index')">Dashboard</a>
</li>
<li x-show="activeView.key !== 'index' ">
<a x-text="activeView.name"></a>
</li>
</ul>
</div>
<a x-show="activeView.key !== 'index'" x-cloak @click="changeView('index')" class="btn btn-primary btn-sm mt-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L7.414 9H15a1 1 0 110 2H7.414l2.293 2.293a1 1 0 010 1.414z"
clip-rule="evenodd" />
</svg>
<span>Back</span>
</a>
</div>
<div class="p-6 lg:max-w-7xl mt-6 mb-20">
<div x-show="activeView.key === 'index'" class="grid grid-cols-2 gap-10 xl:grid-cols-5">
<template x-for="(app, index) in apps" :key="app.name">
<div @click="changeView(app.key)" class="card side bg-base-100 compact shadow dark:shadow-lg hover:shadow-xl bordered pointer">
<div class="flex flex-col justify-start p-4 text-primary ">
<div class="flex flex-row mx-auto">
<img :src="app.icon" height="24" width="24"/>
<h2 class="text-lg text-base-content ml-3 font-bold" x-text="app.name"></h2>
</div>
</div>
</div>
</template>
</div>
<div x-show="activeView.key == 'chart'" x-cloak>
{% include "dashboard/components/charts.html" %}
</div>
<div x-show="activeView.key == 'todo'" x-cloak>
{% include "dashboard/components/todo.html" %}
</div>
</div>
</div>
{% endblock %}
{% block footer_js %}
{{ block.super }}
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts@5.0.2/dist/echarts.min.js"></script>
{# load any variables required by our scripts that have to be rendered using django templates #}
{{chart_data|json_script:"chartData"}} {# backend provided json data#}
<script>
const listIconUrl = "{% static "icons/list.svg" %}"
const chartIconUrl = "{% static "icons/list.svg" %}"
</script>
<script type="text/javascript" src="{% static 'js/charts.js' %}"></script>
<script type="text/javascript" src="{% static 'js/dashboard.js' %}"></script>
{% endblock %}`
Notes:
There is a lot going on here, let’s break down the important bits
x-data='dashboard()'
- Alpine directive that defines a chunk of HTML as an Alpine component. Properties defined in the dashboard Alpine component will be available to all the element children. The alpine component itself is included on the page using this line
<script type="text/javascript" src="{% static 'js/dashboard.js' %}"></script>
, we will compose it later.
@click="changeView('index')"
- @ is a shortcut for the alpine x-bind: directive. In this case, it binds the click event to the
changeView
a function defined in the dashboard component.
x-show="activeView.key !== 'index'"
- x-show provides an expressive way to hide or show DOM elements. On this page, it will hide or show the index view, chart app view, or todo app view depending on what the
activeView
variable is set to.
x-cloak
- to avoid the incorrect view showing before alpine loads, we add the x-cloak direct to each of the views. In our static/styles/site.css file, we also add the required CSS for this to work
[x-cloak] { display: none !important; }
<template x-for="(app, index) in apps" :key="app.name">
- this section is to show the correct app icon. We use the x-for directive to create DOM elements by iterating through a list. This is an example of client-side javascript rendering HTML.
{{chart_data|json_script:"chartData"}}
- in the DashboardView we included chart_data in the page context. This json_script template tag will transform the Python object as JSON wrapped in a script tag which can be consumed by javascript.
const listIconUrl = "{% static "icons/list.svg" %}"
- we also include URLs for our app icons. These will be used in the alpine dashboard component. We are defining them before we import our page js files so that they are available when Alpine loads the component.
At this stage, we have defined the templates but not the Alpine component. Therefore, only the header will be rendered if we visit http://localhost:8000/dashboard/
¶
Other required templates
You can refer to the template/dashboard/components in the codebase for the
todo.html
,
chart.html
and
todo_form.html
template files.
*
todo_form.html
*
text
` ...
<div class="form-control flex w-full mt-4 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<textarea type="text"
placeholder="add todo detail"
class="textarea textarea-primary textarea-bordered"
x-model='data.todo.form.detail'>
</textarea>
</div>
...`
Notes:
x-model
- one thing to note here is the use of the
x-model
directive, which binds the value of the textarea to Alpine data. There are no other new concepts in these templates that we have not covered above.
¶
Creating the dashboard Alpine Component
This is our client side javascript application. It will handle the routing for the Todo, Chart and dashboard landing views as well as logic required for data fetching etc.
*
static/js/dashboard.js
*
text
`// constant for the chart component
let chartComponent;
const index = {
name: "home", id: 1, key: "index",
}
// Define a constant for the dashboard views
const dashboardApps = {
todo: {
name: "Todo App", id: 2, key: "todo",
icon: listIconUrl,
description: "todo app"
},
chart: {
name: "Chart App", id: 3, key: "chart",
icon: chartIconUrl,
description: "chart app"
},
}
/**
* Alpine Dashboard Component
* init - entry point for the alpine component
*
* */
function dashboard() {
return {
apps: dashboardApps,
views: {index, ...dashboardApps},
activeView: index,
data: {
chart: {
chartData: [],
isLoading:false
},
todo: {
list:[],
isLoading:false,
form: {detail: ''},
showForm: false
}
},
hasNavigated: true,
loading: false,
appList: ["todo", "chart"],
/**
* Initialises dashboard view
* Use the url to render the correct view
*
*/
init() {
// check for key from url
let key = new URLSearchParams(location.search).get("view")
console.log(key)
if (this.appList.includes(key)) {
this.changeView(key)
}
// update 'view' url search params to show correct view
if(key===null || key !== null && !this.appList.includes(key)){
this.updateHistoryState('index', true)
}
this.setPopStateEventListener()
},
/** Handle window.popstate event
* When a user navigates to the previous page we should update the view
*/
setPopStateEventListener(){
window.onpopstate = () => setTimeout(()=> {
if(!this.checkNavigationUpdated()){
let url= new URL(window.location)
let key = url.searchParams.get('view')
this.changeView(key)
}
}, 0);
this.checkNavigationUpdated()
},
/** check navigation updated */
checkNavigationUpdated(){
let url= new URL(window.location)
return this.activeView.key === url.searchParams.get('view')
},
/** Dashboard routing */
async changeView(key) {
console.log(key)
this.loading = true
// fetch relevant data for view and navigate to view
try {
this.activeView = this.views[key]
this.updateHistoryState(key)
await this.setupView(key)
} catch (e) {
console.log(`${e} Error loading view`)
}
this.loading = false
},
/** Update browser's session history */
updateHistoryState(view, replace=false, key='view'){
try {
const url = new URL(window.location);
url.searchParams.set(key, view);
replace ? window.history.replaceState({}, '', url) : window.history.pushState({}, '', url);
}catch(e){
console.log(`${e} Error updating history state`)
}
},
/** Handle additional logic required before rendering a view, e.g. fetching data */
async setupView(key) {
// update todos
if (key === this.apps.todo.key) {
this.data.todo.isLoading = true,
// TODO add -> this.fetchTodos()
this.data.todo.isLoading = false
}
// update chart view
if (key === this.apps.chart.key) {
// instead of an api call we use the data already rendered by django
this.data.chart.chartData = JSON.parse(document.getElementById('chartData').textContent)
chartComponent = echarts.init(document.getElementById('chartArea'));
chartComponent.setOption(buildChartOptions(this.data.chart.chartData))
setTimeout(chartComponent.resize(), 100)
}
},
/** TODO */
/** fetch todos */
/** add todos */
/** delete todos etc */
}
}`
This is our basic alpine component, there are some bit and pieces missing, like logic for the todo app for example which we will extend later.
Notes:
const dashboardApps
- javascript object representing the dashboard views, includes index(landing page) and chart and todo apps. This is mapped to the apps variable in the alpine dashboard component.
init()
- this is the entry point for our alpine component called when the component is finalized. Updates the URL to show the correct view. In our case, it will replace the current URL search params to show ?view=index if the view search param is not defined.
changeView
,
updateHistoryState
- contains logic for routing and updating the browser history session.
setPopStateEventListener
- listens to popstate events, i.e. when a user clicks the back button and call
changeView
to update the UI.
async setupView(key)
.. - handles any additional logic for fetching data from the backend and setting up components.
this.data.chart.chartData = JSON.parse(document.getElementById('chartData').textContent)
- consumes data directly from the page context instead of using the API.
if (key === this.apps.chart.key) ... - logic for when a user navigates to the Chart App view.
¶
Updating the dashboard Alpine component - CRUD methods for the Todo app
We will update our alpine dashboard component (same file as above) to include CRUD methods for the Todo’s app. In practice, this can be split out into another Alpine component. We have included it in the same component to keep things simple.
*
static/js/dashboard.js
*
text
`function dashboard() {
return {
.......
/** the following can be refactored to a separate todos app */
/** fetch todos */
fetchTodos(){
client.then(client => client.apis.todos.getTodos())
.then(result => {
console.log(result)
this.data.todo.list = result.obj
})
.catch((error) => alert(error));
},
/** show todos */
toggleForm(clear=false){
this.data.todo.showForm = !this.data.todo.showForm
if(clear){this.data.todo.form = {detail: ''}}
},
/** save todo form */
saveTodoForm(){
this.addTodo(this.data.todo.form)
this.toggleForm(clear=true)
},
/** add todos */
addTodo(data){
client.then(client => client.apis.todos.addTodo({}, {requestBody: data}))
.then(result =>{
console.log(result.obj)
this.data.todo.list.push(result.obj)
})
.catch((error) => alert('error adding todo'));
},
/** add todos */
deleteTodo(id, index){
client.then(client => client.apis.todos.deleteTodo({todo_id: id}))
.then(result =>{
console.log(result.obj)
this.data.todo.list.splice(index, 1)
})
.catch((error) => console.error(error));
}
}
}`
Notes:
fetchTodos()
,
addTodo(data)
,
deleteTodo(id, index)
- CRUD methods for the Todos app
we are using the swagger-js
`
client
`
we declared in our base.html template. instead of typing out the urls we use the operationIds we defined in our api.
addTodo
- includes an empty object for the url parameters {} and the payload i.e. {requestBody: data}.
deleteTodo
- includes {todo_id: id} which represents the required todo_id url parameter. This represents the id of the Todo object we want to delete. If you refer back to dashboard/api.py you will note that we use this id to search for the todo on the database.
That’s it, the todos app should be done.
¶
Adding a function to build chart options for the Chart App
The app uses the
echarts library
to render a traffic stats in an app.
*
static/js/charts.js
*
text
`function buildChartOptions (chartData){
....
}`
Notes:
buildChartOptions (chartData)
- returns the chart options required to build the chart. This method is called by the
setupView()
function in
dashboard.js
whenever a user navigates to the Chart app view.
All done
🥳
🥳
!!
¶
Summary
*
Choosing the architecture
*
- Django gives you the flexibility to use any approach for your front-end architecture. Make the decision based on the requirements of the project and take advantage of what Django has to offer.
*
Structuring your project
*
- a well-structured project is easy to maintain and refactor. Take the time to structure your project before starting out. Whenever possible try to keep your JS and HTML separate.
*
Client-side javascript frameworks
*
- In most cases, a smaller framework like Alpine or even vanilla javascript will suffice for front-end scripting. For more complex use cases where a big client-side javascript framework is needed, it is worth considering the hybrid approach instead of a full SPA.
¶
Next Steps
In the next tutorial, we discuss how we can improve this setup and get our app ready for production.
We will learn how to use a build tool to make it easier to maintain our javascript code. We will also do a deep dive into Swagger and backend APIs.
¶
Further reading
Alpine.js docs
- Excellent introduction to Alpine
TailwindCSS
- Best CSS framework out there.
HTMX Essays
- Excellent resource for learning about a Hypermedia approach to building web apps