I have an AJAX form. I'm using drupal_add_js() after AJAX calls to update the settings and pass new information to my Drupal.behaviour object. However, the results are being merged instead of replaced. I know this is by design but it's messing up my Javascript code. Does anyone know of a good workaround for this?

Comments

tce’s picture

The current workaround I'm using is overriding (just for that page) Drupal.ajax.prototype.commands.settings in ajax.js so settings gets replaced rather than merged.

Drupal.ajax.prototype.commands.settings = function (ajax, response, status) {
  ajax.settings = response.settings;
};

If anyone has a better way, please let me know.

jaypan’s picture

it's messing up my Javascript code

Are we supposed to guess what you mean by this?

Contact me to contract me for D7 -> D10/11 migrations.

tce’s picture

Not to worry Jaypan. I'm marking this as solved.

uberhacker’s picture

This is definitely an interesting topic and the same issue cropped up on me while using ajax to render Google maps. In my case, if I rendered a map with more than one marker and then tried to render a map with only one marker, the other markers showed up on the map that should only have one marker. Definitely not the desired result. I fixed this by adding the following lines with a '+' character in the first column under the settings section of ajax.js below:

/**
 * Provide a series of commands that the server can request the client perform.
 */
Drupal.ajax.prototype.commands = {
...
  /**
   * Command to set the settings that will be used for other commands in this response.
   */
  settings: function (ajax, response, status) {
    if (response.merge) {
      $.extend(true, Drupal.settings, response.settings);
+      if (Drupal.settings.gmap && response.settings.gmap) {
+        Drupal.settings.gmap = response.settings.gmap;
+      }
   }
    else {
      ajax.settings = response.settings;
    }
  },
...

 

While I shamefully admit this is very hackish since I'm modifying a core file, I have yet to figure out a more elegant way. It seems to me there should be a way to replace, rather than simply merge, Drupal.settings.

jaypan’s picture

Settings are merged on purpose. To ensure you aren't overrwriting things, you should use $.once(). This will ensure that code is only run once on a given setting.

Contact me to contract me for D7 -> D10/11 migrations.

tce’s picture

I think if there was ever a case where you didn't want the settings merged, you could use ajax_command_settings()

https://api.drupal.org/api/drupal/includes%21ajax.inc/function/ajax_comm...

$merge: Whether or not the passed settings in $argument should be merged into the global Drupal.settings on the page. By default (FALSE), the settings that are passed to Drupal.attachBehaviors will not include the global Drupal.settings.

I recall having a similar problem with Google maps where when I updated my object with new markers, it was merging with old markers so using ajax_command_settings() was the way I ensured that only new markers were passed to JS via Drupal.settings.

uberhacker’s picture

"Settings are merged on purpose." This is a narrow view of the issue. Sometimes when settings get merged instead of replaced, everything is overwrote correctly but then you have residual data that should be removed. In my case, I had extra map markers on my Google map display.

uberhacker’s picture

@tce Thanks for the suggestion, however, that doesn't work for me either. Apparently, the $merge value is never sent to ajax.js as FALSE. The response.merge value is always TRUE. Even if I explicitly set the value to FALSE or comment out all the merge lines so that only the ajax.settings = response.settings line exists as you suggested in your first comment, it still won't work (meaning the map won't render at all). Do you have any other possible suggestions or maybe I'm just not calling ajax_command_settings correctly? Do you mind sharing your code on how you solved the Google map marker issue?

tce’s picture

I would do something like this:

<?php
function mymodule_form_callback($form, &$form_state) {
  $commands = array();
  $js = drupal_add_js();
  $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']);
  $form['#prefix'] .= theme('status_messages');
  $output = drupal_render($form);
  $commands[] = ajax_command_replace(NULL, $output, $settings);
  // The setting command here
  $commands[] = ajax_command_settings(array(
    'key1' => 'value1',
    'key2' => 'value2',
  ));

  return array(
    '#type'     => 'ajax',
    '#commands' => $commands
  );
}
?>
uberhacker’s picture

@tce Thanks for sharing. I don't understand the $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']); line. Why not just use $settings = $js['settings']['data']? What arrays are you merging? I tried a variation of your code in my callback function without success as shown below:

  if ($type == 'ajax') {
    $gmap = array();
    $js = drupal_add_js();
    $settings = $js['settings']['data'];
    foreach ($settings as $setting) {
      if (isset($setting['gmap'])) {
        $gmap = $setting['gmap'];
        break;
      }
    }
    $commands = array();
    $commands[] = ajax_command_replace('#locations', '<div id="locations">' . $locations . '</div>', $gmap);
    if (!empty($gmap)) {
      $commands[] = ajax_command_settings(array(
        'gmap' => $gmap,
      ));
    }
    $page = array(
      '#type' => 'ajax',
      '#commands' => $commands,
    );
    ajax_deliver($page);
}

 

Is there anything I missed here?
Thanks

jaypan’s picture

What does your code look like, that you are using to generate the maps?

Contact me to contract me for D7 -> D10/11 migrations.

jaypan’s picture

That's what the detach method of Drupal.behaviors, and the $.once() method are for, so you don't end up with this 'residual' behaviors.

Contact me to contract me for D7 -> D10/11 migrations.

RAWDESK’s picture

After struggling for a while with a similar failure behaviour to get altered settings from back- to frontend, I found this method working best for me. :

Backend ajax (load more) callback :

     // Create result list to append to existing content.
      $append_list = array_slice($list, $values['pager_start_idx'], NULL, TRUE);
      $overview = [
        '#type' => 'container',
        'list' => [
          '#theme' => 'my_theme_list_container_append',
          '#article_list' => !empty($append_list) ? array_keys($append_list) : [],
          '#date' => $values['select_date'],
        ],
      ];

      // Build commands array for load more.
      $commands = [
        ajax_command_append('#article-container-content', drupal_render($overview)),
        [
          'command' => 'ArticlesLoadMoreSettings',
          'list' => $list,
          'pager_status' => $pager_status,
          'pager_idx' =>  $pager_status == 'partial' ? $pager_current_idx : NULL,
        ],
      ];

  $page = ['#type' => 'ajax', '#commands' => $commands];
  ajax_deliver($page);

Frontend wise :

   /**
   * Ajax command: Capture altered settings 
   */
  Drupal.ajax.prototype.commands.ArticlesLoadMoreSettings = function(ajax, response, status) {
    Drupal.settings.articles_filter.list = response.list;
    Drupal.settings.articles_filter.pager_status = response.pager_status;
    Drupal.settings.articles_filter.pager_idx = response.pager_idx;

    // Other stuff to arrange based on altered settings
  };
joelstein’s picture

If you just need to preserve the idempotency of a single key (perhaps from a custom module), then do something like this:

var originalSettingsCommand = Drupal.ajax.prototype.commands.settings;

Drupal.ajax.prototype.commands.settings = function(ajax, response, status) {
  originalSettingsCommand(ajax, response, status);
  if ('custom_module_key' in response.settings) {
    Drupal.settings.custom_module_key = response.settings.custom_module_key;
  }
};