Jump to >

JavaScript Extensions

Extensions are not purely server-side. Often, they need to interact with some part of the client-side UI, whether that’s to augment a dialog, dynamically render new UI, or communicate with the API.

To hook into things on the client side, JavaScript-side extensions can be created. These are defined server-side and then served up to the browser. They work much like the typical Python extensions, in that you’ll create an extension subclass (of RB.Extension()) and make use of hooks.

Creating an Extension

You’ll first start by defining some information server-side about your extension. You’ll need to create a subclass of reviewboard.extensions.base.JSExtension and reference it in your main extension class, using the Extension.js_extensions attribute. Your JSExtension subclass will also specify the name of the JavaScript-side extension model class to instantiate.

Here’s an example:

from reviewboard.extensions.base import Extension, JSExtension


class SampleJSExtension(JSExtension):
    model_class = 'SampleExtensionProject.Extension'


class SampleExtension(Extension):
    js_extensions = [SampleJSExtension]

    js_bundles = {
        'default': {
            'source_filenames': (
                'js/extension.js',
            ),
        }
    }

You’ll then need to create your JavaScript-side extension in a file listed in an appropriate bundle (such as the js/extension.js shown above):

window.SampleExtensionProject = {};

SampleExtensionProject.Extension = RB.Extension.extend({
    initialize: function() {
        RB.Extension.initialize.call(this);

        /* Your setup goes here. */
    }
});

Now unlike Python-side extensions, you don’t need to worry about things like managing the shutdown logic for extensions. These extensions only run while the page is loaded, and are re-initialized on every page load. They do not persist.

Tip

You can create multiple JavaScript-side extensions, which is useful when you have different requirements for different pages. You don’t need to do everything in one extension.

See Page-Specific Extensions for more information.

Instantiating Hooks

We ship with a few JavaScript-side extension hooks that you can use. These are documented in the JavaScript Extension Hooks list, and are instantiated like so:

SampleExtensionProject.Extension = RB.Extension.extend({
    initialize: function() {
        RB.Extension.initialize.call(this);

        new RB.SomeExampleHook({
            extension: this,
            ...
        });
    }
});

See the documentation for each hook on its usage.

Note

There aren’t a lot of JavaScript-side hooks yet, and we’re still evaluating what makes sense to add here. If you have a particular need for a hook, you can suggest one on the reviewboard-dev list.

You can also manually listen to events, set up UI, register handlers, etc. without using hooks. Anything you set up will be undone when the user closes or leaves the page. However, please note that JavaScript-side classes/events are subject to change, so please code defensively!

Page-Specific Extensions

You can specify that an extension should only load on one or more specific pages, or define different extensions for different pages. This is really useful when you want to augment the behavior of the review request, a review UI, etc., but don’t want to carry all that logic around to every page.

To do this, you’ll make use of the JSExtension.apply_to attribute. This is a list of URL names that the extension will be loaded on. See the Static Media guide on Applying To Specific Pages for a list.

You should also put your extension in a bundle that will be loaded only for those same pages, using the apply_to key for the bundle.

Here’s an example that loads the extension only for diff viewer page and one custom URL for your extension:

from reviewboard.extensions.base import Extension, JSExtension
from reviewboard.urls import diffviewer_url_names


class SampleJSExtension(JSExtension):
    model_class = 'SampleExtensionProject.Extension'
    apply_to = diffviewer_url_names + [
        'sample-extension-project-my-diff-url',
    ]


class SampleExtension(Extension):
    js_extensions = [SampleJSExtension]

    js_bundles = {
        'diffviewer-extension': {
            'source_filenames': (
                'js/diffviewer-extension.js',
            ),
            'apply_to': SampleJSExtension.apply_to,
        }
    }

Accessing Extension Data

JavaScript-side extensions are automatically instantiated with some information about the extension. There are a few Backbone.js attributes available for your extension interface:

id:
The ID of your extension (same as MyExtensionClass.id).
name:
The name of your extension (see Defining Extension Metadata).
settings:
Settings stored for your extension (see Extension Settings).

You can also define custom data to pass (see Custom Model Data).

Extension Settings

By default, your JavaScript-side extension will receive all of your extension’s settings. These are read-only, and will be accessible through your settings attribute on your extension’s instance.

Here’s an example of how extension settings can work:

extension.py:
class SampleExtension(Extension):
    default_settings = {
        'feature_enabled': True,
    }

    ...
extension.js:
SampleExtensionProject.Extension = RB.Extension.extend({
    initialize: function() {
        RB.Extension.initialize.call(this);

        if (this.get('settings').feature_enabled) {
            ...
        });
    }
});

Warning

You may not want all your settings to be passed onto the page. There might be some secret information (license keys, for instance) that you’d like to keep from the page. Remember that anything loaded onto the page is available for the user to see.

To provide only certain settings to your extension, or to normalize the content for the page, you can override JSExtension.get_settings. For example:

class SampleJSExtension(JSExtension):
    ...

    def get_settings(self):
        settings = self.extension.settings

        return {
            'setting1': settings.get('setting1'),
            'setting2': settings.get('setting2'),
            ...
        }

Custom Model Data

You can also define custom data on the Python side that will be passed to your extension instance, separately from settings. This is useful when you want to precompute some form of data to pass down, based on the state of the server or of your Python-side extension. This can be done by overriding JSExtension.get_model_data.

class SampleJSExtension(JSExtension):
    ...

    def get_model_data(self):
        return {
            'some_state': SampleExtension.calculate_some_state(),
        }

Your JavaScript-side extension can then get access to this data using standard Backbone.js attribute accessors:

SampleExtensionProject.Extension = RB.Extension.extend({
    initialize: function() {
        var someState;

        RB.Extension.initialize.call(this);

        someState = this.get('some_state');

        ...
    }
});