Implementing the strategy pattern in Django with JavaScript import maps
JavaScript import maps play nicely with Django templates.
The strategy pattern is a software design pattern which lets you swap an algorithm at runtime. From wikipedia:
the strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use
Lately I've been experimenting with a new web feature, import maps, and it turns out, they seem to play nicely in Django to implement the strategy pattern for JavaScript code in the frontend.
With simple examples, we're going to introduce the motivation behind this technique, and the actual implementation.
The need for the strategy pattern
Let's imagine a generic piece of JavaScript code in our Django project which needs to behave differently depending on the context where it's executed.
Let's imagine that if I'm on a page with books/
in the pathname, I want to write "Hello import maps from the books app!", whereas when I'm in the authors/
path I want to write "Hello import maps from the authors app!".
This can be achieved in a number of ways.
The suboptimal way is to have our code take decisions with a series of if
statements. Consider the following example:
function renderText() {
if (window.location.pathname === 'books/'){
document.querySelector('p').innerText = "Hello import maps from the books app!";
}
if (window.location.pathname === 'authors/'){
// do something else
}
}
Without going too far, you can easily guess what this code can become. Is there a better solution?
What if instead we can import one or more functions (or variables) from a "swappable" JavaScript import? Consider the following code:
import { renderText } from "strategy";
renderText();
Here we import renderText
from the strategy
JavaScript module.
The calling code does not know where strategy
is coming from. It has only to execute.
It's worth mentioning that here we are talking about JavaScript ES modules, denoted by the syntax import ... from "module-name"
.
It's also important to understand that ES modules are static, that is, they cannot be changed at runtime, and that up until recently, bare imports in the browser where not supported.
To make things clearer, up until recently, the syntax import { renderText } from "strategy"
wasn't valid in the browser, being "strategy"
a bare import. This syntax was supported only by module bundlers like webpack.
However, recently, with the introduction of JavaScript import maps, born to allow control over what URL gets fetched by JavaScript import
statements, we have a way to use bare imports in the browser, and also to potentially swap the import URL at runtime as we will see in a moment.
Pair that with the flexiblity of the Django templating system, and we can now have a neat way to implement a simple version of the strategy pattern in our JavaScript code.
Let's see this technique in practice!
Implementing the strategy pattern with import maps
Implementing the strategy pattern in Django with JavaScript import maps exploits the ability of Django templates to extend and overwrite a given template block.
In brief, here's how it works:
- a child template extends a parent template
- the child template overrides the js block to declare its import map
Let's see it in practice. For our example we have a Django project composed of three apps:
core
, containing the base template and the base JavaScript fileauthors
, extending the base template fromcore
books
, extending the base template fromcore
The core app
The folder structure of the core
app:
core
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│ ├── __init__.py
├── models.py
├── static
│ └── core
│ └── js
│ └── Base.js
├── templates
│ └── core
│ └── base.html
├── tests.py
└── views.py
The base Django template core/templates/core/base.html
declares Django template blocks that inheritors can override, namely content
and js
. In addition, it loads a JavaScript module:
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>The base template</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
{% block js %}
<script type="module" src="{% static "core/js/Base.js" %}"></script>
{% endblock %}
</html>
In detail:
<script type="module" src="{% static "core/js/Base.js" %}"></script>
This is the base JavaScript file, a piece of logic that will behave differently depending on the page where it's called.
The base JavaScript file core/static/core/js/Base.js
imports and executes any function (or constant) it needs from a strategy
, without even knowing where this file is. Here's the file content:
import { renderText } from "strategy";
renderText();
Let's now see the two other Django apps.
NOTE: the module import name can be anything you want, not necessarily "strategy"
, as long as you reference the same name in child templates as well.
The authors app
The authors
app is one of the apps using the base template from core
.
The folder structure of the authors
app:
authors
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│ ├── __init__.py
├── models.py
├── static
│ └── authors
│ └── js
│ └── Authors.js
├── templates
│ └── authors
│ └── list.html
├── tests.py
├── urls.py
└── views.py
The inheritor template in authors/templates/authors/list.html
extends core/templates/core/base.html
, and then overrides the Django js
block to declare its own import via import maps:
{% extends "core/base.html" %}
{% load static %}
{% block content %}
<p></p>
{% endblock %}
{% block js %}
<script type="importmap">
{
"imports": {
"strategy": "{% static "authors/js/Authors.js" %}"
}
}
</script>
{{ block.super }}
{% endblock %}
Now in authors/static/authors/js/Authors.js
we can export our strategy function:
function renderText() {
document.querySelector('p').innerText = "Hello import maps from the authors app!";
}
export { renderText };
The bit of HTML inside the js
block is the key:
<script type="importmap">
{
"imports": {
"strategy": "{% static "authors/js/Authors.js" %}"
}
}
</script>
What happens now is that by loading this template via a Django view, the browser is smart enough to map the strategy
import to the actual JavaScript file authors/static/authors/js/Authors.js
so that our code can import renderText
properly.
The books app
The books
app is one of the apps using the base template from core
.
The folder structure of the books
app:
books
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│ ├── __init__.py
├── models.py
├── static
│ └── books
│ └── js
│ └── Books.js
├── templates
│ └── books
│ └── list.html
├── tests.py
├── urls.py
└── views.py
Using the same mechanism, the inheritor template in books/templates/books/list.html
extends core/templates/core/base.html
, and then overrides the Django js
block to declare its own import via import maps:
{% extends "core/base.html" %}
{% load static %}
{% block content %}
<p></p>
{% endblock %}
{% block js %}
<script type="importmap">
{
"imports": {
"strategy": "{% static "books/js/Books.js" %}"
}
}
</script>
{{ block.super }}
{% endblock %}
In the file books/static/books/js/Books.js
we can export our strategy function:
function renderText() {
document.querySelector('p').innerText = "Hello import maps from the books app!";
}
export { renderText };
Again, let's take a look at the import map inside the js
block:
<script type="importmap">
{
"imports": {
"strategy": "{% static "books/js/Books.js" %}"
}
}
</script>
Again, the browser here will map strategy
to the actual JavaScript file books/static/books/js/Books.js
, so that our code can import renderText
properly from this file.
This simple technique is an implementation of the strategy pattern: the theory here is that we can dynamically point our JavaScript import to a different location, depending on the Django template.
Conclusion
By combining Django templates, JavaScript import maps, and a bit of creativity, we can implement the strategy pattern for our JavaScript frontend code.
This technique can lead to high code reusability, and most important, modularity, two important traits of any healthy codebase.
import maps are available in all modern browsers, so you can start using them today!
Thanks for reading!