Extending Drupal 7 Autocomplete Fields' Output

Extending Drupal 7 Autocomplete Fields' Output image

Drupal 7 autocomplete field is something most content producers love. It’s an easy and convenient way to reference content by title.

The problem is, that’s all you get from an autocomplete field: a title.

One of our partners at X-Team had problems managing front page items, as the autocomplete field provided the title, but they couldn’t see anything else, such as photo, published status or schedule options. This extra info was useful to them, as they used it to plan the contents for the front page.

We came with a proposal: what if you could see some extra info about the selected article below the form field? Nothing fancy, just a photo, the published status and scheduled dates. Enough to keep track of the content on the front page in a single screen.

Homepage Articles - example publish 1

So, how do we do that? While the autocomplete engine itself is not easy to modify, Drupal 7.34 added a custom JavaScript event for autocomplete fields, so when one of those fields gets a value, the event “autocompleteSelect” is fired, and we can do our magic from there. We’re not directly modifying autocomplete output, but the end result will be the same.

What we need to do, and what you’re going to learn by doing it.

So, this is the plan:

  • Step 1: user selects an article using autocomplete field. “autocompleteSelect” event is fired.
  • Step 2: we call Drupal server again to get some extra info about the selected item.

Side note: Why can’t we request all the info directly in one step? It can probably be done this way, but it’s much, much more complicated. As the autocomplete process is technically an AJAX request, you could think it gets done using Drupal’s well-documented and fully-supported AJAX engine, but surprisingly enough, it doesn’t. It runs using its own mechanics, and it always delivers a string as a result. Truth is, while the Autocomplete engine lets coders alter the result list (the backend), trying to alter the output’s shape (the front-end) is a completely different problem with no clear solution.

Requirements

As we’re going to create custom code, we’ll need our own custom module to put our code inside. Before going further, you should know how to create your own empty module [If you need help on this, click here].

The task, step by step:

This is what we’re going to do:

  1. Declare a Drupal custom path (a “menu callback”, in  Drupal terms) to run our own code.
  2. Write our custom function.
  3. Create a template file for the output, so we deliver our results “pretty looking”.
  4. Declare our template for the theme system, so Drupal knows our template exists.
  5. Add a preprocess function for the template. This function will give proper shape to the stuff being put in the template.
  6. Alter content edition form, so it contains a placeholder for our custom content. We will also add custom JS and CSS to the page.
  7. Add our custom CSS to make our extra info look as expected.
  8. Add a piece of custom JS code, to put everything into place when autocomplete finishes its work.

Step 1: Declare a Drupal custom path to run our own code

We need a custom path in our Drupal site to run our custom code. In Drupal, this is called a “menu callback”, and in the end, it’s just a way to tell Drupal “if someone requests this path, return this function we have here”.

This is the code that makes the magic:

/**
* Implements hook_menu().
*/
function [OUR_MODULE]_menu() {
 $items['[OUR_MODULE]/autocomplete/%'] = array(
   'page callback'=>'[OUR_MODULE]_return_article_data',
   'page arguments'=> array(2),
   'access arguments' => array('administer site configuration'),
   'type'=> MENU_CALLBACK,
 );
 return $items;
}

What are we doing here? This is where the meat is:

 $items['[OUR_MODULE]/autocomplete/%'] = array(
   'page callback'=>'[OUR_MODULE]_return_article_data',
   'page arguments'=> array(2),
   'access arguments' => array('administer site configuration'),
   'type'=> MENU_CALLBACK,
 );

The key of the array in line one is our custom path. The % symbol means there will be a parameter there, a node number.

‘Page callback’ is the name of our custom function, the function that needs to be run whenever someone visits our custom path.

‘Page arguments’ is just telling Drupal that the element after the second slash is a parameter, and it has to be passed to our custom function.

‘Access arguments’ is nothing but a security measure. The visitor needs to have a certain permission to visit this URL.

‘Type: MENU_CALLBACK’ is telling Drupal we’re declaring a custom path and nothing else. No menu items, no context, no fancy stuff, just a path and a function.

Step 2: Write our custom function

/**
* AJAX (AHAH) callback for [OUR MODULE] extra info request.
* It receives NID parameter from URL.
*/
function [OUR_MODULE]_return_article_data($nid) {
 if (!is_numeric($nid)) {
   return;
 }
 $node = node_load($nid);
 $output = theme('[OUR_MODULE]__admin__article_extra_info', array('node' => $node));
 return drupal_json_output($output);
}

Very simple function, actually. It does a few things, but nothing of it is complicated. First, it takes the passed parameter “$nid” and it makes sure it’s a number. If it is, then we can proceed, so we load the requested node, and then we pass it to our custom template, to be rendered in proper shape. That is, we’re not just delivering the raw info, but the HTML for it, so Javascript doesn’t need to process the output, it just needs to insert it at the right place. No extra front-end process required.

After that, we use drupal_json_output() function to deliver our result. Even if we’re sending a chunk of HTML, this is the right way to do it, because this function not only encodes the output as JSON: it also let Drupal know this is an AJAX response and it has to behave accordingly: It will deliver the result, JSON encoded, and nothing else.

Just a detail, how does theme() function know where the template is, what’s its name, and all that? Wait for step 4…

Step 3: Create a template file for the output.

This is just a standard Drupal template file. HTML plus some PHP snippets. Trying to get things tidy, we put our custom template in a “templates” folder inside our module, so we have:

/[OUR_MODULE]/templates/[OUR_MODULE]—admin—article_extra_info.tpl.php

Just create your template with the HTML and variables you want to use. Don’t worry about populating your variables, we’ll take care of it on step 5.

Step 4: Declare our template for the theme system.

Again a step whose only purpose is to let Drupal know something exists. This is it:

/**
* Implements hook_theme().
*/
function [OUR_MODULE]_theme($existing, $type, $theme, $path) {
 $themes = array();
 $themes['[OUR_MODULE]__admin__article_extra_info'] = array(
   'variables' => array('node' => NULL),
   'preprocess functions' => array(
    '[OUR_MODULE]_preprocess_admin_article_extra_info',
   ),
   'template' => 'templates/[OUR_MODULE]--admin--article-extra-info'
 );
 return $themes;
}

This function makes the following: the array key gives a name for the theme registry to store our template info. This is the name we have to use to put our template to work using the theme() function.

The other parameters just give the info: variables we’re going to pass to the template, name of a preprocess function we want to run to prepare info for it, and location and name for the template itself.

Step 5: Add a preprocess function for the template.

As said before, our custom preprocess function will feed our template with the appropriate data, the right way. For example, it will make sure we render the photo at the right size. It’s just a standard theme preprocess function. It receives a referenced $variables array (the original one, not just a copy), it modifies or adds content where required, and that’s it. Nothing needs to be returned.

/**
* Preprocess function providing values for the HTML template that will be returned for article extra info request.
*/
function [OUR_MODULE]_preprocess_admin_article_extra_info(&$variables) {
 $image_options = array(
   'style_name' => 'mini',
   'path' => $variables['node']->field_main_image[LANGUAGE_NONE][0]['uri']
 );
 $variables['article_image'] = theme_image_style($image_options);
 [...]
}

This is just an example, not the full function, but we can see how we’re doing what we said before: we want to deliver an image at the right size. We get the image location and a proper image style. We render it and put in inside $variables array. That’s all we need to do to send this value to the template.

Step 6: Alter content edition form, so it contains a placeholder for our custom content. We will also add custom JS and CSS to the page.

In order to alter a form, you must create a function following some naming rules, so Drupal knows you want to make changes on this form just by reading the function name.

/**
 * Implements hook_form_FORM_ID_alter().
 */
function [OUR_MODULE]_form_[OUR_CONTENT_TYPE]_node_form_alter(&$form, &$form_state, $form_id) {foreach ($form['field_sidekick_slide_reference'][LANGUAGE_NONE] as $k => &$row) {
   if (is_numeric($k)) {
     $row['slide_extra_info'] = array(
       '#type' => 'markup',
       '#markup' => '',
       '#weight' => 900,
     );
   }
 }
 $module_path = drupal_get_path('module', '[OUR_MODULE]');
 drupal_add_css($module_path . '/css/[OUR_CONTENT_TYPE]_add_edit_form.css');
 drupal_add_js($module_path . '/js/article_selector_autocomplete_form.js');
}

We’re doing two things here: first, we add a placeholder for our custom info to be added. An empty DIV we will use later to put our stuff into.

After that, we’re adding our custom CSS and JS to the page. Please notice we keep things tidy: CSS goes to its own subfolder in our module, and the same goes for our JS. This is not mandatory, but maintenance becomes easier if you keep things organized from the beginning.

Step 7: Add our custom CSS.

We want to make things look good! As we added this container:

<div class="article-info"></div>

…to the form, just below the autocomplete field, we can use it as scope for our CSS.

Please notice we are adding our custom CSS to the module, not to the theme! We’re admin side here, and we’re not supposed to make changes directly on the admin theme.

Step 8: Add a piece of custom JS code, to put everything into place when autocomplete finishes its work.

Last but very important step. Everything before this has just been preparing stuff. This is the step that makes all this preparation join together.

We’re creating a Drupal attach behavior to manage all the frontend part. Attach behaviors are run on page load, but they also run after some new HTML is added to the page, so it works both as a “jQuery ready” event, and an “after AJAX HTML insertion” event, and that’s very convenient for us at this moment.

We want to attach an event to certain elements in the page. These elements can appear from the beginning, or as result of an AJAX call, so we want to make sure every element goes covered, no matter when it appears on our page. That’s why we’re using Drupal.behavior.[BEHAVIOR_NAME].attach.4

This is our code:

(function ($) {
 'use strict'; 
 Drupal.behaviors.[OUR_CONTENT_TYPE]AutocompleteExtraInfo = {
   attach: function (context, settings) { 
     var slideInfo = {
       init : function() {
         var that = this,
             $autocompleteFields = $('.form-autocomplete', this.config.$autocompleteContainers);
         $autocompleteFields.each(function() {
           that.processAutocompleteResult(this);
         });
         $autocompleteFields.on('autocompleteSelect', function() {
           that.processAutocompleteResult(this);
         });
       },
 
       processAutocompleteResult: function(field) {
         var nid = this.getNIDfromString(field.value),
             $fieldContainer = this.config.$autocompleteContainers.has(field);
 
         this.renderSlideExtraInfo(nid, $fieldContainer);
       },
 
       getNIDfromString: function(string) {
         var match = string.match(/((\d+))/g),
             lastMatch = false;
 
         if (match !== null) {
           lastMatch = match.pop();
         }
 
         return lastMatch;
       },
 
       renderSlideExtraInfo: function(nid, $fieldContainer) {
         var $slideInfoContainer = $fieldContainer.find('.slide-info');
 
         if (typeof nid === 'string') {
           $slideInfoContainer.html(this.config.throbber);
           $slideInfoContainer.load(this.config.slideInfoRequestPath + nid);
         }
       },
 
       config: {
        throbber: '',
         slideInfoRequestPath : Drupal.settings.basePath + 'fox_content_type_sidekick/autocomplete/',
         $autocompleteContainers : $('.field-name-field-sidekick-slide-reference .field-multiple-table tr', context),
       }
     };
 
     slideInfo.init();
 
   }
 };
})(jQuery);

It seems like an awful lot of code, but it’s much more simple if we go through it step by step. The anonymous function at the beginning and end of our code is just an isolation layer: it creates a custom, isolated context for our code and jQuery. The real deal starts after the “attach”.

Config:

       config: {
         throbber: ' ',
         slideInfoRequestPath : Drupal.settings.basePath + 'fox_content_type_sidekick/autocomplete/',
         $autocompleteContainers : $('.field-name-field-sidekick-slide-reference .field-multiple-table tr', context),
       }

Config, at the end of the file, is just an object containing some variables we’ll need a few times across different functions. We put them in one place to keep things organized.

Init:

       init : function() {
         var that = this,
             $autocompleteFields = $('.form-autocomplete', this.config.$autocompleteContainers);
 
         $autocompleteFields.each(function() {
           that.processAutocompleteResult(this);
         });
         $autocompleteFields.on('autocompleteSelect', function() {
           that.processAutocompleteResult(this);
         });
       },

init() function just gets the ball rolling. You’ll see the same process is firing twice, what’s happening there? Well, this first one runs for every field on load, and it’s needed for already populated fields. How is that? If you’re creating new content, all fields will be blank, but if you’re editing, some of them will be already populated.

The second process fires as a result of “autocompleteSelect” event, that is, when someone uses an autocomplete field and he/she chooses a result.

processAutocompleteResult:

       processAutocompleteResult: function(field) {
         var nid = this.getNIDfromString(field.value),
             $fieldContainer = this.config.$autocompleteContainers.has(field);
 
         this.renderSlideExtraInfo(nid, $fieldContainer);
       },

This function does two things: one, it gets the NID from the autocomplete result. Two, it uses this NID to get and render the info.

getNIDfromString:

       getNIDfromString: function(string) {
         var match = string.match(/((\d+))/g),
             lastMatch = false;
 
         if (match !== null) {
           lastMatch = match.pop();
         }
 
         return lastMatch;
       },

No mystery here. It just extracts the NID number from Autocomplete result. It doesn’t evaluate if the result makes any sense, it will be checked server-side.

renderSlideExtraInfo:

       renderSlideExtraInfo: function(nid, $fieldContainer) {
         var $slideInfoContainer = $fieldContainer.find('.slide-info');
 
         if (typeof nid === 'string') {
           $slideInfoContainer.html(this.config.throbber);
           $slideInfoContainer.load(this.config.slideInfoRequestPath + nid);
         }
       },

Again, two things here to be run if NID is a proper string, as it should be. First, we put the throbber in place (the throbber is this turning wheel we see when Drupal is waiting for some request to finish). This is just a visual cue that Drupal is working on something. Then, we do our AJAX call and render using jQuery’s very handy load() AJAX function. Yes, this one line makes both the AJAX call and renders the result in place. Handy, very handy jQuery.

Wrapping up

OK, if you covered all those steps, you created a custom path, you made Drupal run your custom code, and you added your own template and preprocess file, so you’re not only rendering HTML the way you want, but you’re also making it easy to maintain. You then added your own JS and CSS code to an admin page to  make it work the way you want.

Most important, you made your content producers’ work much easier. That’s what you’re there for.

How’s that for a day?

X-Team is hiring Drupal developers!
Click here to learn how to join the league of the extraordinary.

KEEP MOVING FORWARD

Ignacio Segura / code