=================================================
How to get Django and JavaScript to work together
=================================================
There tend to be two broad problems to solve when working with Django and
JavaScript:
* getting your JavaScript to the browser
* communicating between your Django and JavaScript code
This guide shows how Django and its ecosystem can help with both. We'll build
on the polls application from the :doc:`tutorial ` in the
examples that follow.
How to add JavaScript to your pages
===================================
The basic building block is the :ttag:`static` template tag for referencing
your JavaScript files. For example, to add JavaScript to the polls app:
.. code-block:: html+django
* The file sits under ``polls/static/polls/js/`` so it's namespaced to
the polls app (see :doc:`/howto/static-files/index`). You could also add
the file to a folder in :setting:`STATICFILES_DIRS`.
* The ``defer`` attribute tells the browser to run the script after the HTML
is parsed; see `MDN
`_.
* If your site sends a ``Content-Security-Policy``, add the
:ttag:`csp_nonce_attr` template tag so the script is permitted (see
:doc:`/howto/csp`).
As your own JavaScript grows, split it across files using `native ES modules
`_.
Use ``import``/``export`` and load the entry point with ``type="module"``:
.. code-block:: html+django
.. note::
To rewrite the hashed file names referenced in ``import`` statements,
override
:class:`~django.contrib.staticfiles.storage.ManifestStaticFilesStorage`
with a subclass that sets ``support_js_module_import_aggregation`` to
``True`` or map the names yourself with an `importmap
`_.
You can load third-party JavaScript straight from a CDN, or you can manually
download and copy it to your static files.
To manage more dependencies and keep them updated, use a package manager.
`npm `_ is the main registry for JavaScript and there
are several package managers that work off a list of packages in a
``package.json`` file and download them to a folder, typically
``node_modules``. You will need to link this to static files:
* To deliver library files unchanged, let static files read from
``node_modules``; the ecosystem provides packages that help with this.
* Or, more commonly, to combine, convert, or tree-shake JavaScript (including
your own), use a JavaScript build tool. The output of the build tool should
go to a folder that is part of the static files tree.
.. _javascript-hmr:
.. note::
Many build tools also provide a development server with features like
Hot Module Reload (HMR), allowing your front-end to update as you change
your development files. To integrate this with Django, search for
``django `` to find a community package that supports your
setup.
How to communicate between Django and JavaScript
=================================================
How to update part of a page without a full reload
---------------------------------------------------
You can use JavaScript to make requests to a Django view and handle its
responses. Consider the following update to the polls tutorial.
.. code-block:: html+django
:caption: ``polls/templates/polls/results.html``
{% endpartialdef %}
.. code-block:: html+django
:caption: ``polls/templates/polls/detail.html``
{# Form as in the tutorial (with id="vote-form"), plus load static and: #}
.. code-block:: javascript
:caption: ``polls/static/polls/js/vote.js``
const form = document.getElementById("vote-form");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const response = await fetch(form.action, {
method: "POST",
headers: {
"X-CSRFToken": getCookie("csrftoken"),
"X-Requested-With": "XMLHttpRequest",
},
mode: "same-origin",
body: new FormData(form),
});
document.getElementById("results").outerHTML = await response.text();
});
The ``getCookie()`` helper is the one from
:ref:`the CSRF documentation `.
.. code-block:: python
:caption: ``polls/views.py``
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(pk=request.POST["choice"])
except (KeyError, Choice.DoesNotExist):
return render(request, "polls/detail.html", {...}) # as in the tutorial
selected_choice.votes = F("votes") + 1
selected_choice.save()
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
# fetch() submission: return the updated fragment, no redirect needed
# (no navigation occurs, so there's no back-button resubmission to guard
# against).
return render(
request, "polls/results.html#results-list", {"question": question}
)
# Plain form submission: Post/Redirect/Get, as in the tutorial.
return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
This technique uses :ref:`partials ` from the Django
templating engine to send only the body of the results page as the response.
JavaScript overrides the form submission and sends it to the same view,
but with an additional identifier the view can use to choose a response path.
This pattern is common enough that several JavaScript libraries handle the
request and partial swap logic for you. Some even let you declare the behavior
with HTML attributes, saving you from writing any JavaScript.
How to mount a JavaScript component
-----------------------------------
Another common pattern is for the JavaScript to take over rendering a section
of the page.
.. code-block:: html+django
:caption: ``polls/templates/polls/detail.html``
{{ results_data|json_script:"results-data" }}
The :tfilter:`json_script` filter safely outputs the data for the JavaScript
to read.
.. code-block:: javascript
:caption: ``polls/static/polls/js/results-widget.js``
const data = JSON.parse(document.getElementById("results-data").textContent);
const root = document.getElementById("results-widget");
function render({question, choices}) {
root.innerHTML = `
${question}
` +
choices.map((c) =>
`
-- ${c.votes}
`
).join("") +
`
`;
}
async function vote(choiceId) {
const response = await fetch(data.voteUrl, {
method: "POST",
headers: {
"X-CSRFToken": getCookie("csrftoken"),
"Content-Type": "application/json",
},
mode: "same-origin",
body: JSON.stringify({choice: choiceId}),
});
render(await response.json());
}
// Delegate on root, which survives each render() replacing its contents.
root.addEventListener("click", (event) => {
const button = event.target.closest("button[data-choice]");
if (button) {
vote(button.dataset.choice);
}
});
render(data);
The details page continues to be served by a Django view. The view is reworked
to pass the ``results_data`` object into the context for the JavaScript widget
to render instead of the question. The vote view is changed to a JSON endpoint
using :class:`~django.http.JsonResponse`, which updates the vote and returns
the new state.
.. code-block:: python
:caption: ``polls/views.py``
import json
from django.http import JsonResponse
from django.urls import reverse
from django.views.decorators.http import require_POST
def results_data(question):
return {
"question": question.question_text,
"voteUrl": reverse("polls:vote_json", args=(question.id,)),
"choices": [
{"id": c.id, "text": c.choice_text, "votes": c.votes}
for c in question.choice_set.all()
],
}
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(
request, "polls/detail.html", {"results_data": results_data(question)}
)
@require_POST
def vote_json(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
choice_id = json.loads(request.body)["choice"]
selected_choice = question.choice_set.get(pk=choice_id)
except (json.JSONDecodeError, KeyError, Choice.DoesNotExist):
return JsonResponse({"error": "Choice does not exist."}, status=400)
selected_choice.votes = F("votes") + 1
selected_choice.save()
return JsonResponse(results_data(question))
.. code-block:: python
:caption: ``polls/urls.py``
# Added to the tutorial's urlpatterns.
path("api//vote/", views.vote_json, name="vote_json"),
As the data you are passing gets more detailed, you can look to the `ecosystem
for a third-party library to help build an API for your models
`_.
How to serve a JavaScript front-end against a Django API
--------------------------------------------------------
If the front-end is rendered entirely in JavaScript (a single-page app), with
Django serving the API, you can still serve the entry view via Django. Add a
catch-all URL pattern with :func:`~django.urls.re_path` pointing at a
:class:`~django.views.generic.base.TemplateView` for the client-side routing,
and place it last so it doesn't capture your API or admin:
.. code-block:: python
:caption: ``urls.py``
re_path(r"^.*$", TemplateView.as_view(template_name="index.html")),
As before, place the JavaScript build tool's output somewhere staticfiles can
collect, and use the ecosystem to :ref:`enable HMR ` in
development.
Alternatively, you can use the serving infrastructure of your SPA framework,
in which case you will get HMR for free. You'll have to adjust how you pass
the ``csrftoken`` though:
* Set it on the first view the app calls with
:func:`~django.views.decorators.csrf.ensure_csrf_cookie`.
* Read the token from the cookie on each request since it rotates on login.
The bigger issue then is how to deal with two origins, ``localhost:8000`` and
``localhost:9000``, in development. By default, browsers won't let JavaScript
read from a different origin and CSRF won't work out of the box across two
origins either. There are two possible solutions:
* proxy requests so they come from one origin (your JavaScript framework's
serving infrastructure can typically proxy API requests to your Django
development server)
* configure your system to handle two different origins
It's a good idea to pick the one closest to your production setup. In
production you can still serve everything from the same origin, either by
using Django as above or configuring one reverse proxy for both Django and
JavaScript.
If you do need to serve the front-end from a different origin, then you
will need to configure two things: CORS and CSRF.
For `CORS `_,
set the appropriate headers on your Django responses. Typically you'd do this
with a middleware and there are `packages in the ecosystem that help with this
`_.
.. code-block:: text
Access-Control-Allow-Origin: http://localhost:9000
Access-Control-Allow-Credentials: true
Then configure :doc:`CSRF ` for the two origins:
* Configure :setting:`CSRF_TRUSTED_ORIGINS` ``= ["http://localhost:9000"]``.
* In a production setup, with ``app.example.com`` and ``api.example.com``,
add :setting:`CSRF_COOKIE_DOMAIN` ``= ".example.com"`` too.
* Each JavaScript ``fetch`` must send ``credentials: "include"``.
The above configuration also suffices for
:doc:`authentication ` across the two origins. The session
cookies require those CORS headers and the fetch credentials.
.. warning::
If you are using different domains, not just different subdomains, this all
becomes increasingly unworkable. The browser treats the cookies as
third-party, and the security restrictions placed on them make it difficult
to keep using Django's session-based auth. The alternative is token auth,
which you may already be using for any mobile app that consumes the API.
Your API package will support it, but token auth shifts more security
responsibility onto you than the default session cookies do, so read its
documentation closely.