I'm having an issue with a custom module I'm developing and was wondering if someone could help. I've attached a stripped down version of what I'm trying to do in my module to keep things as clear as possible.

The problem is with a ctools modal button on a page - created with the ctools_modal_text_button function:

$button = ctools_modal_text_button('Test', 'eludom/nojs/test', '', 'button thebutton');

The "test" at the end of the url on that link is a parameter to pass to the modal, which will then set the default value for a textfield in the modal to "test". This bit works and sets the value of the textfield in the modal popup when you click the button:

Modal

My issue is that I then take the value of the textfield (update it from "test" to say "foo") and when the submit button is clicked it updates the link's href attribute via an ajax_command_invoke in the callback:

<?php
$output[] = ajax_command_invoke('.thebutton', 'attr', array(
  array('href' => 'eludom/nojs/' . $form_state['input']['changeit'])
));
?>

and this works as you can see from browser inspect:

Foo

The problem is that even though the href on the link has changed if you click the test button again the modal opens with the original value of "test" in the textfield, and the dpm() I have on there confirms that it is getting "test" as the callback parameter rather than "foo".

I've tried removing the processed classes on submit, wrote my own process function (see the Javascript file in the module), tried various binds/unbinds to try and overwrite what ctools is doing on click and even changed the ajax command to replace the button completely, but this breaks the button and it links to the nojs version as the behavior doesn't apply on replace.

Anybody know how I could get this working?

CommentFileSizeAuthor
eludom.zip1.91 KBChris Graham
foo.png26.52 KBChris Graham
test.png23.3 KBChris Graham
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Chris Graham created an issue. See original summary.

Chris Graham’s picture

OK, so I don't know the implications of this as it seems a bit hacky from my perspective but I did manage to unbind the ctools click handler and bind my own. Just in case anyone else stumbles onto this, this is how I got it working.

I did the unbinds/bind in my behavior:

// Unbind the default ctools click handler
$('a.ctools-use-modal').unbind('click');
$('a.ctools-use-modal-processed').unbind('click');

// Bind the new click handler
$('.thebutton').bind('click', foo);

Then I wrote my bind function (outside the behavior):

// Click bind function to open modal
    var foo = function(event) {
    event.preventDefault();

    if ($(this).hasClass('ctools-ajaxing')) {
      return false;
    }

    var old_url = $(this).attr('href');
    $(this).addClass('ctools-ajaxing');

    var ajax = {};
    ajax.element = $(this);

    try {
      url = old_url.replace(/\/nojs(\/|$)/g, '/ajax$1');
      $.ajax({
        type: "POST",
        url: url,
        data: { 'js': 1, 'ctools_ajax': 1 },
        global: true,
        success: function(data) {
          Drupal.CTools.Modal.modal_display(ajax, data[1]);
        },
        error: function(xhr) {
          Drupal.CTools.AJAX.handleErrors(xhr, url);
        },
        complete: function() {
          $('.ctools-ajaxing').removeClass('ctools-ajaxing');
        },
        dataType: 'json'
      });
    }
    catch (err) {
      alert("An error occurred while attempting to process " + url);
      $('.ctools-ajaxing').removeClass('ctools-ajaxing');
      return false;
    }

    return false;
  }

The reason that I find it a bit hacky is that Drupal.CTools.Modal.modal_display() call in the success function of the ajax call. I noticed from doing a console.log on the response data that 2 objects were returned structured like so (stripped down that output to make it readable):

[Object, Object]
0: Object
  command: "settings"
  merge: true
  settings: Object
1: Object
  command: "modal_display"
  output: "  <form action=......> ........ </form>  "
  title: "Test"

I basically ignored the settings command as I didn't know what to merge it with and used the second object as the response parameter on modal_display(). I get the feeling that there should be a way of invoking both commands automagically but I couldn't what to use in the CTools modal.js so... it works, but take it with a pinch of salt if you are using it.

kwfinken’s picture

I am experiencing a similar thing. It seems like this should be changed from a support request to a bug report:

My Drupal form has a dynamically generated (select) field via ajax (works fine). When a user presses the browse index button, it opens a modal dialog for the person to search the index (works fine). I want to pass information from the field to the modal. If I change the value of the field and then click browse, it gives me the following error:

    An AJAX HTTP error occurred.
    HTTP Result Code: 200
    Debugging information follows.
    Path:
    /tgif/search/ajax/BrowseIndex/edit-searchitems-3-searchtest/SUB3
    StatusText: OK
    ResponseText:

If I dont' change the field's value, the modal works fine. If I change it and then change it back to the default, I get the error. If I change it and then add or remove a copy of the field, then the modal works fine.

Sorry to add so much code, but I am at a loss.

    function msul_tgif_xml_browse_index($ajax, $element,$searchText, $indextype) {
      if ($ajax) {
        ctools_include('ajax');
        ctools_include('modal');

        $form_state = [
          'ajax' => TRUE,
          'title' => t('Browse Index'),
        ];
        $form_state['build_info']['args'] = [
          'ajax' => $ajax,
          'indexType' => $indextype,
          'element' => $element,
          'searchText'=>$searchText,
        ];

        $output = ctools_modal_form_wrapper('msul_tgif_xml_index_form', $form_state);

        if (!empty($form_state['ajax_commands'])) {
          $output = $form_state['ajax_commands'];
        }

        print ajax_render($output);
        drupal_exit();
      }
      else {
        drupal_get_form('msul_tgif_xml_index_form');
      }
    }

    function msul_tgif_xml_index_form($form, &$form_state) {
      $indexTypeOptions = [
        'SUB3' => 'Keyword',
        'A' => 'Author',
        'FACW' => 'Facility',
       ];

      $form = [
        'searcharea' => [
          '#type' => 'container',
          '#attributes' => ['class' => ['container-inline']],
          'IndexType' => [
            '#type' => 'select',
            '#options' => $indexTypeOptions,
            '#required' => TRUE,
            '#empty_option' => t('Select from List'),
            '#title' => t('Index Type'),
            '#default_value' =>
            isset($form_state['values']['IndexType']) ?
                $form_state['values']['IndexType'] :
                ( in_array($form_state['build_info']['args']['indexType']
                    , $indexTypeOptions) ?
                    $form_state['build_info']['args']['indexType'] :
                    'SUB3'
                ),
          ],
        ],
        'submit' => [
          '#type' => 'submit',
          '#value' => t('Display Index'),
          '#name' => 'displayIndex',
        ],
      ];

      return $form;
    }
    function msul_tgif_xml_guided_search_form_submit($form, &$form_state) {
      $form_state['rebuild'] = TRUE;
    }
    function msul_tgif_xml_guided_search_form($form, &$form_state) {
      ctools_include('modal');
      ctools_modal_add_js();

      $form['searchItems'] = [
        '#prefix' => '<div id="guided_search_fields">',
        '#suffix' => '</div>',
        '#type' => 'container',
        '#tree' => TRUE,
      ];

      if (empty($form_state['storage']['num_search_items'])) {
        $form_state['storage']['num_search_items'] = 1;
      }

      for ($i = 0; $i < $form_state['storage']['num_search_items']; $i++) {
        $form['searchItems'][$i] = [
          '#type' => 'container',
          '#attributes' => ['class' => ['container-inline']]];

        $form['searchItems'][$i]['BrowseIndexURL'] = [
          '#type' => 'hidden',
          '#value' => "/tgif/search/nojs/BrowseIndex/edit-searchitems-$i-searchtext",
          '#attributes' => [
            'class' => [
              'edit-searchitems-' . $i . '-url',
            ],
          ],
        ];

        $form['searchItems'][$i]['SearchField'] = [
          '#type' => 'select',
          '#title' => 'as a',
          '#options' => [
            'SUB3' => 'Keyword',
            'A' => 'Author',
            'FACW' => 'Facility',
          ],
          '#attributes' => [
            'class' => [
              'edit-searchitems-' . $i . '-url',
            ],
          ],
          '#default_value' =>
          isset($form_state['values']['searchItems'][$i]['SearchField']) ?
              $form_state['values']['searchItems'][$i]['SearchField'] :
              'BITEXT',
        ];
        $form['searchItems'][$i]['BrowseIndex'] = [
          '#type' => 'submit',
          '#value' => t('Browse Index'),
          '#limit_validation_errors' => [],
          '#attributes' => [
            'class' => [
              'ctools-use-modal'
            ],
          ],
          '#id' => 'edit-searchitems-' . $i,
        ];
      }

      $form = array_merge($form, [
        'RemoveRow' => [
          '#type' => 'submit',
          '#submit' => array('msul_tgif_xml_guided_search_ajax_remove_last_one'),
          '#value' => t('Remove last row'),
          '#limit_validation_errors' => [],
          '#attributes' => [
            'style' => 'float:right',
          ],
          '#ajax' => [
            'wrapper' => 'guided_search_fields',
            'callback' => 'msul_tgif_xml_guided_search_ajax_callback',
          ],
        ],
        'AddRow' => [
          '#type' => 'submit',
          '#submit' => array('msul_tgif_xml_guided_search_ajax_add_one_more'),
          '#value' => t('Add search row'),
          '#limit_validation_errors' => [],
          '#attributes' => [
            'style' => 'float:right',
          ],
          '#ajax' => [
            'wrapper' => 'guided_search_fields',
            'callback' => 'msul_tgif_xml_guided_search_ajax_callback',
          ]
        ],
        'Submit' => [
          '#type' => 'submit',
          '#value' => t('Search'),
        ],
        'Clear' => [
          '#type' => 'button',
          '#button_type' => 'submit',
          '#value' => t('Clear'),
          '#limit_validation_errors' => [],
          '#attributes' => [
            'onclick' => 'this.form.reset(); return false;',
          ],
        ]
      ]);

      return $form;
    }
    function msul_tgif_xml_guided_search_ajax_callback($form, $form_state) {
      return $form['searchItems'];
    }
    function msul_tgif_xml_guided_search_ajax_add_one_more($form, &$form_state) {
      $form_state['storage']['num_search_items'] ++;
      $form_state['rebuild'] = TRUE;
    }
    function msul_tgif_xml_guided_search_ajax_remove_last_one($form, &$form_state) {
      $form_state['storage']['num_search_items'] --;
      $form_state['rebuild'] = TRUE;
    }
kwfinken’s picture

Category: Support request » Bug report
bleen’s picture

FWIW it seems that this same issue made its way into the core modal behavior in D8 ...

open.source.developer’s picture

I have found solution for updating the Ctools link initial URL with javascript. Below is the code and explanation

$.fn.update_service_link = function (point_id) {
     var link = $(".drop-off-radio-myshipment a#service-point-link").attr('href');
     link_parts = link.split('/');
     link_parts[7] = point_id;
     var new_link = link_parts.join('/');
     $(".drop-off-radio-myshipment a#service-point-link").attr('href', new_link);

          
      if (new_link != link) {

          if (typeof Drupal.ajax[link] != 'undefined') {
              Drupal.ajax[link].options.beforeSend = function (xmlhttprequest, options) {
                  var new_link = $(".drop-off-radio-myshipment a#service-point-link").attr('href');
                   var ajax_link = new_link.replace("nojs", "ajax");
                   options.url = ajax_link;
              }

          }
     }
}

I am calling above JS function using ajax_command_invoke

As seen in the above JS function in first 5 lines I read the existing link href and append it with new value as required and update the href attribute on the anchor tag.

Now to modify the actual ajax url which ctools modal window calls while initializing is modified using beforeSend event

The beforeSend event callback is registered on Drupal.ajax[link] where link is the inital modal url, if any doubts you can console.log this to check that you are on right track.

Finally inside the beforeSend callback update options.url with the new link

This worked for me

guschilds’s picture

As mentioned in #5, this behavior is in Drupal 8 core.

I experienced it when altering the URL for the "Browse media" button in the new Media Library widget (I was using JS to change the Media Library's exposed filter options in the URL based on the value of another field).

The reason it happens is because core's Drupal.ajax.bindAjaxLinks() adds an event listener to such a button that doesn't react to any future changes to the button's URL. This can be overcome by removing what that function adds (jquery.once('ajax') and the click handler) and then calling it again. An example:

$browseButton.removeOnce('ajax').off('click').attr('href', newButtonUrl);
Drupal.ajax.bindAjaxLinks($subform);

Hope that helps the next person!

imclean’s picture

Thanks @guschilds, that works a treat. In my case the DOM was being updated with VueJS but the URLs weren't changing. Drupal just lost track of which links referred to which page so the links looked correct but the wrong page opened in the modal. To just refresh the binding you can use this:

$('#myelement').find('.use-ajax').removeOnce('ajax').off('click').attr('href', function (i, val) {
  return val;
});
imclean’s picture

To add some information to #8, if you're populating a page with Vuejs and the content includes ajax modal links, the standard ajax link binding won't work for initial page load either. In that case, add the following to your attach behaviour:

      $('body').bind('DOMSubtreeModified', function () {
        Drupal.ajax.bindAjaxLinks(document.body);
      });