Create Custom Commands with the Drupal 7 AJAX Framework

January 2012

With version 7, Drupal introduced a powerful new framework for executing javascript in the browser by issuing server side commands. AJAX commands in core wrap common jQuery DOM manipulation methods, allowing you to “replace”, “append” and “insert” HTML into the DOM without having to write any javascript code. These commands are most often used in conjunction with callbacks defined by the new “#ajax” property of the Form API, but they can also prepare responses to any AJAX callback.

If your UI is frequently using a complex jQueryUI effect, or invoking a third-party jQuery plug-in, you may want to create your own javascript command so that you can easily and consistently re-use that behavior from the server side.

Example: Creating AJAX Commands for the BeautyTip plug-in.

In this example, we’ll create some custom commands to allow us to use the jQuery BeautyTip plug-in (for creating Netflix-style tooltips) from our server-side code. We’ll add a link to node teasers that opens a comment form inside a BeautyTip and we’ll handle updating the form upon submission.

The JavaScript

First, we’ll create a custom javascript file that contains our javascript commands. The first command “bt_display” opens a BeautyTip tooltip populated by HTML sent from the server:

/**
   * AJAX command to place HTML within a BeautyTip.
   */
  Drupal.bt_form.bt_display = function(ajax, response, status) {
    // Close any other active BeautyTips.
    $('.bt-form-active').removeClass('active').btOff();

    // Initialize the BeautyTip settings.
    var settings = {};
    settings = Drupal.settings.bt_form;
    settings.trigger = 'none';

    // Open the BeautyTip.
    $active_element = $(ajax.element);
    $active_element.bt(response.data, settings).btOn().addClass('bt-form-active');

    // Attach behaviors to our newly added elements.
    Drupal.attachBehaviors($active_element);
  }

All Drupal AJAX commands share the same signature and are passed the “ajax”, “response”, and “status” parameters. The “ajax” object provides context to the command like what event triggered the command and what DOM element that event was attached to. The “response” object includes information sent from the server. In a bit, we’ll see how we populate that object on the server-side; for now, just know that the “data” element includes the HTML that needs to be added inside the BeautyTip. Lastly, the “status” parameter indicates the success or failure of the AJAX call.

In this example, we use the “element” property of the “ajax” element to determine which element triggered the AJAX callback—which link was clicked—and we attach the BeautyTip to that element. Alternatively, we could have included a “selector” value in our response—as many of the core AJAX commands do—and used that information to determine which element receives the BeautyTip.

We need a way to close the BeautyTip so we also have a simple “dismiss” command, to close all open BeautyTips on the page.

  /**
   * AJAX command to dismiss BeautyTip.
   */
  Drupal.bt_form.bt_dismiss = function(ajax, response, status) {
    $('.bt-form-active').removeClass('bt-form-active').btOff();
  };

And finally, we tell the Drupal AJAX framework about these new commands by adding them to the commands collection.

  // Add our commands to the Drupal commands collection.
  Drupal.ajax.prototype.commands.bt_display = Drupal.bt_form.bt_display;
  Drupal.ajax.prototype.commands.bt_dismiss = Drupal.bt_form.bt_dismiss;

 

The PHP

Now, let’s implement the PHP code needed to actually use these commands. First, let’s create PHP functions to encapsulate the corresponding AJAX commands. This step is not required but it’s good practice and follows the design pattern in core.

<?php
 
/**
 * AJAX command to place HTML within the BeautyTip.
 *
 * @param $title
 *   The title of the BeautyTip.
 * @param $html
 *   The html to place within the BeautyTip.
 */
function bt_form_command_display($title, $html) {
  if (is_array($html)) {
    $html = drupal_render($html);
  }
  $html = '<h2>' . $title . '</h2>' . $html;
  return array(
    'command' => 'bt_display',
    'data' => $html,
  );
}

/**
 * AJAX command to dismiss active BeautyTips.
 */
function bt_form_command_dismiss() {
  return array(
    'command' => 'bt_dismiss',
  );
}
?>

These functions simply generate a command array suitable for sending back to the page to be executed by the AJAX framework.

Second, we’ll add an AJAX “Add Comment” link to all node teasers using hook_node_view.

<?php
/**
 * Implements hook_node_view().
 *
 * @param $node
 * @param $view_mode
 * @return
 */
function bt_form_node_view($node, $view_mode) {
  if ($node->comment > 0 && $view_mode == 'teaser' && user_access('post comments')) {
    // Add an AJAX link to the node teaser.
    $node->content['node-add-comment'] = array(
      '#type' => 'link',
      '#title' => t('Add comment'),
      '#href' => 'bt-form/callback/nojs/'. $node->nid,
      '#id' => 'ajax_link_' . $node->nid,
      '#ajax' => array(
        'wrapper' => '#ajax_link_' . $node->nid,
        'method' => 'html',
      ),
    );
  }
  return $node;
}
?>

Above, we’ve used a render array to add the link to the node. We could have inserted plain HTML as long as we gave the link a class of “use-ajax”. When a user clicks on “AJAX-ified” links—and has javascript enabled—the AJAX Framework posts a request to a URL that closely resembles the “href” property of the link except that the “nojs” portion of the URL is replaced with “ajax”. It’s our responsibility to make sure that the URL returns a valid response whether or not an AJAX call made the request. If the user doesn’t have javascript, our callback should return a standard page.

So, let’s go ahead and build that callback and some related helpers. Let’s start by implementing hook_menu and declaring our callback:

<?php
/**
 * Implements hook_menu().
 */
function bt_form_menu() {
  $items = array();

  $items['bt-form/callback'] = array(
    'page callback' => 'bt_form_callback',
    'access callback' => 'user_access',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK
  );

  return $items;
}
?>

 

Before we can use our AJAX commands we need to add the required javascript and css to the page. That’s what the following function does; we’ll call it from our callback function.

<?php
/**
 * Helper function to add required javascript and CSS to the page.
 *
 * @return mixed
 */
function bt_form_add_js() {
  // Initialize a flag; we only need to do this one per page request.
  static $done = FALSE;
  if ($done) {
    return;
  }

  // Create a settings array suitable for passing to BeautyTip Plug-in.
  $bt_settings = array(
    'bt_form' =>
    array(
      'fill' => '#fff',
      'strokeStyle' => '#666666',
      'width' => 'auto',
      'overlap' => 0,
      'shadow' => TRUE,
    )
  );

  // Add BeautyTip settings to Drupal.settings.
  drupal_add_js($bt_settings, 'setting');

  // Add the requisite libraries and our custom js file.
  $js_path = drupal_get_path('module', 'bt_form') . '/js/';
  drupal_add_js($js_path . 'jquery.bt.min.js');
  drupal_add_js($js_path . 'excanvas_r3/excanvas.compiled.js');
  drupal_add_js($js_path . 'bt_form.js');

  // Add the BeautyTip plug-in's css.
  drupal_add_css($js_path . 'jquery.bt.css');

  // Set flag to true.
  $done = TRUE;
}
?>

Now we can build the menu callback that will respond to a click of the AJAX link.

<?php
/**
 * Page callback for menu item.
 */
function bt_form_callback($method, $nid) {

  // Add the requisite javascript and css.
  bt_form_add_js();

  // Try to load the node.
  if ($node = node_load($nid)) {
    // Construct the appropriate comment form id.
    $form_id = "comment_node_{$node->type}_form";

    // Generate the form; we pass the method as custom parameter so our hook_form_alter
    // can make some changes.
    $form = drupal_get_form($form_id, (object) array('nid' => $node->nid), $method);

    // If ajax, open the form in the BeautyTip.
    if ($method == 'ajax') {
      $commands[] = bt_form_command_display(t('Add a Comment'), $form);
      $response = array('#type' => 'ajax', '#commands' => $commands);
      ajax_deliver($response);
    }
    // If not AJAX, just return the form; Drupal will render it in a page.
    else {
      return $form;
    }
  }
}
?>

This function receives two parameters: the $method which will be “nojs” or “ajax” and the node id ($nid). Remember that those parameters were included in the href of the AJAX link earlier. The function attempts to load the node and generate a comment form. If the request came via AJAX, we pass the form array to our BeautyTip display command and build a response array that “ajax_deliver” turns to JSON and returns to the page. If the request was “normal”, we simply return the form array which Drupal renders as a full page.

That’s great, but I bet you’re wondering what happens when I click “Save” on that comment form. Well, right now, it would create a whole page load and redirect you to the node page. It would be better if we could also submit the form via AJAX and, either re-present the form inside the BeautyTip with errors that need to be fixed or close the BeautyTip and add a success message to the page.

To achieve this end, we need to do a little form altering on the comment form:

<?php
 
/**
 * Implements hook_form_alter().
 *
 * @param $form
 * @param $form_state
 * @param $form_id
 */
function bt_form_form_alter(&$form, $form_state, $form_id) {

  // If this is the comment form.
  if (!empty($form_state['build_info']) && $form_state['build_info']['base_form_id'] == 'comment_form') {

    // If drupal_get_form was called with an extra parameter for method and that method is ajax.
    if (!empty($form_state['build_info']['args'][1]) && $form_state['build_info']['args'][1] == 'ajax') {

      // Wrap the form.
      $form['#prefix'] = '<div id="ajax-comment-form">';
      $form['#suffix'] = '</div>';

      // Add an ajax element to the submit button and specify a callback function.
      $form['actions']['submit']['#ajax'] = array(
        'callback' => 'bt_form_form_callback',
        'wrapper' => 'ajax-comment-form'
      );

      // Use a plain-text textarea instead of a formatted textarea.
      $form['comment_body'][LANGUAGE_NONE][0]['#type'] = 'textarea';

      // Remove the preview button.
      unset($form['actions']['preview']);
    }
  }
}
?>

The main alteration we make to the comment form is that we add an “#ajax” property to the submit button which will cause the AJAX framework to take over instead of submitting the form the “normal” way. Note that the callback we specify “bt_form_form_callback” is a function name; using this syntax Drupal “automagically” handles wiring up a URL to handle this callback. In other words, you don’t need to create a URL for this callback in hook_menu the way we did with the link on the node teaser. You should also note that “wrapper” is required for reasons that we’ll get to in just a moment. Lastly, I’ve made some minor changes to the form, forcing the body field to a plain textarea and removing the “Preview” button.

Next, we need to create the callback function “bt_form_form_callback”. This function’s job is to prepare the AJAX response after the form has been processed. All the normal form altering, submission, and validation will have already occurred. Let’s take a look:

<?php
/**
 * AJAX form response callback.
 *
 * @param $form
 * @param $form_state
 * @return array
 */
function bt_form_form_callback(&$form, &$form_state) {
  // If there are errors, just return the form.
  if ($errors = form_get_errors()) {
    return $form;
  }
  // No errors; close the BeautyTip and add a success message.
  else {

    // Get themed status messages.
    $messages = theme('status_messages');

    // Close the BeautyTip.
    $commands[] = bt_form_command_dismiss();

    // Display status (success) messages after the link.
    $commands[] = ajax_command_after('#ajax_link_' . $form['#node']->nid, $messages);

    // Return the response array.
    return array(
      '#type' => 'ajax',
      '#commands' => $commands
    );
  }
}
?>

AJAX callback functions can return a render array, an HTML string or a response array containing AJAX commands. In the case of a render array or HTML string, the AJAX framework implicitly calls the AJAX “replace” command, which replaces whatever “wrapper” element you specified in the “#ajax” property with the contents of the render array or string. That’s why in the above function, when there are validation errors, we can simply return the form array (form arrays are a type of render array). Drupal is even smart enough to append error messages to the form render array. It’s actually slightly more complicated when there are no errors. In that case, we use our custom AJAX command to dismiss the BeautyTip and also insert any status messages (like “Your comment has been posted”) after the “Add Comment” link.

Wrap-up and Further Reading

This example is pretty simple and is still missing some pieces. For instance, we need to better handle a case when someone clicks the AJAX links multiple times and it would be nice to re-render the links that display the number of comments. But it’s a great start for adding slick functionality that degrades gracefully.

Here are a couple of great resources for learning more about the Drupal 7 AJAX framework:

  • Examples module includes very well commented code that demonstrates a lot of the benefits of using the AJAX framework.
  • The Drupal.org documentation is a good place to start, especially if you are looking to manipulate elements on a form after successive submissions.
  • merlinofchaos’ presentation Drupal 7 Advanced AJAX Tips And Tricks. Merlin pioneered the approach with cTools for Drupal 6 and has obviously leveraged the AJAX framework with much success in the development of Views 3.

 

This post was written by former Isoveran Kelly Lucas.