Wednesday 25 July 2012

Form API and AJAX callbacks


Here's a piece of information that's well hidden.

If you're using Drupal's AJAX functionality with forms - but you don't actually want to change the form but instead do other stuff on the page, you may run into trouble because there's something that's not clearly explained about the AJAX callback function in your PHP code.

You know you can add this:

$form['tickbox'] = array(
  '#type' => 'checkbox',
  '#title' => t('Change something on the page'),
  '#ajax' => array(
    'wrapper' => 'id-of-some-div-on-the-page',
    'callback' => 'mymodule_form_ajax_callback',
  ),
);

And when you click the box your function in the PHP gets called:

function mymodule_form_ajax_callback($form, $form_state) {
  return '<div id="">Hello!</div>';
}

Okay that just does a straight replace. But what if you want to append it instead? The documentation says that this function can return AJAX commands instead. So I can do this, right?

function mymodule_form_ajax_callback($form, $form_state) {
  return ajax_command_append(NULL, 'Hello!');
}

Nope. The documentation says I can return an array of commands. So I can do this, right?

function mymodule_form_ajax_callback($form, $form_state) {
  return array(
    ajax_command_prepend(NULL, 'Hello!'),
    ajax_command_append(NULL, 'Goodbye!'),
  );
}

Nope.

The answer is hidden around line 219 of includes/ajax.inc, this will work:

function mymodule_form_ajax_callback($form, $form_state) {
  return array(
    '#type' => 'ajax',
    '#commands => array(
      ajax_command_prepend(NULL, 'Hello!'),
      ajax_command_append(NULL, 'Goodbye!'),
    ),
  );
}

It needs to be a renderable array so this is what works.

Sunday 22 July 2012

jQuery publish/subscribe custom events

There seems to be a stuck idea with respect to jQuery which demands binding custom events and their functions to a specific DOM object (like 'document' or 'body') and triggering the event on that object which then tells whatever objects might want to know about the event using another event.


There's an example of this here: http://stackoverflow.com/questions/399867/custom-events-in-jquery and another here http://jamiethompson.co.uk/web/2008/06/17/publish-subscribe-with-jquery/ (okay, that's four years ago but it's top of the Google results on this subject).


I may be being stupid (it's been known) but that seems a completely unnecessary step - possibly inherited from OOP coding in a non-HTML environment where it may be necessary to have two stages.


So, here I am building a new carousel system for Drupal 7 - not because I'm a glutton for punishment but because none of the existing options does what I need for my current contract - and come up against this issue and something is nagging me. I've been here before.


If you imagine, a modern carousel has its little indicator buttons to show which slide we're currently on and allow the user to select a slide to view with a click. It also has forward and back arrows (which may be hidden or displayed if there's a slide to go forward or back to). And there may be an auto-change option if the user isn't selecting slides manually.


Now each of those indicators, arrows and auto-scroll items is an object which has behaviours attached. Let's call them carousel "tools". And they need to know what's going on.


Let's say the last indicator is clicked by the user, the system must scroll to the last item and then a check must be made to see if the "next" arrow should be hidden, the old indicator unhighlighted, the new indicator highlighted and the auto-scroll switched off (maybe with another timer started so the auto-scroll restarts after a period of time of user inactivity).


Or, if it's the auto-scroll in action, similar actions must be taken when a new slide is displayed.


Now you could hard-code all this into the slide function but we all know that's naughty tight coupling and will be difficult to write without lots of bugs and to maintain for anyone else. Since each object has its own behaviours this problem is ripe for proper OOP implementation.


So we could encapsulate the tools and then keep a list of those tools and hard-code a function to call in each one of them when the slide changes. Okay, that's better functionality, looser coupling but it can be done better.


Instead what we intuitively want to do is send a custom event saying "this carousel has changed its slide" to every object that needs to know (and don't forget we might have more than one carousel on a page so we also need to distinguish between the tools belonging to each carousel).


Okay, so we could bind a function for the "slideChange" event to the root carousel DOM element and then have that element triggered by the slideChange function (with data including the old and new slide IDs, plus whether this is the first slide or the last slide in the list - so that the previous/next arrows know whether to hide themselves).


But why do we have to use the root carousel element at all?


We don't. What about this:


$('.carousel-tool').trigger('slideChange.' + myCarouselID, {...slide change data...});


And in the set-up for each tool we can have this:


$(this).bind('slideChange. + myCarouselID, function(e, data) {
  var me = $(this);
  ... process the slideChange event
});


And in the HTML every tool has a "carousel-tool" class. If we do this we are completely encapsulating the actions of the tools. The slideChange function can reference every tool, without actually having to know who they are.


This uses the custom event namespacing feature available in jQuery to ensure that only the tools that belong to a specific carousel have the event triggered when that carousel slide changes. You could namespace the HTML class but that's less efficient in some respects.


Or you could modify the trigger line:


$('#' + myCarouselID).find('.carousel-tool').trigger('slideChange', {...slide change data...});


Actually this is probably the most efficient option even if it's not the most elegant, and note you wouldn't use the namespacing in the binding either if you do it this way.


In case you think that's an odd way to do the selection process, it's quicker to have the single $('#id') search on its own and then do a find() from there, than it is to combine to two. (See http://24ways.org/2011/your-jquery-now-with-less-suck.)


So there you are: how to do publish/subscribe properly with jQuery. (In my opinion.)


UPDATE: One caveat, the events propagate up through the DOM tree, which means that if you have a handler for a custom event higher up the tree it will get called as many times as there are handlers lower down the tree. You can avoid this using the e.stopPropagation() method.



Monday 9 July 2012

Search API, Facet API and Display Suite

Just spent most of the day tracking down a nasty little issue involving these three modules.

Let's face it Search API with Facet API (and all the Search API support modules) are brilliant, the core Search is very difficult to customise and you usually have to end up with nasty core hacks if you want to do anything clever.

Search API on the other hand is lovely, and Facet API is just great with almost everything extendible. And, of course, you can display search results as a view, which adds that level of delightfulness.

Display Suite is also awesome (I may have already mentioned this).

But if you have all three together - displaying search results and facet blocks on a Display Suite configured page, you may run into the problem of the facet blocks refusing to appear.

The reason is really simple: there are no facets to display.

But you say (well, I screamed in my head) I'm displaying search results through a view, I'm looking at them right now and I know they have facets.

I finally, eventually, lit upon this issue in Facet API http://drupal.org/node/1392288 which explains the problem but doesn't come up with any solid solution. The issue is that if the page displays the blocks before the search query has been run they will be empty because search results are needed before the facets can be calculated.

Simple? Yes. Solvable? Not easily. There's no way of telling Display Suite what order you want the blocks and content displayed (it would be a nice touch but not worth for just this problem, or maybe a way to defer content rendering of blocks to the end). There is talk of having the facet run the query if it's not been run, but that's in the future if it ever happens.

The solution is legal but ugly.

The way to ensure the query is run first is to do it in hook_init() in my case by rendering the view and saving it. Then adding the view to the output when required. Yucky but it works.

UPDATE: Typical really, what I hadn't done is fully explored the DS Extras module which allows Views pages to be configured. This is very handy but the issue above still remains.

Wednesday 4 July 2012

Changing view_mode mid-stream

In my current contract I have to display a hierarchy of complex taxonomy terms - each one as a page with relevant data hanging off it. There are three levels to the taxonomy: Level 1 displays one set of data and links, Level 2 is never displayed on its own (which is something I'll have to take care of) and Level 3 has a different page structure again.

Different page layouts means Display Suite (http://drupal.org/project/ds) and that's great. It works fine when you want to configure a different layout for a node - as long as it's the same for every node type. Or entity bundle.

DS works fine for taxonomy terms in just the same way as for nodes. It is one of my favourite modules.

But I needed to be able to change the view_mode on the fly: to check what level the taxonomy term is and change the view_mode based on that. Which is when I ran into trouble - and it's not DS's fault. Though I never had to do it apparently this was not a problem in D6 but in D7 there is a bug which makes it tricky to change view_mode. You can read all about it here: http://drupal.org/node/1154382

This issue gives some hints as to the solution (hook_entity_prepare_view() is not it), but there is a link to this blog here. The solution described  is for changing build mode for nodes based on the current theme (so you can change things if you're using a mobile theme).

My solution is the same except slightly more generic, instead of intercepting hook_node_...() hooks, I intercept hook_entity_...() hooks.

I'm just going to throw my code at you because I know you can work out what to do in your own situation.


/**
 * Implements hook_entity_prepare_view().
 *
 * We have to play silly games to change the view_mode, first we intercept
 * hook_entity_prepare_view() and establish what view_mode we want, and
 * save it. But there's a bug which means this has no effect on the output
 * so...
 */
function page_g2g_entity_prepare_view($entities, $entity_type, $langcode) {
  if ($entity_type!='taxonomy_term') {
    return;
  }


  foreach ($entities as $id => $entity) {
    if ($entity->vocabulary_machine_name!='gtg_tags' || $entity->view_mode!='full') {
      // wrong vocabulary or not a "full page"
      continue;
    }


    // Change the display dependent on the number of parents
    switch (count(taxonomy_get_parents_all($entity->tid))) {

      case 1:

        $entity->view_mode = 'g2g_level_1';
        break;

      case 2:

        // Hm. Need to do something else here, maybe
        // do a redirect to the parent term.
        break;

      case 3:

        $entity->view_mode = 'g2g_level_3';
        break;
    }
  }
}


/**
 * Implements hook_entity_view_alter().
 *
 * ...we intercept before the full build is enacted. You can test and see that
 * even though we changed the view_mode in the term itself, it hasn't transferred
 * to the build theme. So having verified we want to do it with this entity
 * we transfer it. And now it gets changed.
 */
function page_g2g_entity_view_alter(&$build) {
  if (isset($build['#entity_type']) && $build['#entity_type']=='taxonomy_term') {
    $build['#view_mode'] = $build['#term']->view_mode;
  }
}


Arguably you don't need the first hook, you could do it all in the second call. But it's a matter of elegance and splitting actions into their appropriate locations.