Take the 2-minute tour ×
Drupal Answers is a question and answer site for Drupal developers and administrators. It's 100% free, no registration required.

I'm trying to use ajax to update the value of a field that can have unlimited values. I am using hook_form_FORM_ID_alter() to add a button to the form with an ajax callback. The problem is that I can not simply set $form['field_name']['language'][0]['value'] because I need to add/remove items from the fields value. So what I've done is change the value on the entity in $form_state and build the form again. The field reloads with the items that I set but when I save the entity only the first new item saves. For example, if the field has no value then only one of the items will save; if the field has one item already then the first two items will be saved; etc.

In my specific example I am using the commerce_product form with a commerce_installments field. However, I have created a simplified example using nodes. I have created a content type called testing with an integer field called field_int_test that has a cardinality of -1 (unlimited). The "Calculate" button I have added to the form uses ajax to generate a random number of items from 2-10 for the field and sets each item's value to a random number. Clicking "Calculate" reloads the field with the new items, but saving exhibits the same behavior as above. I have checked the post and all of the values are being posted back to the web server so I am not sure where the problem is. Also, I feel that there has to be a better way to do this but I have not come up with one.

I have included the code used for the simple node example below. Any help is greatly appreciated.

/**
 * Implements hook_form_FORM_ID_alter().
 */
function mymodule_form_testing_node_form_alter(&$form, &$form_state, $form_id) {
  $form['calculate'] = array(
    '#type' => 'button',
    '#value' => t('Calculate'),
    '#ajax' => array(
      'callback' => 'mymodule_testing_ajax',
      'event' => 'click',
      'wrapper' => 'testing-replace',
      'method' => 'replace',
      'effect' => 'fade',
    ),
    '#weight' => 1,
  );

  $form['field_int_test']['#prefix'] = '<div id="testing-replace">';
  $form['field_int_test']['#suffix'] = '</div>';
}

/**
 * AJAX callback that will generate a random number of items for the
 * field_int_test field wiht random values for each and re-build the form with
 * the new values.
 */
function mymodule_testing_ajax(&$form, &$form_state) {
  $total = mt_rand(2, 10);
  for ($i = 0; $i < $total; ++$i) {
    $form_state['node']->field_int_test['und'][$i]['value'] = mt_rand();
  }

  $form_state = array(
    'build_info' => array(
      'args' => array($form_state['node']),
    ),
    'node' => $form_state['node'],
    'cache' => FALSE,
    'no_cache' => TRUE,
    'input' => array(),
  );
  $form = drupal_build_form('testing_node_form', $form_state);

  return $form['field_int_test'];
}

I have made some changes to the callback function (see below) that now allows the field to be saved but any changes made to the node form after clicking "Calculate" will not be saved; this includes changes to the field being calculated. I have modified the callback to save the new form and form state into new variables, replace $form['field_int_test'] with the value in the new form and update the form's cache.

/**
 * AJAX callback that will generate a random number of items for the
 * field_int_test field wiht random values for each and re-build the form with
 * the new values.
 */
function mymodule_testing_ajax(&$form, &$form_state) {
  $total = mt_rand(2, 10);
  for ($i = 0; $i < $total; ++$i) {
    $form_state['node']->field_int_test['und'][$i]['value'] = mt_rand();
  }

  $field_form_state = array(
    'build_info' => array(
      'args' => array($form_state['node']),
    ),
    'node' => $form_state['node'],
    'cache' => FALSE,
    'no_cache' => TRUE,
    'input' => array(),
  );
  $field_form = drupal_build_form('testing_node_form', $field_form_state);

  $form['field_int_test'] = $field_form['field_int_test'];
  form_set_cache($form['#build_id'], $form, $form_state);

  return $form['field_int_test'];
}
share|improve this question

1 Answer 1

up vote 2 down vote accepted

I found that my problem was both how I was rebuilding the form and when. Instead of doing a full build I needed to call drupal_default_form() to build just the field's form and I had to do it during mymodule_form_testing_node_form_alter() rather than the ajax callback. My completed code is below:

/**
 * Implements hook_form_FORM_ID_alter().
 */
function mymodule_form_testing_node_form_alter(&$form, &$form_state, $form_id) {
  // If this form has been submitted by "Caluculate" button then we need to
  // process the form to calculate and add the integers based on the user's
  // input.
  if (isset($form_state['triggering_element'])
    && $form_state['triggering_element']['#value'] == 'Calculate') {
    _mymodule_calculate_process_form($form, $form_state);
  }

  $form['calculate'] = array(
    '#type' => 'button',
    '#value' => t('Calculate'),
    '#ajax' => array(
      'callback' => 'mymodule_testing_ajax',
      'event' => 'click',
      'wrapper' => 'testing-replace',
      'method' => 'replace',
      'effect' => 'fade',
    ),
    '#weight' => 1,
  );

  $form['field_int_test']['#prefix'] = '<div id="testing-replace">';
  $form['field_int_test']['#suffix'] = '</div>';
}

/**
 * AJAX callback to reload the integer test field with calculated values.
 */
function mymodule_testing_ajax(&$form, &$form_state) {
  return $form['field_int_test'];
}

/**
 * Process a form submitted via AJAX to calculate the value of the integer test
 * field.
 */
function _mymodule_calculate_process_form(&$form, &$form_state) {
  // We don't care about the installment field values that were submitted with
  // the POST.
  unset($form_state['values']['field_int_test']);
  unset($form_state['input']['field_int_test']);

  $items = array();
  $total = mt_rand(2, 10);
  for ($i = 0; $i < $total; ++$i) {
    $items[$i]['value'] = mt_rand();
    $form_state['node']->field_int_test['und'][$i]['value'] = $items[$i];
  }

  // Update the items count for the field.
  $form_state['field']['field_int_test']['und']['items_count'] = count($items);

  // Build the field form with the new values.
  $language = $form['language']['#value'];
  $field = $form_state['field']['field_int_test'][$language]['field'];
  $instance = $form_state['field']['field_int_test'][$language]['instance'];
  $field_form = field_default_form('node', $form_state['node'], $field,
    $instance, $language, $items, $form, $form_state);

  // Replace the field form that was built previously with the new one.
  dpm($field_form['field_int_test']);
  $form['field_int_test'] = $field_form['field_int_test'];
}

I will refrain from accepting my own answer for the time being, in case somebody else has a better solution.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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