Javascript Modules How-To

This section describes how to write Javascript modules for the OSF, use webpack to build assets, and include built assets in HTML. We also provide starter templates for new JS modules.

See also

Looking for the JS style guidelines? See here .

Writing Modules

  • Use the CommonJS module style.
  • Reuseable modules go in website/static/js/. Name modules in lowerCamelCase.
  • Initialization code for a page goes in a module within website/static/js/pages/. Name page modules with lower-dashed-case.

A Note on Utility Functions

Put reusable utility functions in website/static/js/osfHelpers.js.

// osfHelpers.js

var myCopaceticFunction = function() {...}

// ...
module.exports = {
    // ...
    myCopaceticFunction: myCopaceticFunction
};

Example

Let’s say you’re creating a reuseable Markdown parser module for the wiki edit page. Your module would go in website/static/js/.

website/static/js/osfMarkdownParser.js

/**
 * A Markdown parser with special syntax for linking to
 * OSF projects.
**/
'use strict';

// CommonJS/Node-style imports at the top of the file

var $osf = require('js/osfHelpers');

// Private methods go up here
function someHelper() {
    // ....
}
// This is the public API
// The constructor
function OSFMarkdownParser (selector, options) {
    this.selector = selector;
    this.options = options;
    this.init();
}
// Methods
OSFMarkdownParser.prototype.init = function() {
    //...
}

OSFMarkdownParser.prototype.somePublicMethod = function() {
    //...
}

// Export the constructor
module.exports = OSFMarkdownParser;

The initialization of your Markdown parser would go in website/static/js/pages/wiki-edit-page.js (assume that this file already exists).

website/static/js/pages/wiki-edit-page.js

// Initialization of the Markdown parser
var OSFMarkdownParser = require('js/osfMarkdownParser');

new OSFMarkdownParser('#wikiInput', {...});

// ... other wiki-related initialization.

Third-party Libraries

The following libraries can be imported in your JS modules (using require('name')):

Building and Using Modules

Webpack Entry Points

Each module in website/static/js/pages corresponds to an entry point in webpack and has a rough one-to-one mapping with a page on the OSF. Here is what the wiki-edit-page entry would look like in the webpack configuration file.

webpack.common.config.js

// Entry points built by webpack. The keys of this object correspond to the
// names of the built files which are put in /website/static/public/js/. The values
// in the object are the source files.
var entry = {
    //...
    'wiki-edit-page': staticPath('js/pages/wiki-edit-page.js'),
    // ...
}

Note

You will seldom have to modify webpack.common.config.js. The only time you may need to care about it is when a completely new page is added to the OSF.

Building with Webpack

Webpack parses the dependency graphs of the modules defined in the entry points and builds them into single files which can be included on HTML pages. The built files reside in website/static/public/js/. Therefore, the built file which would include your Markdown parser initialization would be in /static/public/js/wiki-edit-page.<hash>.js. This is the file that would be included in the HTML template.

Note

Webpack will add a hash to the filenames of the built files to prevent users’ browsers from caching old versions (example: wiki-edit-page.js becomes wiki-edit-page.4ec1318376695bcd241b.js).

Therefore, we need to resolve the short filenames to the full filenames when we include them in the HTML. More on that in the next section.

To build the assets for local development, use the assets invoke task.

$ inv assets --debug --watch
# OR
$ inv assets -dw

Loading the Modules in HTML with webpack_asset

Once you have the built assets, you can include them on HTML pages with a <script> tag. In order to resolve the short filenames to the filenames on disk (which include hashes), use the webpack_asset Mako filter.

website/templates/wiki/edit.mako

<%def name="javascript_bottom()">
<script src=${"/static/public/js/wiki-edit-page.js" | webpack_asset}></script>
</%def>

Examples

Todo

Document how to use mako variables in JS modules (contextVars)

Knockout Modules

A module contains the Knockout model(s) and ViewModel(s) for a single unit of funtionality (e.g. login form, contributor manager, log list, etc.)

Knockout modules aren’t much different from regular modules.

  • Apply bindings in the constructor.
  • Use the osfHelpers.applyBindings helper. This will ensure that your ViewModel will be bound to the element that you expect (and not fall back to <body>, as ko.applyBindings will sometimes do). You can also pass $osf.applyBindings a selector instead of an HTMLElement.
  • Name the HTML ID that you bind to with “Scope”. Example: <div id="logfeedScope">.
  • Adding the scripted CSS class to the div you bind to will hide the div until $osf.applyBindings finishes executing. This is useful if you don’t want to show any HTML for your component until the ViewModel is bound.

website/static/js/logFeed.js

/**
 * Renders a log feed.
 **/
'use strict';
var ko = require('knockout');

var $osf = require('js/osfHelpers');

/**
* Log model.
*/
var Log = function(params) {
    var self = this;
    self.text = ko.observable('');
    // ...
};

/**
* View model for a log list.
* @param {Log[]} logs An array of Log model objects to render.
*/
var LogViewModel = function(logs) {
    var self = this;
    self.logs = ko.observableArray(logs);
    // ...
};

////////////////
// Public API //
////////////////

var defaults = {
    data: null,
    progBar: '#logProgressBar'
};

function LogFeed(selector, options) {
    var self = this;
    self.selector = selector;
    self.options = $.extend({}, defaults, options);
    self.$progBar = $(self.options.progBar);
    self.logs = self.options.data.map(function(log) {
        return new Log(log.params);
    })
};
// Apply ViewModel bindings
LogFeed.prototype.init = function() {
    var self = this;
    self.$progBar.hide();
    $osf.applyBindings(new LogViewModel(self.logs), self.selector);
};

module.exports = LogFeed;

website/static/pages/some-template-page.js

'use strict';

var LogFeed = require('js/logFeed');

// Initialize the LogFeed
new LogFeed('#logScope', {data: ...});

website/templates/some_template.mako

<div class="scripted" id="logScope">
    <ul data-bind="foreach: {data: logs, as: 'log'}">
        ...
    </ul>
</div>

<%def name="javascript_bottom()">
<script src=${"/static/public/js/some-template-page.js" | webpack_asset}></script>
</%def>

Templates

To help you get started on your JS modules, here are some templates that you can copy and paste.

JS Module Template

/**
 * [description]
 */
'use strict';
var $ = require('jquery');


function MyModule () {
    // YOUR CODE HERE
}

module.exports = {
    MyModule: MyModule
};

Knockout Module Template

/**
 * [description]
 */
'use strict';
var ko = require('knockout');

var $osf = require('js/osfHelpers');

function ViewModel(url) {
    var self = this;
    // YOUR CODE HERE
}

function MyModule(selector, url) {
    this.viewModel = new ViewModel(url);
    $osf.applyBindings(this.viewModel, selector);
}

module.exports = {
    MyModule
};