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 inpython/nav/web/static/js/require_config.*.jsif 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.jsis 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.