Managing Dependencies

HQ’s JavaScript is being gradually migrated from a legacy, unstructured coding style the relies on the ordering of script tags to instead use RequireJS for dependency management. This means that dependencies are managed differently depending on which area of the code you’re working in. This page is a developer’s guide to understanding which area you’re working in and what that means for your code.

How do I know whether or not I’m working with RequireJS?

You are likely working with RequireJS, as most of HQ has been migrated. However, several major areas have not been migrated: app manager, reports, and web apps. Test code also does not currently use RequireJS; see Testing for working with tests.

To tell for sure, look at your module’s hqDefine call, at the top of the file.

RequireJS modules look like this, with all dependencies loaded as part of hqDefine:

hqDefine("my_app/js/my_file", [
    "knockout",
    "hqwebapp/js/initial_page_data"
], function (
    ko,
    initialPageData
) {
    var myObservable = ko.observable(initialPageData.get("thing"));
    ...
});

Non-RequireJS modules look like this, with no list and no function parameters. HQ modules are loaded using hqImport in the body, and third party libraries aren’t declared at all, instead relying on globals:

hqDefine("my_app/js/my_file", function () {
    var myObservable = ko.observable(hqImport("hqwebapp/js/initial_page_data").get("thing"));
    ...
});

How do I write a new page?

New code should be written in RequireJS, which is oriented around a single “entry point” into the page.

Most pages have some amount of logic only relevant to that page, so they have a file that includes that logic and then depends on other modules for shared logic.

data_dictionary.js fits this common pattern:

hqDefine("data_dictionary/js/data_dictionary", [    // Module name must match filename
    "jquery",                                       // Common third-party dependencies
    "knockout",
    "underscore",
    "hqwebapp/js/initial_page_data",                // Dependencies on HQ files always match the file's path
    "hqwebapp/js/main",
    "analytix/js/google",
    "hqwebapp/js/knockout_bindings.ko",             // This one doesn't need a named parameter because it only adds
                                                    // knockout bindings and is not referenced in this file
], function (
    $,                                              // These common dependencies use these names for compatibility
    ko,                                             // with non-requirejs pages, which rely on globals
    _,
    initialPageData,                                // Any dependency that will be referenced in this file needs a name.
    hqMain,
    googleAnalytics
) {
    /* Function definitions, knockout model definitions, etc. */

    var dataUrl = initialPageData.reverse('data_dictionary_json');  // Refer to dependencies by their named parameter
    ...

    $(function () {
        /* Logic to run on documentready */
    });

    // Other code isn't going to depend on this module, so it doesn't return anything or returns 1
});

To register your module as the RequireJS entry point, add the requirejs_main template tag to your HTML page, near the top but outside of any other block:

{% requirejs_main 'data_dictionary/js/data_dictionary' %}

Some pages don’t have any unique logic but do rely on other modules. These are usually pages that use some common widgets but don’t have custom UI interactions.

If your page only relies on a single js module, you can use that as the module’s entry point:

{% requirejs_main 'locations/js/widgets' %}

If your page relies on multiple modules, it still needs one entry point. You can handle this by making a module that has no body, just a set of dependencies, like in gateway_list.js:

hqDefine("sms/js/gateway_list", [
    "hqwebapp/js/crud_paginated_list_init",
    "hqwebapp/js/bootstrap3/widgets",
], function () {
    // No page-specific logic, just need to collect the dependencies above
});

Then in your HTML page:

{% requirejs_main 'sms/js/gateway_list' %}

The exception to the above is if your page inherits from a page that doesn’t use RequireJS. This is rare, but one example would be adding a new page to app manager that inherits from managed_app.html.

How do I add a new dependency to an existing page?

RequireJS

Add the new module to your module’s hqDefine list of dependencies. If the new dependency will be directly referenced in the body of the module, also add a parameter to the hqDefine callback:

hqDefine("my_app/js/my_module", [
    ...
    "hqwebapp/js/my_new_dependency",
], function (
    ...,
    myDependency
) {
    ...
    myDependency.myFunction();
});

Non-RequireJS

In your HTML template, add a script tag to your new dependency. Your template likely already has scripts included in a js block:

{% block js %}{{ block.super }}
  ...
  <script src="{% static 'hqwebapp/js/my_new_dependency.js' %}"></script>
{% endblock js %}

In your JavaScript file, use hqImport to get access to your new dependency:

hqDefine("my_app/js/my_module", function () {
    ...
    var myDependency = hqImport("hqwebapp/js/my_new_dependency");
    myDependency.myFunction();
});

Do not add the RequireJS-style dependency list and parameters. It’s easy to introduce bugs that won’t be visible until the module is actually migrated, and migrations are harder when they have pre-existing bugs. See the troubleshooting section of the RequireJS Migration Guide if you’re curious about the kinds of issues that crop up.

How close are we to a world where we’ll just have one set of conventions?

As above, most code is migrated, but most of the remaining areas have significant complexity.

hqDefine.sh generates metrics for the current status of the migration and locates umigrated files. At the time of writing:

$ ./scripts/codechecks/hqDefine.sh

98%     (825/843) of HTML files are free of inline scripts
88%     (375/427) of JS files use hqDefine
59%     (249/427) of JS files specify their dependencies
91%     (765/843) of HTML files are free of script tags