Javascript Best Practices

I have been working on more JavaScript this year. Most recently, I improved the speed of the Drupal API autocomplete. I am now working on Drupal.org’s new dashboard and an annotation tool. I focus on improving speed for the user. JavaScript can do great things for rich interactions, but depending on AJAX makes everything nearly as slow (and as performance-expensive) as a full page reload.

I am presenting material from this blog post at Bay Area Drupal Camp this Saturday October 17th.

First, some basics. Follow the coding standards, they are different from PHP. Use jQuery and jQuery UI as much as possible. It is not just for avoiding the DOM, it also has some handy utilities. Use JSLint, which checks for many code quality improvements, like Drupal’s Coder module. JSLint will save you time debugging IE issues, like

 

{
  foo: 'bar',
  baz: 'quux', // <- this trailing comma causing hard-to-debug problems.
}

 

Use Drupal.settings or hidden elements for additional page content, not AJAX. Going back to the server for more content has the overhead of an HTTP request and bootstrapping Drupal. Adding content is instant when the browser already has it from the initial page load. Save AJAX for truly dynamic content from a large data set.

I frequently use an expandable template. An example from the API module:

 
theme('api_expandable',
  '<h3>'. api_show_l('▸ '. $call_title) .'</h3>',
  '<h3>'. api_hide_l('▾ '. $call_title) .'</h3>'. theme('api_functions', $call_functions));

The first argument is a prompt and the second is content. api_show_l() and api_hide_l() are helper functions, defined in api.module to make the hide and show links. The template is simply:

 
<div class="api-expandable<?= (is_null($class) ? '' : ' '. $class) ?>">
  <div class="prompt"><?= $prompt ?></div>
  <div class="content"><?= $content ?></div>
</div>

A little Javascript and CSS makes everything work. Notice that the content is hidden page load by CSS, not Javascript; CSS is faster than Javascript. This template is incredibly versatile, swapping between two states is surprisingly common.

When users perform actions that are saved via AJAX, update the page as soon as possible. Do not wait for an AJAX response if it do not have useful data. Update the UI instantly, and then save data in the background. I use this technique in the Dashboard module. An example is removing a widget:

 
Drupal.behaviors.dashboardWidget = function(context) {
  $("#dashboard>div.column>div.widget:not(.dashboard-processed)", context)
  .addClass('dashboard-processed').each(function() {
    var $this = $(this);
    $('a.remove-widget', $this).data('widget', $this).click(function () {
      var $widget = $(this).data('widget').slideUp('fast');
      jQuery.post(Drupal.settings.basePath + 'dashboard/' + Drupal.settings.dashboardPage + '/remove-widget', {
        token: Drupal.settings.dashboardToken,
        widget_id: $widget.attr('id').replace(/^widget-/, '')
      });
      return false;
    });
  });
}

The widget is quickly hidden with .slideUp('fast'), followed by telling the server with a custom menu callback. Additionally, this demonstrates using behaviors. When the initial page load, and subsequent AJAX additions, are ready, all behaviors are invoked. Always use behaviors to attach functionality to HTML.

Another necessary practice in the previous example is using tokens. Any actions or data saved to the server must be validated as what the user intended, not a CSRF exploit. This is done with tokens. All forms are automatically protected with Drupal’s Form API, but any custom callback need manual validation. The token is stashed in Drupal.settings with

 
drupal_add_js(array(
  'dashboardToken' => drupal_get_token('dashboard ' . $page->page_id),
),'setting');

When the callback is handled, the token is checked with a corresponding call to drupal_valid_token(). Callbacks should require POST for actions as well. Browsers sometimes prefetch content and crawlers follow all links; it is best to make actions specific using POST.

Updating the page as soon as possible can be done using AHAH with forms. The Form API builds in lots of good things, use form wherever possible. AHAH makes AJAX doable entirely within Drupal. The example below submits form data to a menu callback:

 
$form['edit_submit'] = array(
  '#type' => 'submit',
  '#value' => t('Save'),
  '#ahah' => array(
    'event' => 'click',
    'path' => 'dashboard/'. $page->path .'/rename',
  ),
);

The callback is straightforward, but returns an empty result:

 
function dashboard_page_rename($page) {
  drupal_get_form('dashboard_page_edit_form', $page);
  drupal_json(array('status' => TRUE, 'data' => ''));
}

AHAH is designed to update the page content with the result; but we don't want to wait around for something we already know. An additional jQuery click handler sets the tab’s title to what the user entered and removes the form:

 
$dashboardEditForm.find('#edit-edit-submit').click(function() {
  $dashboardActiveSpan.find('>a.edit').html(Drupal.checkPlain($dashboardEditForm.find('#edit-edit-title').attr('value')) + '<span class="edit-icon"></span>');
  dashboardRemoveEditPageForm();
});

Using these best practices and paying attention to latecy-causing actions will make your site fast for your users.

Topics