12

I have a module that updates a node via ajax when a link is clicked.

The link is a toggle, it should update the node with value 1 on first click, then with value 0 on a subsequent click, etc. Like turning something on/off.

The code below works on the first click after page load, but not on subsequent clicks. I believe Drupal.attachBehaviors has to be called/triggered after each clicks, but I can't figure out how to do this.

  1. The Module

    function mymodule_menu() {
      $items['mypath/%/%/ajax'] = array(
      'title' => 'My title',
      'page callback' => 'mymodule_ajax_callback',
      'page arguments' => array(1,2),
      'access arguments' => array('access content'),
      'type' => MENU_CALLBACK,
      );
    
      ...
    }
    
    function mymodule_ajax_callback($id, $status) {
      //Validation[...]
    
      //Node Update using $id as the nid and $status as the field value[...]
    
      // Define a new array to hold our AJAX commands.
      $ajax_commands = array();
    
      // Create a new AJAX command that replaces the #div.
      $replacedivid = '#status'.$id;
      $replacestring = '<div id="status'.$id.'"><a data-url="'.base_path().'mypath/'.$id.'/'.$new_status.'/ajax" title="This item is marked as '.$status_text.'" id="statuslink'.$id.'" class="midui">'.$status_text.'</a></div>';
    
    
      $ajax_commands[] = ajax_command_replace($replacedivid, $replacestring);
    
    
      return drupal_json_output($ajax_commands);
    }
    
  2. Javascript

    (function ($) {
      Drupal.behaviors.mymodule = {
        attach: function(context, settings) {
          var $uilink = $('.midui'); //find all links
    
          for (var i=0;i<$uilink.length;i++) { //Loop
            var $link = $('#' + $uilink[i].id);
            if (!$link.hasClass("middone")) {
    
              new Drupal.ajax('#' + $uilink[i].id, $link, {
                url: $link.attr('data-url'),
                effect: 'fade',
                settings: {},
                progress: {
                  type: 'throbber'
                },
                event: 'click tap'
              });
    
              $link.addClass("middone"); //add class when we're done
    
            }
          }
        }
      }
    })(jQuery);
    
  3. What I have tried so far:

(a)Add an ajax_command_invoke(NULL, 'mymodule'); to the $ajax_commands array coupled with a $.fn.mymodule function

(b)Add $('body').ajaxSuccess(Drupal.attachBehaviors); to my javascript. ajaxComplete tried as well. Tried it on the document as well.

(c)Create a custom command as detailed here http://www.jaypan.com/tutorial/calling-function-after-ajax-event-drupal-7

Note : I know that it's just a matter of triggering attachBehaviors after each click to 'ajaxify' the new html being inserted/modified. If I click the link and then type Drupal.attachBehaviors() in the console, the link will be processed again by my javascript, as evidenced by the addition of the 'middone' class, and can be clicked again.

Note: Also of interest, if I leave the $ajax_commands empty and return it (empty array) at the end of the callback function, the link will remain clickable on the first and subsequent clicks. It will have the functionality that I am looking for (a toggle). However, since no change is made to the html after each click, there is no way for the user to know if the toggle is on or off.

Any pointers would be greatly appreciated.

=======================================================

A partial Answer :

Drupal ajax.js success function only re-attaches behaviors for forms (I think ?)

    if (this.form) {
      var settings = this.settings || Drupal.settings;
      Drupal.attachBehaviors(this.form, settings);
    }

so I decided to hack all my ajax objects' success function.

The Javascript now becomes

    (function ($) {
      Drupal.behaviors.mymodule = {
        attach: function(context, settings) {
          var $uilink = $('.midui'); //find all links

          for (var i=0;i<$uilink.length;i++) { //Loop
            var $link = $('#' + $uilink[i].id);
            if (!$link.hasClass("middone")) {

              myAjax = new Drupal.ajax('#' + $uilink[i].id, $link, {
                url: $link.attr('data-url'),
                effect: 'fade',
                settings: {},
                progress: {
                  type: 'throbber'
                },
                event: 'click tap'
              });

              myAjax.options.success = function (response, status) {
                //Trigger Attach Behaviors
                setTimeout(function(){Drupal.attachBehaviors($(myAjax.selector))}, 0);
                // Sanity check for browser support (object expected).
                // When using iFrame uploads, responses must be returned as a string.
                if (typeof response == 'string') {
                  response = $.parseJSON(response);
                }

                return myAjax.success(response, status);
              }

              $link.addClass("middone"); //add class when we're done

            }
          }
        }
      }
    })(jQuery);

The success function is a copy paste of the default from ajax.js with an added line for reattaching behaviors. For some reason, Drupal.attachBehaviors has to be within a timer. I can't just have it on its own for a reason that I ignore.

I'll leave this question open for few in case someone can either find a more elegant solution and/or explain the timer oddity.

Many thanks

3
  • Why would you want to attach behaviors again and again? they are automatically re-run on any new element created using AJAX, so why it's not enough?
    – Mołot
    Dec 10, 2013 at 21:51
  • behaviors are not automatically reattched in my case. thx
    – JSL
    Dec 10, 2013 at 22:01
  • While slightly off topic, this is a good example of how to use a custom JS solution, while maintaining support for Drupal core ajax handling (e.g. commands). For example, making an ajax call on page load (no UI trigger) that returns Drupal commands in the response.
    – bfuzze
    Sep 28, 2020 at 22:49

3 Answers 3

3

It can be tricky attaching ajax behaviours to content that is returned from an ajax request itself. However, it is possible.

Although your hook_menu code snippet looks incomplete, assuming it's correct (returns $items and function is closed) - In your case, you may just need to adjust the delivery callback to 'ajax_deliver'

i.e:

/**
 * Implements hook_menu
 */
function mymodule_menu() {

  $items['mypath/%/%/ajax'] = array(
    'title' => 'My title',
    'page callback' => 'mymodule_ajax_callback',
    'page arguments' => array(1,2),
    'access arguments' => array('access content'),
    'delivery callback' => 'ajax_deliver',
    'theme callback' => 'ajax_base_page_theme',
    'type' => MENU_CALLBACK,
  );

  return $items;

}
1
  • Thanks for the suggestion but this does not work either. I use your code in hook_menu and behaviors are not re-attaching automatically.
    – JSL
    Dec 12, 2013 at 20:09
2

I can't comment first answer, but you can set property for colorbox in settings. For example:

myAjax = new Drupal.ajax('#' + $uilink[i].id, $link, {
  url: $link.attr('data-url'),
  effect: 'fade',
  settings: {
    colorbox: {
      mobiledetect: false
    }
  },
  progress: {
    type: 'throbber'
  },
  event: 'click tap'
});
2

After some debugging, I realized the issue was not with my code.

The problem was with another module, in my case the colorbox module, which was the source of a js error in its own behaviors function. I believe the error caused the attach behaviors process to halt, and as such, my own behaviors function was not re-attaching. The error could be seen in the console.

The colorbox error : in 7.24

Uncaught TypeError: Cannot read property 'transition' of undefined colorbox_inline.js?

and in 7.25

Uncaught TypeError: Cannot read property 'mobiledetect' of undefined

My solution was to disable the colorbox module.

Many thanks to everyone who helped.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.