I have a custom module that has a few sections that utilize Ajax updates. I also have a "managed_file" field (Form API).
The ajax works fine and updates two dependent fields, but the managed_file loses the form_state after I hit the Upload button.
If I only select the drop-down ajax field, then the $form_state['values'] is updated and retains the values for the fields that exist in the form. However, if I select a file to upload and click on the "Upload" button that is generated by the managed_file element, the $form_state['values'] seems to get reset and the only thing that is present there is the ID of the file that got uploaded. Anything else that was in the $form_state['values'] prior to the upload is lost and the drop-down dependency is broken.

I've put together a quick example (I'm not able to post it here - I'm missing a privilege - I'll try to resolve that). It contains a drop-down box that updates the description of a text field and under that is a managed_file upload field.

Steps to re-create issue:
1. Select an option from the drop-down menu
- you'll see the form_state['values'] be dumped on the screen
2. Enter any text in the textfield
3. Click "Browse" - select a file (.txt file)
4. Then click "Upload"
- you will see the $form_state['values'] be dumped on the screen any it will only contain the file_upload and file_upload_button values (file_upload' => 'SOME_ID_HERE', 'file_upload_upload_button' => 'Upload',)

Any help would be appreciated.
I can use simple "file" to get around this issue, but I was wondering if anyone else had encountered this problem and/or if there is a solution for it.

Comments

chochko84’s picture

I think I managed to answer my own question. I traced the issue I was experiencing to the “file” module, part of the Drupal Core. More specifically to the “file_ajax_upload” and “file_managed_file_process” functions of file.module.
The issue comes down to the way that Ajax requests are processed based on whether they use “PATH” or “CALLBACK” or both.
Here is the code snippet I am referencing (I’ve removed the variable declarations because it’s not letting me post them here for some reason):

ajax_settings = array(
    'path' => 'file/ajax/' . implode('/', element['#array_parents']) . '/' . form['form_build_id']['#value'],
        'wrapper' => element['#id'] . '-ajax-wrapper',
    'effect' => 'fade',
    'progress' => array(
      'type' => element['#progress_indicator'],
      'message' => element['#progress_message'],
    ),
  );

  // Set up the buttons first since we need to check if they were clicked.
  element['upload_button'] = array(
    '#name' => implode('_', element['#parents']) . '_upload_button',
    '#type' => 'submit',
    '#value' => t('Upload'),
    '#validate' => array(),
    '#submit' => array('file_managed_file_submit'),
    '#limit_validation_errors' => array(element['#parents']),
    '#ajax' => ajax_settings,
    '#weight' => -5,
  );

The issue has to do with the sequence of execution on the “Upload” button. The Ajax “path” events get executed first and then the button’s “submit” ('file_managed_file_submit') function gets executed. The issue is that in the ajax PATH execution, the Ajax attempts to get the form state from the cache (ajax_get_form() ). The form_state[‘values’] does not seem to be cached and when other Ajax events take place on the original form, these values are lost when a file upload takes place – i.e. form_state[‘values’] is overwritten. This may be an issue in my setup, but I did try everything in the book to enable proper caching, excluding the manipulation of form_state[‘storage’], which I know would have worked.

The Solution.

Don’t use “PATH” in the ajax event for the upload button, but use “CALLBACK”.
ajax_settings = array(
    'callback' => 'file_ajax_upload_callback',
        'wrapper' => element['#id'] . '-ajax-wrapper',
    'effect' => 'fade',
    'progress' => array(
      'type' => element['#progress_indicator'],
      'message' => element['#progress_message'],
    ),
  );

The 'file_ajax_upload_callback' function is essentially a carbon-copy of the 'file_ajax_upload', with less than 10 lines of code modifications

function file_ajax_upload_callback(form, form_state) {
  form_parents = isset(form_state['triggering_element']['#parents'][0]) ? array(form_state['triggering_element']['#parents'][0]) : array(form_state['triggering_element'] ['#array_parents'][0]);
  form_build_id = form['#build_id'];

if (!isset(form_build_id) || empty(form_build_id)) {
… 
  }
// REMOVED – we already have form and form_state
//  list(form, form_state, form_id, form_build_id, commands) = ajax_get_form();

This modification worked for me. It’s not ideal because it is modifying part of the drupal Core, but it makes sense to preserve the form state throughout the form processing.
P.s. I am not using a multi-step form.

josephcheek’s picture

you can get the element to use a callback instead of a path without hacking core. Add a #process element to your managed_file:

$form['doc1'] = array(
  '#type' => 'managed_file',
  '#process' => array('fix_ajax_upload'),
);

and then use that function to alter the managed_file element:

function fix_ajax_upload($element, &$form_state, $form) {

  // process $element as normal
  $element = file_managed_file_process($element, $form_state, $form);

  // remove path, add callback
  unset($element['upload_button']['#ajax']['path']);
  $element['upload_button']['#ajax']['callback'] = 'file_ajax_upload_callback';

  return $element;

}

Joseph Cheek, Founder
CEO and Drupal Architect
The ZDS Group, Salt Lake City, UT, USA
https://zds.group/
Drupal consulting for media, publishing, and government

Joe Huggans’s picture

.

Joe Huggans’s picture

Thanks for taking the time to explain your fix for this. I can't seem to get this working and it's a nightmare because my client has seen the upload field work previously and now I'm saying they can't have it, I'm explaining there is a bug and they are clueless and now think their website has a virus. But anyway, I have copied the file_ajax_upload function, but not sure if I have made the correct modifications as I am getting an error which states - Notice: Undefined index: #suffix in file_ajax_upload_callback()

Here is the modified function

function file_ajax_upload_callback($form, $form_state) {
  $form_parents = isset($form_state['triggering_element']['#parents'][0]) ? array($form_state['triggering_element']['#parents'][0]) : array($form_state['triggering_element'] ['#array_parents'][0]);
  $form_build_id = $form['#build_id'];

  if (!isset($form_build_id) || empty($form_build_id)) {
    // Invalid request.
    drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error');
    $commands = array();
    $commands[] = ajax_command_replace(NULL, theme('status_messages'));
    return array('#type' => 'ajax', '#commands' => $commands);
  }

  

  if (!$form) {
    // Invalid form_build_id.
    drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error');
    $commands = array();
    $commands[] = ajax_command_replace(NULL, theme('status_messages'));
    return array('#type' => 'ajax', '#commands' => $commands);
  }

  // Get the current element and count the number of files.
  $current_element = $form;
  foreach ($form_parents as $parent) {
    $current_element = $current_element[$parent];
  }
  $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0;

  // Process user input. $form and $form_state are modified in the process.
  drupal_process_form($form['#form_id'], $form, $form_state);

  // Retrieve the element to be rendered.
  foreach ($form_parents as $parent) {
    $form = $form[$parent];
  }

  // Add the special Ajax class if a new file was added.
  if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
    $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
  }
  // Otherwise just add the new content class on a placeholder.
  else {
    $form['#suffix'] .= '<span class="ajax-new-content"></span>';
  }

  $form['#prefix'] .= theme('status_messages');
  $output = drupal_render($form);
  $js = drupal_add_js();
  $settings = drupal_array_merge_deep_array($js['settings']['data']);

  $commands[] = ajax_command_replace(NULL, $output, $settings);
  return array('#type' => 'ajax', '#commands' => $commands);
}
Joe Huggans’s picture

Here is the other part of my code, if anyone can help with this I am willing to pay, please leave a message

/**
 * Implements hook_form_alter().
 */
function form_alter_form_alter(&$form, &$form_state, $form_id) {
    
    if ($form_id === 'commerce_cart_add_to_cart_form_CHQU73IYWMGr6uPDEWPhR9tSmUbATM6TU2DS-v1hkHU') {
    	$form['#after_build'][] = 'form_alter_after_build'; 
    }

}

function form_alter_after_build($form, &$form_state) {
	$form['line_item_fields']['field_upload_images'][LANGUAGE_NONE][0]['#process'][] = 'fix_ajax_upload';

	return $form;
}


function fix_ajax_upload($element, &$form_state, $form) {
  
	$element = file_managed_file_process($element, $form_state, $form);
	unset($element['upload_button']['#ajax']['path']);
	$element['upload_button']['#ajax']['callback'] = 'form_alter_ajax_upload_callback';
	return $element;

}
snarf5’s picture

Hi there,

I am trying to create additional 'add more' button to duplicated/clone certain input fields inside a form. I managed to control it all except for the 'managed_file' field. Although the managed_file 'clones' are working (i.e.: i can click on it to choose file), but those 'clones' are always losing the form_state and when i do a file_load on it, it gave me error. Is there something that i missed out here? maybe i need to bind the clones into a different trigger/function?

Any help would be good here.

Thanks!

timcamps’s picture

I was facing the same issue, so I hacked together some jQuery which solves the problem.

Basically, what I'm doing is calling the Attribute selection again after the Upload button is clicked. This way the image gets uploaded and the attribute selection gets saved.

This results in the following js:
// Track clicks on the Upload button
var fire; // This is used to avoid a continuous loop on the ajaxComplete event
Drupal.behaviors.uploadButtons = {
    attach: function (context) {
    jQuery('button.form-submit', context).bind('mousedown', Drupal.upload.Fired ); // If the Upload button is clicked ...
    }
};
Drupal.upload = Drupal.upload || {
    Fired : function(){
        fire = true; // ... set the event to true
    }
};

Then, on an ajaxComplete event, this code gets fired:

jQuery(document).ajaxComplete(function() {
    
    // Fire the Product Attribute selection again
    if (fire) {
    var parent = jQuery('.form-type-commerce-fancy-attributes-ajax .description .description-selected').parent().parent();
        jQuery('input[type=radio]', parent).click();
        jQuery('input[type=radio]', parent).change();
        fire = false; // Set fire to False to avoid a continuous loop here
    } 
  
});

If anyone cares to optimise my hacked code, be my guest.

In the meantime, I hope this helps you out.