Javascript hacking

When writing JavaScript code, try to focus on modules, not pages. In short: follow the module pattern.

If the code is HTML-related, it should take selectors or objects as input and concern itself solely with those. This makes for much easier testing and reuse. And of course: Write the tests first.

When the module is done you write a controller for the page that plugs the needed plugins to the page elements. This should fail gracefully if the needed elements are not present.

When this documentation uses the term module, it refers to the AMD (see API docs) principle, which follows the module pattern. NAV’s JavaScript code uses RequireJS to load modules and specify their dependencies. RequireJS provides a rationale for why using AMD is a good idea.

Avoiding caching

We highly suggest you create python/nav/web/static/js/require_config.dev.js and enable Django debug in etc/nav.conf when developing.

Make sure to put this in your RequireJS configuration file:

require.urlArgs = "bust=" +  (new Date()).getTime();

This makes sure you’re not using cached resources in your browser when developing, something many browsers love to do! See the RequireJS documentation on using urlArgs for details.

The python/nav/web/static/js/require_config.dev.js is in the global Git ignore list (file:.gitignore).

Javascript testing

We use Karma as our Javascript test runner. See python/nav/web/static/js/test/* for examples on how to write tests using Karma with Mocha/Chai.

Javascript hierarchy layout

JavaScript sources are placed under python/nav/web/static/js/ under NAV’s SCM root.

In the JavaScript root directory (python/nav/web/static/js/) there should normally only be global configuration files for RequireJS, jshint, etc.

python/nav/web/static/js
|-- extras/
|-- geomap/
|-- libs/
|-- resources/
|-- src/
`-- test/
extras/

contains special dependencies and tools that are useful for JavaScript hacking, but which aren’t necessarily implemented using JavaScript themselves. As of this writing there is only downloadify, which adds support for a save-as dialog for asynchronous download requests made from JavaScript.

geomap/

contains JavaScript files related to geomap module in NAV.

libs/

contains vendored 3rd party libraries (both AMD and non-AMD libraries) which we use in NAV. These are managed via tools/vendor.py (see Managing vendored JS libraries). Make sure you add the JavaScript as a shimmed library in python/nav/web/static/js/require_config.*.js if it is not an AMD library.

resources/

contains resources that should be available under the Karma testing environment. python/nav/web/static/js/resources/libs/text.js is such a module which is required to be available in such an environment to run tests with templates that get loaded using the AMD pattern.

src/

contains the source code to NAV modules which use RequireJS for dependency handling.

src/netmap/

is the Netmap Backbone application.

src/plugins/

contains re-usable JavaScript plugins.

CSRF Token Handling

When making AJAX requests that modify data (POST, PUT, DELETE), you must include Django’s CSRF token for security.

Getting the CSRF Token

Method 1: From a hidden form (Recommended)

const csrfToken = $('#some-form-id input[name="csrfmiddlewaretoken"]').val();

Method 2: From any form on the page

const csrfToken = $('[name=csrfmiddlewaretoken]').val();

Using CSRF Tokens in AJAX Requests

There are three ways to include a CSRF Token in the requests.

Method 1: With jQuery POST data object:

This method includes the CSRF token directly in the POST data. This is the most straightforward approach when you have simple form data.

$.post({
    url: '/some/endpoint/',
    data: {
        'field': 'value',
        'csrfmiddlewaretoken': csrfToken
    }
});

Method 2: With jQuery headers:

This method sends the CSRF token in the HTTP headers using Django’s expected header name. This is useful when posting complex data like FormData objects or JSON.

$.post({
    url: '/some/endpoint/',
    data: formData,
    headers: {
        'X-CSRFToken': csrfToken
    }
});

Method 3: With serialized form data:

This method does not require getting the token from the template explicitly, but is done as part of native HTML form processing. The CSRF token is automatically included when the form is serialized.

// If posting a complete form, the token is included automatically
$.post(url, $('#my-form').serialize());

Including CSRF Token in Templates

Django templates provide the {% csrf_token %} template tag to automatically include the CSRF token in forms. This is the recommended approach for standard form submissions.

Basic form with CSRF token:

This is the most common pattern for regular form submissions. The CSRF token is included automatically when the form is submitted normally.

<form method="post" action="{% url 'some-endpoint' %}">
    {% csrf_token %}
    <input type="text" name="field_name" value="">
    <input type="submit" value="Submit">
</form>

Hidden form for JavaScript access:

This pattern creates a hidden form solely to provide JavaScript access to the CSRF token. This is useful when you need to make AJAX requests from JavaScript but don’t have a visible form on the page.

<form id="example-form" style="display: none;">
    {% csrf_token %}
</form>

Multiple forms on the same page:

When you have multiple forms that perform different actions, each form needs its own CSRF token. This example shows two example forms for resource operations - one for renaming and one for deleting.

<form id="form-rename-resource" method="post" action="{% url 'rename-resource' resource.pk %}">
    {% csrf_token %}
    <input type="text" name="resource-name" value="{{ resource.name }}">
    <input type="submit" value="Rename resource">
</form>

<form id="form-delete-resource" method="post" action="{% url 'delete-resource' resource.pk %}">
    {% csrf_token %}
    <input type="submit" value="Delete resource">
</form>

HTMX forms with CSRF token:

When using HTMX for dynamic content updates, the CSRF token is still required for POST requests. HTMX will automatically include the token from the form when making the request.

<form method="post"
      hx-post="{% url 'some-endpoint' %}"
      hx-target="#result-container">
    {% csrf_token %}
    <input type="text" name="data">
    <button type="submit">Submit</button>
</form>

The {% csrf_token %} tag renders as a hidden input field with name csrfmiddlewaretoken that JavaScript can access to include in AJAX requests.

Managing vendored JS libraries

Third-party JavaScript libraries in libs/ are vendored as minified files and tracked as npm dependencies in package.json for version management. The tools/vendor.py script automates installing, updating and removing these files.

Listing vendored libraries

To see all vendored libraries and their versions:

python tools/vendor.py list

Syncing all libraries

After a fresh clone or when package.json has changed, install npm dependencies and sync the vendored files:

npm install --legacy-peer-deps
python tools/vendor.py sync

Adding a new library

python tools/vendor.py add d3 --version 7.9.0

This installs the package via npm, copies the minified file to libs/ as d3-7.9.0.min.js, and prints the path entry to add to require_config.js:

Added: d3-7.9.0.min.js
Add to require_config.js: "d3": "libs/d3-7.9.0.min",

Add the printed entry to the paths object in python/nav/web/static/js/require_config.js. If the library is not an AMD module, also add a shim entry.

Updating a library

python tools/vendor.py update d3 --version 7.10.0

This replaces the old minified file, updates require_config.js references automatically, and pins the new version in package.json. Omit --version to update to the latest release.

Removing a library

python tools/vendor.py remove d3

This removes the minified file from libs/, removes the matching entry from require_config.js, and uninstalls the npm package.