4

I'm trying to build an admin form for a module, using system_settings_form().

I want the users to be able to select sub-options, from a list that is dynamically generated depending on the main option.

For example

$main_options = array('foo','bar');
$sub_options['foo'] = array('foolish','football','foodstuff');
$sub_options['bar'] = array('barley','baroque','barbarian');

$form = array();

$form['main_option'] = array(
    '#type' => 'select',
    '#options' => $main_options,
);

$form['sub_option'] = array(
    '#type'  => 'select',
    '#options' => $sub_options[SOMEHOW USE THE SELECTED VALUE OF main_option AS INDEX],
);

return system_settings_form($form);

So I would expect sub_option to be unpopulated the first time the form is called, and then populated as soon as the user populates main_option.

Is this possible?

Update

Thanks for the advice re Ajax. I looked up https://www.drupal.org/node/752056 and this is what I've built: here the values in olt_test need to change based on the value selected for olt_provider.

  $form['olt_provider'] = array(
    '#type' => 'select',
    '#title' => t('Provider'),
    '#options' => variable_get('providers'),
    '#ajax' => array(
      'callback' => 'olt_provider_select_callback',
      'wrapper' => 'tests-div',
      'method' => 'replace',
      'effect' => 'fade',
    ),
  );

  $form['olt_test'] = array(
    '#type' => 'select',
    '#title' => t('Test'),
    '#prefix' => '<div id="tests-div">',
    '#options' => array(''),
  );

and in olt_provider_select_callback() I have

  function olt_provider_select_callback($form, $form_state) {

  $new_options_value = [new value calculation snipped]  

  $form['olt_test'] = array(
    '#type' => 'select',
    '#title' => t('Test'),
    '#prefix' => '<div id="tests-div">',
    '#options' => $new_options_value,
  );

  return $form['olt_test'];
}

The problem I'm now seeing is that the Ajax only fires on the first time the olt_provider select box is clicked - subsequent clicks don't fire. Any thoughts?

The problem I'm now seeing is that although the olt_test dropdown is correctly updated via Ajax, this replacement value is not saved on form submission. Any thoughts?

Final Update - My form is working correctly now. The data was being saved on form submission, but I needed to add some more code to retrieve the saved data and display the default values when the form was loaded.

1
  • 3
    It is possible, but with Drupal AJAX. See #ajax in the form API.
    – avpaderno
    Commented Aug 28, 2015 at 15:42

3 Answers 3

5

Ajax would be the better way to get precisely what you are asking. One important part of using the form ajax is if you dont tell it which part to rewrite, and which to return, it will rewrite the whole form. It is easy to cause an Inception style recursion problem. This is a pretty good explanation of the ajax.

An alternate, because I always like to provide them, is using form #states. Most straightforward use of states would be if you have one sub_option select element for each of the main_options, or perhaps whole fieldsets.

0
6

As said before you could make use of Form APIs #states to display one element only after a certain input to another form element has been made. In your case you'd maybe go with two additional select lists, both initially hidden. Check form_examples_states_form() for more information.

$form['main'] = array(
  '#type' => 'select',
  // ...
);
$form['sub_a'] = array(
  '#type' => 'select',
  // ...
  '#states' => array(
    'visible' => array(
      ':input[name="main"]' => array('value' => 'foo'),
    ),
  ),
);
$form['sub_b'] = array(
  '#type' => 'select',
  // ...
  '#states' => array(
    'visible' => array(
      ':input[name="main"]' => array('value' => 'bar'),
    ),
  ),
);
3

There is an example of dependent drop-down in the developer examples.

function ajax_example_dependent_dropdown($form, &$form_state) {
  // Get the list of options to populate the first dropdown.
  $options_first = _ajax_example_get_first_dropdown_options();
  // If we have a value for the first dropdown from $form_state['values'] we use
  // this both as the default value for the first dropdown and also as a
  // parameter to pass to the function that retrieves the options for the
  // second dropdown.
  $selected = isset($form_state['values']['dropdown_first']) ? $form_state['values']['dropdown_first'] : key($options_first);

  $form['dropdown_first'] = array(
    '#type' => 'select',
    '#title' => 'Instrument Type',
    '#options' => $options_first,
    '#default_value' => $selected,

    // Bind an ajax callback to the change event (which is the default for the
    // select form type) of the first dropdown. It will replace the second
    // dropdown when rebuilt.
    '#ajax' => array(

      // When 'event' occurs, Drupal will perform an ajax request in the
      // background. Usually the default value is sufficient (eg. change for
      // select elements), but valid values include any jQuery event,
      // most notably 'mousedown', 'blur', and 'submit'.
      // 'event' => 'change',
      'callback' => 'ajax_example_dependent_dropdown_callback',
      'wrapper' => 'dropdown-second-replace',
    ),
  );

  $form['dropdown_second'] = array(
    '#type' => 'select',
    '#title' => $options_first[$selected] . ' ' . t('Instruments'),

    // The entire enclosing div created here gets replaced when dropdown_first
    // is changed.
    '#prefix' => '<div id="dropdown-second-replace">',
    '#suffix' => '</div>',

    // When the form is rebuilt during ajax processing, the $selected variable
    // will now have the new value and so the options will change.
    '#options' => _ajax_example_get_second_dropdown_options($selected),
    '#default_value' => isset($form_state['values']['dropdown_second']) ? $form_state['values']['dropdown_second'] : '',
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

function ajax_example_dependent_dropdown_callback($form, $form_state) {
  return $form['dropdown_second'];
}

The purpose of the code is clear: The second drop-down element is populated of values that depend from the value the user selects from the first drop-down, using the following function.

function _ajax_example_get_second_dropdown_options($key = '') {
  $options = array(
    t('String') => drupal_map_assoc(array(
      t('Violin'),
      t('Viola'),
      t('Cello'),
      t('Double Bass'),
    )),
    t('Woodwind') => drupal_map_assoc(array(
      t('Flute'),
      t('Clarinet'),
      t('Oboe'),
      t('Bassoon'),
    )),
    t('Brass') => drupal_map_assoc(array(
      t('Trumpet'),
      t('Trombone'),
      t('French Horn'),
      t('Euphonium'),
    )),
    t('Percussion') => drupal_map_assoc(array(
      t('Bass Drum'),
      t('Timpani'),
      t('Snare Drum'),
      t('Tambourine'),
    )),
  );
  if (isset($options[$key])) {
    return $options[$key];
  }
  else {
    return array();
  }
}

The main difference with your code is that the AJAX callback just returns the form element that is dynamically changed; it's the form builder that changes the drop-down options. The most common error is changing a form element in the AJAX callback, which is not what Drupal expects it to do.

A note about using #states: For more simple cases, like hidding one or more elements basing on the value of another element, that is the perfect use case. In your case, supposing that the first drop-down element has N different values, using #states would mean having N+1 drop-downs, with just two being visible.
Considering also that each drop-down should have a different element id, the submission handler would be a little complicated to write. Differently, the submission handler for the example I show would just check $form_state['values']['dropdown_first'] and $form_state['values']['dropdown_second']. With the example given by leymannx, you should check the value of $form_state['values']['main'] to understand which of the other drop-down elements would contain the selected value.

0

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.