Client-side support

es6 module loader

Since v2.0, the monolithic app.js which used global App container, was refactored into es6 modules, which makes the client-side development more flexible. The modules themselves still use es5 syntax, with the exception of es6 imports / exports. To run the code in outdated browser which does not support es6 modules (eg IE11), django_deno bundling app should be used. It also has optional terser support. There is sample django_deno config (see djk-sample settings.py for full working example):

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 'sites' is required by allauth
    'django.contrib.sites',
    # django_deno is an optional Javascript module bundler, not required to run in modern browsers
    'django_deno',
] + DJANGO_JINJA_APPS + [
    'djk_ui',
    'django_jinja_knockout',
    'django_jinja_knockout._allauth',
] + DJK_APPS + [
    'allauth',
    'allauth.account',
    # Required for socialaccount template tag library despite we do not use social login
    'allauth.socialaccount',
]

DENO_ENABLE = True
DENO_DEBUG = False
DENO_USE_VENDOR = True
DENO_RELOAD = False
DENO_CHECK_LOCK_FILE = True

DENO_ROLLUP_ENTRY_POINTS = [
    'sample/js/app.js',
    'sample/js/club-grid.js',
    'sample/js/icon-test.js',
    'sample/js/member-grid.js',
    'sample/js/tooltips-grid.js',
]

DENO_ROLLUP_BUNDLES = {
    'djk': {
        'writeEntryPoint': 'sample/js/app.js',
        'matches': [
            'djk/js/*',
            'djk/js/lib/*',
            'djk/js/grid/*',
        ],
        'excludes': [],
        'virtualEntryPoints': 'matches',
        'virtualEntryPointsExcludes': 'excludes',
    },
}

# Do not forget to re-run collectrollup management command after changing rollup.js bundles module type:
DENO_OUTPUT_MODULE_TYPE = 'module' if DEBUG else 'systemjs-module'
DJK_JS_MODULE_TYPE = DENO_OUTPUT_MODULE_TYPE

# Run $VIRTUAL_ENV/djk-sample/cherry_django.py to check the validity of collectrollup command output bundle.
DENO_ROLLUP_COLLECT_OPTIONS = {
    'terser': True,
}

Old browsers such as IE11 will use bundled system.js loader. Note that modern browsers do not require any bundling at all, however could benefit from optional generating terser-optimized es6 bundles.

Client-side entry points

See Injection of custom script urls into loaded page how to add custom Javascript entry points.

Note that DENO_ROLLUP_ENTRY_POINTS setting is optional and is used only when django_deno is installed and enabled in settings.py to generate the minified bundle and / or to generate IE11 compatible bundle.

Client-side modules include the following features:

Client-side initialization

There are two different hooks / methods of client-side initialization:

  • documentReadyHooks - the list of function handlers which are called via $(document).ready() event handler, so these do not interfere with the third party scripts code.
  • initClientHooks - the ordered list of function handlers applied to content generated by the viewmodels / Underscore.js / Knockout.js templates to provide the dynamic styles / event handlers / client-side components. It’s processed via calling initClient function. OrderedHooks class instance is used to add hooks in proper order, where the component initialization hook should always be executed at the last step.

Read more about viewmodels here: Client-side viewmodels and AJAX response routing.

It supports mandatory ‘init’ and optional ‘dispose’ types of handlers for the DOM subtrees, where ‘dispose’ handlers are called in the reverse order. It’s also possible to define custom types of handlers.

To add new client-side initialization handlers of the ‘init’ / ‘dispose’ types:

import { initClientHooks } from '../../djk/js/initclient.js';

initClientHooks.add({
    init: function($selector) {
        $selector.myPlugin('init');
    },
    dispose: function($selector) {
        $selector.myPlugin('dispose');
    }
});

To add only the ‘init’ type of handler (when disposal is not needed):

import { initClientHooks } from '../../djk/js/initclient.js';

initClientHooks.add(function($selector) {
    $selector.myPlugin('init');
});

To call all the chain of ‘init’ handlers:

import { initClient } from '../../djk/js/initclient.js';

initClient($selector);

To call all the chain of ‘dispose’ handlers:

import { initClient } from '../../djk/js/initclient.js';

initClient($selector, 'dispose');

Note that the handlers usually are called automatically, except for grid rows where one has to use grid .useInitClient option to enable .initClient() call for grid rows DOM. See Datatables for more info.

Custom 'formset:added' jQuery event automatically supports client initialization, eg form field classes / form field event handlers when the new form is added to inline formset dynamically.

Viewmodels (client-side response routing)

See Client-side viewmodels and AJAX response routing for the detailed explanation.

  • Separates AJAX calls from their callback processing, allowing to specify AJAX routes in button html5 data attributes not having to define implicit DOM event handler and implicit callback.
  • Allows to write more modular Javascript code.
  • Client-side view models can also be executed in Javascript directly.
  • Possibility to optionally inject client-side viewmodels into html pages, executing these on load.
  • Possibility to execute client-side viewmodels from current user session (persistent onload).
  • vmRouter - predefined built-in AJAX response viewmodels router to perform standard client-side actions, such as displaying BootstrapDialogs, manipulate DOM content with graceful AJAX errors handling. It can be used to define new viewmodel handlers.

Simplifying AJAX calls

  • Url - mapping of Django server-side route urls to client-side Javascript.

  • AjaxButton - automation of button click event AJAX POST handling for Django.

  • AjaxForm - Django form AJAX POST submission with validation errors display via response client-side viewmodels.

    Requires is_ajax=True argument of bs_form() / bs_inline_formsets() Jinja2 macros.

    The whole process of server-side to client-side validation errors mapping is performed by the built-in FormWithInlineFormsetsMixin class .form_valid() / form_invalid() methods.

    Supports multiple Django POST routes for the same AJAX form via multiple input[type="submit"] buttons in the generated form html body.

  • AppGet / AppPost automate execution of AJAX POST handling for Django using named urls like url(name='my_url_name') exported to client-side code directly.

Global IoC

Since v2.0, monolithic App.readyInstances was replaced by globalIoc instance of ViewModelRouter class, which holds lazy definitions of global instances initialized when browser document is loaded. It allows to override built-in global instances and to add custom global instances in user scripts (usually in the Client-side entry points) like this:

import { globalIoc } from '../../djk/js/ioc.js';

// Late initialization allows to patch / replace classes in user scripts.
globalIoc.add('UserClass', function(options) {
    return new UserClass(options);
});

// To check whether the class name was already registered:
globalIoc.hasView('UserClass');

// To add custom class just once:
globalIoc.once('UserClass', function(options) {
    return new UserClass(options);
});

Component IoC

Base example:

import { componentIoc } from '../../djk/js/ioc.js';

function UserComponentClass(options) {
    // ... skipped ...
};

componentIoc.add('UserComponentClass', function(options) {
    return new UserComponentClass(options);
});

Client-side localization

It’s possible to format Javascript translated messages with Trans function:

import { Trans } from '../../djk/js/translate.js';

Trans('Yes')
Trans('No')
Trans('Close')
Trans('Delete "%s"', formModelName)
// named arguments
Trans(
    'Too big file size=%(size)s, max_size=%(maxsize)s',
    {'size': file.size, 'maxsize': maxSize}
)
// with html escape
Trans('Undefined viewModel.view %s', $.htmlEncode(viewModelStr))

Automatic translation of html text nodes with localize-text class is performed with localize by Client-side initialization

<div class="localize-text">Hello, world in your language!</div>

Underscore.js templates

Underscore.js templates can be autoloaded as Dialog modal body content. Also they are used in conjunction with Knockout.js templates to generate components, for example AJAX grids (Django datatables).

Template processor is implemented as Tpl class. It’s possible to extend or to replace template processor class by calling globalIoc factory method:

import { propGet } from '../../djk/js/prop.js';
import { inherit } from '../../djk/js/dash.js';
import { Tpl } from '../../djk/js/tpl.js';
import { globalIoc } from '../../djk/js/ioc.js';

globalIoc.removeAll('Tpl').add('Tpl', function(options) {
    var _options = $.extend({}, options);
    if (propGet(_options, 'meta_is_ie')) {
        return new IeTpl(_options);
    } else {
        return new Tpl(_options);
    }
});

IeTpl = function(options) {
    inherit(Tpl.prototype, this);
    return this.init(options);
};

Such custom template processor class could override one of the (sub)templates loading methods such as .expandTemplate() or .compileTemplate().

In the underscore.js template execution context, the instance of Tpl class is available as self variable. Thus calling Tpl class .get('varname') method is performed as self.get('varname'). See ko_grid_body() templates for the example of self.get method usage.

Internally template processor is used for optional client-side overriding of default grid templates, supported via Tpl constructor options.templates argument.

  • compileTemplate provides singleton factory for compiled underscore.js templates from <script> tag with specified DOM id tplId.
  • Tpl.domTemplate converts single template with specified DOM id and template arguments into jQuery DOM subtree.
  • Tpl.loadTemplates recursively loads existing underscore.js templates by their DOM id into DOM nodes with html5 data-template-id attributes for specified $selector.
  • bindTemplates - templates class factory used by initClient auto-initialization of DOM nodes.

The following html5 data attributes are used by Tpl template processor:

  • data-template-id - destination DOM node which will be replaced by expanded underscore.js template with specified template id. Attribute can be applied recursively.

  • data-template-class - optional override of default Tpl template processor class. Allows to process different underscore.js templates with different template processor classes.

  • data-template-args - optional values of current template processor instance .extendData() method argument. This value will be appended to .data property of template processor instance. The values stored in .data property are used to control template execution flow via self.get() method calls in template source code.

  • data-template-args-nesting - optionally disables appending of .data property of the parent template processor instance to .data property of current nested child template processor instance.

  • data-template-options - optional value of template processor class constructor options argument, which may have the following keys:

    • .data - used by Tpl class .get() method to control template execution flow.
    • .templates - key map of template ids to optionally substitute some or all of template names.

Template attributes merging

The DOM attributes of the template holder tag different from data-template-* are copied to the root DOM node of the expanded template. This allows to get the rid of template wrapper when using the templates as the foundation of components. For example datatables / grid templates do not use separate wrapper tag anymore and thus become simpler.

Custom tags

  • Since v2.1.0 built-in custom tags are replaced by Custom elements, disabled by default and may be removed in the future.

The built-in template processor supports custom tags via TransformTags Javascript class applyTags() method. By default there are the CARD-* tags registered, which are transformed to Bootstrap 4 / 5 cards or to Bootstrap 3 panels, depending on the djk_ui version.

Custom tags are also applied via initClient to the loaded DOM page and to dynamically loaded AJAX DOM fragments. However because the custom tags are not browser-native, such usage of custom tags is not recommended as extra flicker may occur. Such flicker never occurs in built-in Underscore.js templates, because the template tags are substituted before they are attached to the page DOM.

It’s possible to add new custom tags via supplying the capitalized tagName argument and function processing argument fn to TransformTags class add() method.

To enable built-in custom tags, set AppConf('compatTransformTags') value to True via custom page context.

See DJK_PAGE_CONTEXT_CLS for more detailed explanation how to set custom client conf values.

Custom elements

Since v2.1.0, built-in Elements class allows to create custom elements in es5 syntax:

import { elements } from './elements.js';

elements.newCustomElements(
    {
        ancestor: HTMLDivElement,
        name: 'form-group',
        extendsTagName: 'div',
        classes: ['form-group'],
    },
    {
        ancestor: HTMLLabelElement,
        name: 'form-label',
        extendsTagName: 'label',
        classes: ['control-label'],
    }
)

See Elements.builtInProperties list for the description of awailable elements options.

Custom elements also can be used to simplify creation of Components. For example in document.js:

import { elements } from './elements.js';

elements.newCustomElements(
    {
        name: 'list-range-filter',
        defaultAttrs: {
            'data-component-class': 'ListRangeFilter',
        }
    },
    {
        name: 'ko-grid',
        defaultAttrs: {
            'data-component-class': 'Grid',
            'data-template-id': 'ko_grid_body',
        }
    },
);

Note that <list-range-filter> component does not use the default template, while <ko-grid> component does use it.

Components

Components class allows to automatically instantiate Javascript classes by their componentIoc string path specified in element’s data-component-class html5 attribute and bind these to that element. It is used to provide Knockout.js Grid component auto-loading / auto-binding, but is not limited to.

Components can be also instantiated via target element event instead of document ‘ready’ event. To enable that, define data-event html5 attribute on target element. For example, to bind component classes to button ‘click’ / ‘hover’:

<button class="component"
    data-event="click"
    data-component-class="GridDialog"
    data-component-options='{"filterOptions": {"pageRoute": "club_member_grid"}}'>
    Click to see project list
</button>

When target button is clicked, GridDialog class registered by componentIoc will be instantiated with data-component-options value passed as it’s constructor argument.

JSON string value of data-component-options attribute can be nested object with many parameter values, so for convenience it can be generated in Jinja2 macro, such as ko_grid() See the example of overriding two default templates in cbv_grid_breadcrumbs.htm:

{{
ko_grid(
    grid_options={
        'pageRoute': view.request.resolver_match.view_name,
        'pageRouteKwargs': view.kwargs,
    },
    dom_attrs={
        'data-template-options': {
            'templates': {
                'ko_grid_filter_choices': 'ko_grid_breadcrumb_filter_choices',
                'ko_grid_filter_popup': 'ko_grid_breadcrumb_filter_popup',
            }
        },
    }
)
}}

By default, current component instance is re-used when the same event is fired multiple times. To have component re-instantiated, one should save target element in component instance like this:

MyComponent.runComponent = function(elem) {
    this.componentElement = elem;
    // Run your initialization code here ...
    this.doStuff();
};

Then in your component shutdown code call components instance .unbind() method, then .add() method:

import { components } from '../../djk/components.js';

MyComponent.onHide = function() {
    // Run your shutdown code ...
    this.doShutdown();
    // Detect component, so it will work without component instantiation too.
    if (this.componentElement !== null) {
        // Unbind component.
        var desc = components.unbind(this.componentElement);
        if (typeof desc.event !== 'undefined') {
            // Re-bind component to the same element with the same event.
            components.add(this.componentElement, desc.event);
        }
    }
};

There is built-in $.component plugin, which allows to get the Javascript component instance bound to particular DOM element. It returns either an component object, null when there is no bound component, or an instance of Promise to resolve the lazy loaded component, see $.component promise.

See Component IoC how to register custom Javascript component class.

See GridDialog code for the example of built-in component, which allows to fire AJAX datatables via click events.

Because GridDialog class constructor may have many options, including dynamically-generated ones, it’s preferable to generate data-component-options JSON string value in Python / Jinja2 code (see String formatting).

Search for data-component-class in djk-sample code for the examples of both document ready and button click component binding.

Components use ComponentManager class which provides the support for nested components and for sparse components.

Nested components

It’s possible to nest component DOM nodes recursively unlimited times:

<div class="component" data-component-class="Grid">
    <input type="button" value="Grid button" data-bind="click: onClick()">
    <div class="component" data-component-class="MyComponent">
        <input type="button" value="My component button" data-bind="click: onClick()">
    </div>
</div>

The Knockout.js click bindings of the Grid button will be directed to Grid class instance onClick() method and from the My component button to MyComponent class instance onClick() method.

Note that to achieve nested binding, DOM subtrees of nested components are detached until the outer components are run. Thus, in case the outer component is run on some event, for example data-event="click", nested component nodes will be hidden until outer component is run via the click event. Thus it’s advised to think carefully when using nested components running on events, while the document ready nested components have no such possible limitation.

The limitation is not so big, however because most of the components have dynamic content populated only when they run.

See the demo project example of nested datatable grid component: member_grid_tabs.htm.

Sparse components

In some cases the advanced layout of the page requires one component to be bound to the multiple separate DOM subtrees of the page. In such case sparse components may be used. To specify sparse component, add data-component-selector HTML attribute to it with the jQuery selector that should select sparse DOM nodes bound to that component.

Let’s define the datatable grid:

{{
    ko_grid(
        grid_options={
            'classPath': 'ClubEditGrid',
            'pageRoute': 'club_edit_grid',
            'pageRouteKwargs': {'club_id': view.kwargs['club_id']},
        },
        dom_attrs={
            'id': 'club_edit_grid',
            'class': 'club-edit-grid',
            'data-component-selector': '.club-edit-grid',
        }
    )
}}

Let’s define separate row list and the action button to add new row for this grid located in arbitrary location of the page:

<div class="club-edit-grid">
    <div data-bind="visible:gridRows().length > 0" style="display: none;">
        <h3>Grid rows:</h3>
        <ul class="auto-highlight" data-bind="foreach: {data: $('#club_edit_grid').component().gridRows, as: 'row'}">
            <li>
                <a data-bind="text: row.displayValues.name, attr: {href: getUrl('member_detail', {member_id: row.values.member_id})}"></a>
            </li>
        </ul>
    </div>
</div>
<div>This div is the separate content that is not bound to the component.</div>
<div class="club-edit-grid">
    <button class="btn-choice btn-info club-edit-grid" data-bind="click: function() { this.performAction('create_inline'); }">
        <span class="iconui iconui-plus"></span> Add row
    </button>
</div>

When the document DOM will be ready, ClubEditGrid class will be bound to three DOM subtrees, one is generated via ko_grid() Jinja2 macro and two located inside separate <div class="club-edit-grid"> wrappers.

Sparse components may also include inner non-sparse (single DOM subtree) nested components. Nesting of sparse components is unsupported.

Knockout.js subscriber

Javascript mixin class Subscriber may be used to control Knockout.js viewmodel methods subscriptions. To add this mixin, use mixProps() function, which is similar to inherit(), but is lighter because does not use inheritance chain:

import { mixProps } from '../../djk/js/dash.js';
import { Subscriber } from '../../djk/js/ko.js';

mixProps(Subscriber.prototype, this);

In case there is observable property:

this.meta.rowsPerPage = ko.observable();

Which changes should be notified to viewmodel method:

Grid.on_meta_rowsPerPage = function(newValue) {
    this.actions.perform('list');
};

Then to subscribe that method to this.meta.rowsPerPage() changes:

this.subscribeToMethod('meta.rowsPerPage');

An example of temporary unsubscription / subscription to the method, used to alter observable value without the execution of an observation handler:

Grid.listCallback = function(data) {
    // ... skipped ...
    // Temporarily disable meta.rowsPerPage() subscription:
    this.disposeMethod('meta.rowsPerPage');

    // Update observable data but .on_meta_rowsPerPage() will not be executed:
    this.meta.prevRowsPerPage = this.meta.rowsPerPage();
    this.meta.rowsPerPage(data.rowsPerPage);

    // Re-enable meta.rowsPerPage() subscription:
    this.subscribeToMethod('meta.rowsPerPage');
    // ... skipped ...
}

dash.js

This module implements low-level Javascript helpers, such as:

  • advanced typechecking isMapping() / isScalar()
  • value conversion intVal() / capitalize() / camelCaseToDash()
  • ODict ordered dict element, used by NestedList / GridColumn (See Datatables for more info.)
  • Multiple level Javascript class inheritance

Multiple level Javascript class inheritance

  • inherit() - implementation of meta inheritance. Copies parent object prototype methods into instance of pseudo-child. Supports nested multi-level inheritance with chains of _super calls in Javascript via SuperChain class.
  • Multi-level inheritance should be specified in descendant to ancestor order.

For example to inherit from base class ClosablePopover, then from immediate ancestor class ButtonPopover, use the following Javascript code:

import { inherit } from '../../djk/js/dash.js';
import { ButtonPopover, ClosablePopover } from '../../djk/js/popover.js';

CustomPopover = function(options) {
    // Immediate ancestor.
    inherit(ButtonPopover.prototype, this);
    // Base ancestor.
    inherit(ClosablePopover.prototype, this);
    this.init(options);
};

(function(CustomPopover) {

    CustomPopover.init = function(options) {
        // Will call ButtonPopover.init(), with current 'this' context when such method is defined, or
        // will call ClosablePopover.init(), with current 'this' context, otherwise.
        // ButtonPopover.init() also may call it's this._super._call('init', options) via inheritance chain.
        this._super._call('init', options);
    };

})(CustomPopover.prototype);

An example of multi-level inheritance from the built-in grid/dialogs.js:

import { Dialog } from '../dialog.js';

function FilterDialog(options) {

    inherit(Dialog.prototype, this);
    this.create(options);

} void function(FilterDialog) {

    FilterDialog.create = function(options) {
        // ... skipped ...
    };

    // ... skipped ...

}(FilterDialog.prototype);

function GridDialog(options) {

    inherit(FilterDialog.prototype, this);
    inherit(Dialog.prototype, this);
    this.create(options);

} void function(GridDialog) {

    GridDialog.template = 'ko_grid_body';

    GridDialog.create = function(options) {
        this.componentSelector = null;
        this._super._call('create', options);
    };

    // ... skipped ...

}(GridDialog.prototype);

See Datatables for more info.

popovers.js

Advanced popovers

ClosablePopover creates the popover with close button. The popover is shown when mouse enters the target area. It’s possible to setup the list of related popovers to auto-close the rest of popovers besides the current one like this:

import { ClosablePopover } from '../../djk/js/popover.js';

messagingPopovers = [];

var messagingPopover = new ClosablePopover({
    target: document.getElementById('notification_popover'),
    message: 'Test',
    relatedPopovers: .messagingPopovers,
});

ButtonPopover creates closable popover with additional dialog button which allows to perform onclick action via overridable .clickPopoverButton() method.

plugins.js

Set of jQuery plugins.

jQuery plugins

  • $.autogrow plugin to automatically expand text lines of textarea elements;
  • $.linkPreview plugin to preview outer links in secured html5 iframes;
  • $.scroller plugin - AJAX driven infinite vertical scroller;
  • $.replaceWithTag plugin to replace HTML tag with another one, used by initClient and by Underscore.js templates to create custom tags.

ko.js

Some of these jQuery plugins have corresponding Knockout.js bindings in ko.js, simplifying their usage in client-side scripts:

  • ko.bindingHandlers.autogrow:

    <textarea data-bind="autogrow: {rows: 4}"></textarea>
    
  • ko.bindingHandlers.linkPreview:

    <div data-bind="html: text, linkPreview"></div>
    
  • ko.bindingHandlers.scroller:

    <div class="rows" data-bind="scroller: {top: 'loadPreviousRows', bottom: 'loadNextRows'}"></div>
    

To make these bindings available, one has to import and to execute useKo function:

import { useKo } from '../../djk/js/ko.js';

useKo(ko);

which is performed already in document.js.

tooltips.js