Implementing the strategy pattern in Django with JavaScript import maps

JavaScript import maps play nicely with Django templates.

Implementing the strategy pattern in Django with JavaScript import maps

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:

  1. a child template extends a parent template
  2. 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:

  1. core, containing the base template and the base JavaScript file
  2. authors, extending the base template from core
  3. books, extending the base template from core

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!

Suggested resources

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!

More from the blog: