0 #permalink Javascript

For lack of a better place to put this documentation, this will do. TODO: Upgrade to better layout https://design.gitlab.com/

Source: styleguide/styleguide.less, line 3

0.1 #permalink Dependencies

Toggle full screen Open in new window Toggle example guides Toggle HTML markup

Third-party libraries we want to use have previously been kept in the /v2/js/deps folder.

As of 2018-10 we are moving to using node packages and NPM to manage versions and dependencies. This will be a gradual migration. Meanwhile, libraries in /v2/js/deps will be updated as needed, but no additional libraries will be added.

Moving to NPM resolves some of the outstanding questions around 3rd party libraries.

  • A way to document the libraries' functionality, and current maintenance status.
  • A simple way to track pages using the libraries by greping for require(). This may generate an automated list in the future.
  • A way to upgrade without risking having multiple versions in production, although we will need to be careful about libraries already in /v2/js/deps.
  • This approach will simplify minimizing libraries as most packages provide a minimized version or are compatible with minify/uglify.
  • Packaged modules should make it easier to concatenate everything into a single minified package without any special cases. This should reduce the number of requests to load a page and may have gzip benefits.

To use:

npm install library-name --save

or for things used only in the build or testing

npm install library-name --save-dev

Then in the Javascript file (*.bsfy) use the library with:

var libraryName = require("library-name");

Versions will be automatically kept and maintained in package.json and package-lock.json. To update to the latest versions run.

npm update

Test thouroughly and commit package.json and package-lock.json.

Example
Markup:
Markup
Markup:
Source: styleguide/styleguide.less, line 10

0.2 #permalink deferred, asap.js, minimal.js, and loadJS

Toggle full screen Open in new window Toggle example guides Toggle HTML markup

With speed being a primary concern, we don't allow javascript execution to block page loading. It's entirely possible to do anything you're used to, but it could take a little more work. For example, you can't assume that you have access to jQuery in the head or at any point in the body of your page, so putting

<script type="text/javascript">$(window).onload(function(){//etc});</script>

in your head or something like

<script type="text/javascript">$('.clazz').hide();<script>

in your body will not work.

Instead, the deferred array, asap.js and minimal.js files, and loadJS function should help you to get around most of these issues.

deferred

deferred is a simple javascript array that you can add functions to, and the functions will be executed once jQuery and the rest of our environment is set up. It's defined right up at the top of head.inc:

<script type="text/javascript">
   var deferred = [];
</script>

There are two ways to add functions to the deferred array. The things added can be functions taking no arguments, so:

<script type="text/javascript">deferred.push(positionSelectors);</script>
<script type="text/javascript">deferred.push(function(){$(window).onload(function(){//etc});});</script>

or you can add an array where the first element is a string matching the name of the function, and the remaining elements are arguments to be passed to the function:

deferred.push(["loadJS","/v2/js/modules/tile.js"])
deferred.push(["gg.img.loadBg","hero","/img/banners/hero_nonprofits","jpg"])

asap.js and minimal.js

/v2/js/asap.js and /v2/js/minimal.js are the (only!) scripts that are run on every page on our site. asap.js is run (asynchronously) quite early in the head, and minimal.js is run once the page has loaded. asap.js contains functions that absolutely must be run early in the pageload, such as the Optimizely snippet (to remove the undesirable "flash"), and code to set the refcode and conversion code (to give us the best chance possible of tracking people if they navigate away quickly). This is also where deferred and the top-level gg object are defined, so that pages have access to these variables. Because these scripts are run on every pageload, we need to be really vigilant to make sure everything in there needs to be in there.

loadJS

This is a tiny function (see the documentation here) that allows us to load scripts sequentially and asynchronously. The function takes two arguments: the first is the source of the script to load, and the second is the callback function to execute once it's loaded. It's being used to load minimal.js, since it (currently) requires jQuery, so first we load jQuery, then load minimal.js in the callback.

loadJS("/v2/js/deps/jquery-1.12.2.min.js", function() {
   loadJS("/v2/js/minimal.js");
});

If you need to load a number of scripts that don't directly depend on each other, you can pass an array of scripts to loadJS. Note that each script is loaded asynchronously, so there is no gurantee of execution order. But this can be useful if your page relies on a number of 3rd party scripts (say in PE or an admin console) that do not directly rely on each other. The following example show loading 5 scripts, and then calling an init function. Although the 5 indicated scripts may load (and execute) in any order, the init function will not get called until after all scripts are loaded. Note that the init function passed as a separate argument to loadJS.

   loadJS([
      "/javascript/jquery-ui/1.11.1/jquery-ui.js",
      "/javascript/jquery/jquery.blockUI-2.6.6.js",
      "/javascript/jquery/jquery.form.js",
      "/v2/js/deps/jquery.tablesorter-v2.45.5.combined.min.js",
      "/v2/js/admin/checkentry/edit-batch.js"
   ],
   init);

Or, to do things with deferred:

deferred.push([
   "loadJS", [
      "/javascript/jquery-ui/1.11.1/jquery-ui.js",
      "/javascript/jquery/jquery.blockUI-2.6.6.js",
      "/javascript/jquery/jquery.form.js",
      "/v2/js/deps/jquery.tablesorter-v2.45.5.combined.min.js",
      "/v2/js/admin/checkentry/edit-batch.js"
   ],
   init
]);

If you need to include external javascript, you can include it in the head or in the foot_scripts apache variable using the async parameter:

<!--#set var="foot_scripts" value='
  <script src="/path/to/myScript.js" async></script>
'-->

Or you can load it with loadJS. But note that your statement will be processed before loadJS has been defined, so you'll want to wrap it in a function and pass that to deferred:

deferred.push(
    ["loadJS",'/path/to/myScript.js',function(){//optional callback here}];
);
Example
Markup:
Markup
Markup:
Source: styleguide/styleguide.less, line 47

0.3 #permalink Browserify

Toggle full screen Open in new window Toggle example guides Toggle HTML markup

Browserify brings some common programming conventions to the wild west of javascript. Most important, probably, is that it makes it easier to reuse pieces of code, and handles dependencies across a broad codebase.

To use browserify, just create a new file with the .bsfy extension. The gulp javascript task will "browserify" it and save it in the same location with a .js extension. For the examples below, let's say you've made a file foobar.bsfy and put it next to your index.html.

This video helped me to get my head around what's going on here.

Imports

In your file, declare any dependencies at the top using the require keyword and the relative path to the bsfy file you want to include. Let's say you want to see if the request contained a certain query parameter, then do something with that information. We have a convenience method for dealing with query parameters in meta/www/v2/js/modules/params.bsfy, so, taking care to get the relative path right, you might start your file with:

var params = require('../v2/js/modules/params.bsfy');

Now you have a javascript object containing the functionality exported from the params.bsfy file, so you can, e.g.

var fooParam = params.get('foo')
if(fooParam){
  //do something
} else {
  //do something else
}

At this point, you can include the file in your page and it will run when called just like regular javascript (it compiles to regular javascript!). If you saved the above file as foobar.bsfy in the same directory as your index.html, include it with

deferred.push(["loadJS","foobar.js"]);

Then when the page loads, your param logic will run.

Exports

At the end of your file, export any functions you want to expose publicly, like so:

function foo(){
  console.log('foo');
}

function bar(){
  foo()
  console.log('bar');
}

module.exports = {
  "barExport": bar
}

So if you save that code to foobar.bsfy, then in a new file, baz.bsfy you simply

var myImport = require('./foobar.bsfy');
myImport.barExport();

and add this to a script tag in your html:

deferred.push(function(){
  ["loadJS","baz.js"];
});

then when the script is asynchronously loaded, the console will output:

foo
bar

A few things to note:

  • loadJS("foobar.js") will define the top-level foo and bar functions, but won't run anything when it's loaded.
  • myImport.foo and myImport.bar will return undefined since the former wasn't exported and the latter was exported with the name barExport.
  • You should try to design your page so that it can be rendered with just css and html. If you use javascript to change the display behavior of an element, be aware that if you load the script asynchronously, you may observe a "flash" of the element changing as the script is loaded. If there's a good reason for it, the "right way" is to use javascript in a script block which will run immediately (because it blocks render). Note that you won't have access to jQuery yet, nor to any elements that are defined after your script.
Example
Markup:
Markup
Markup:
Source: styleguide/styleguide.less, line 135

0.4 #permalink Handlebars

Toggle full screen Open in new window Toggle example guides Toggle HTML markup

Handlebars is a minimal templating language using embedded expressions as variables. The templates are precompiled into raw javascript functions to lessen the burden on the client, but keep in mind the advice in the last section that adding or modifying elements in javascript can introduce an unwanted flash. They're perfect for adding content in response to an AJAX call (see the login.hbs template and how it's used in login.bsfy) or to reduce repetition in elements that aren't visible on pageload (see bio.hbs and team.bsfy).

See the documentation for full details, but here's the gist of how it fits into our stack:

Client-side use

  • Write your template in a file in meta/www/ with an .hbs extension. For instance, suppose you write a hello.hbs file with this content:

    <h1>Hi, {{name}}</h1>
     <p>My favorite color is {{color}}</p>
    
  • Then just require the template in your page's bsfy file. The gulp javascript command contains a browserify transform that compiles hbs templates into browserify-ready components.

    var hello = require('../../v2/js/templates/hello.hbs');
    
  • Next, get a javascript object with the context the template requires. You could build it in your bsfy file as below (and as in team.bsfy), or it could just as easily be fetched with an ajax call. This is how the login section works.

    var model = {
      name: "Nick",
      color: "orange"
    }
    
  • Finally, saturating the template with the context (the template exports a function which is passed the context as an argument) results in a string containing the resulting html, so just put it in the place you want it to go in the DOM:

    helloHtml = hello(model);
    $('.js-helloDiv').html(helloHtml);
    

    This will result in the div with class js-helloDiv receiving this completed html when the javascript is loaded and processed:

    <h1>Hi, Nick</h1>
     <p>My favorite color is orange</p>
    

Server-side use

As of May 2016, you can now include handlebars files from within your freemarker files. The point here is to abstract your components' html markup into handlebars templates, which are first rendered on the server (for fast initial pageload and no flash or loading spinner), then can be updated client-side (as in the previous section) in response to user actions or ajax calls. Awesome! Previously, a function like this would have required duplicating markup in both an ftl or html file and an hbs file (or jquery/raw javascript).

This functionality is provided by a taglib, and used as follows: simply include the tag library descriptor from your ftl file:

   <#assign hbs=JspTaglibs["/WEB-INF/handlebars.tld"]>

Then place the tag where you want the template to be rendered. The tag takes one argument: the path of the template to be used, relative to the www folder, and without extension:

   <@hbs.hbs template="v2/js/templates/tile"/>

The handlebars template has access to all public getters in your action, just as freemarker does, so if you have a getTotal() method, you can access its return value from handlebars by {{total}}. In addition, the handlebars template should have access to all variables prior #assigned in your freemarker template, as well as any local scope variables defined in a loop or macro call inside which the tag is placed. In short, any variable you can refer to from freemarker by $​{foo} should be available in handlebars as {{foo}}. There may be other things you normally have access to that are unavailable to handlebars such as properties files or text processors. Feel free to add any additional context you may need in the GGHandlebars.getContext method, or ask Nick for help.

Client-server integration

The system as built only provides a consistent view layer. It's up to the developer to decide on the model implementation and the controller logic, and to ensure that they are treated consistently on initial pageload and in response to user interaction. I'm open to either a structured set of functions to simplify or abstract these matters, or a convention that we agree on for how to do this.

Handlebars Helpers

Helpers are small javascript functions that provide added functionality to the templating system. Because of the restrictive (by design) nature of the language, it sometimes saves a lot of time to write a small function to provide the missing functionality, but before jumping to this solution I suggest you consider whether you could refactor the logic you're trying to provide into the javascript code, the action, or the freemarker template. Although the java handlebars implementation is able to use Rhino to interpret helpers written in javascript, it seems unacceptably slow (in limited local testing, it seems on the order of 1s per helper). Unless anyone can figure out a way around that, I suggest we not use this function, with the possible exception of static cached pages.

Instead, you can write two versions of a helper you want to use: one in javascript and one in java (in the HelperSource class). Note however, that some shortcuts that take advantage of javascript's untyped nature of course won't work in java, so tread carefully. See the {{#compare}} and {{#math}} helpers which are implemented this way.

Handlebars partials

To my surprise, they just work (with one little caveat)! The java implementation searches for them from the root of www, just as it does to resolve the templates when using the tag library. So if you write a partial in meta/www/path/to/page/partial.hbs, you can use it within any handlebars template by `{{> path/to/page/partial}}. This means, however, that that's the name of the partial, so if you want to use it on the client side, your registration has to be a little clunky:

    Handlebars = require("hbsfy/runtime");
     var myPartial = require("./partial.hbs");                     //location relative to the file you're writing in
     Handlebars.registerPartial("path/to/page/partial",myPartial); //the name you used in the template; relative to www
Example
Markup:
Markup
Markup:
Source: styleguide/styleguide.less, line 208