1

I want to implement a Dependent Dropdown for a custom field widget.

I have a custom field with 5 select boxes. Each subsequent dropdown should populate based on values from previous Dropdown. I have a 5 functions that takes arguments to get the values.

I have implemented that, but I need to Ajaxify it, so that I can populate without page refreshes. Any pointers on how to do that?

Basically, I am trying to implement the below thing (from examples module) with FieldWidget.

https://cgit.drupalcode.org/examples/tree/ajax_example/src/Form/DependentDropdown.php

1
  • drupal.stackexchange.com/a/313005/69441 needs very little adjustments to do this. Instead of two fields you would work with $element['property1'] and $element['property2'] but otherwise, no change is necessary. Ie. where you see $element['#prefix'] you write $element['property1']['#prefix'] and instead of $element['value'] you write $element['property2'] and done. The two -- rather small -- after build helpers and the also tiny ajax callback requires no change whatsoever. Sep 19, 2022 at 2:58

2 Answers 2

2

You didn't deserve the downvote: this is a perfectly good question, one that has simply no answer anywhere on the web yet. All of them only speak about dependent dropdowns in forms, not custom fields where we have two extra problems: first, being out of real form context when setting up ourselves, second, potentially being multivalued.

It took me some time to figure it out but here it is. We're extending WidgetBase:

public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
  $id = $this->fieldDefinition->getUniqueIdentifier();
  $wrapper_id = "XXX-$id-$delta-replace";

  $selected_value1 = $items[$delta]->value1 ?? 0;
  $element['value1'] = $element + [
    '#type' => 'select',
    '#options' => $value1_options,
    '#empty_option' => '---',
    '#default_value' => $selected_value1,
    '#weight' => 1,
    '#ajax' => [
      'event' => 'change',
      'method' => 'html',
      'callback' => [$this, 'getValues'],
      'wrapper' => $wrapper_id,
      'progress' => [
        'type' => 'throbber',
        'message' => NULL,
      ],
    ],
  ];

  $selected_value2 = $items[$delta]->value2 ?? 0;
  $element['value2'] = [
    '#type' => 'select',
    '#empty_option' => '---',
    '#default_value' => $selected_value2,
    '#options' => $this->loadOptions($selected_value1),
    '#prefix' => "<div id='{$wrapper_id}'>",
    '#suffix' => '</div>',
    '#validated' => TRUE,
    '#weight' => 2,
  ];

  $element += [
    '#type' => 'container',
    '#attributes' => ['class' => ['container-inline']],
  ];

  return $element;
}

Fill $value1_options with the options of your first dropdown. Change $wrapper_id as you please but we don't know the actual ID the select will receive, so we have to make sure we make it unique in some other way (note that there might be more than one of the same custom field type in the final form, so using the name and the delta only will not suffice).

Using '#validated' => TRUE is usually frowned upon but this is the only easy way to make sure the second select will not result in an "Illegal choice" validation error after we replace the options without the form being aware of the possible values. One standard advice is to pre-fill the second dropdown with the union of all possible values, making sure that later dependent lists always use a subset of that full list but this wasn't really feasible in my case (or any other really dynamic case). Dependent lists in custom forms can simply return the original form element, so this doesn't apply to them, but custom fields can't locate the actual corresponding $form[] element, so this is the best I could come up with. It works.

Next, the function that loads the options of the dependent dropdown, depending on the value selected in the master one:

private function loadOptions($id) {
  $options = [];
  if ($id) {
    // load whatever options belong to the value of $id
    $options = ...;
  }
  return $options;
}

And finally, the Ajax glue to hold it together:

public function getValues(array &$form, FormStateInterface $form_state) {
  $triggeringElement = $form_state->getTriggeringElement();
  $selected_value = $triggeringElement['#value'];
  $html = '';
  $options = $this->loadOptions($selected_value);
  foreach ($options as $key => $value) {
    $html .= "<option value='$key'>$value</option>";
  }

  $ajax_response = new AjaxResponse();
  $wrapper_id = $triggeringElement['#ajax']['wrapper'];
  $ajax_response->addCommand(new HtmlCommand("#{$wrapper_id} select", $html));
  return $ajax_response;
}

If you compare the whole lot to the custom form solution, it's mostly the same but there are small differences in how to identify the correct element and how to actually replace the options of the dependent list that make this work in the field case.

1
  • Thhis should be the accepted answer.
    – dickwan
    Oct 13, 2021 at 16:58
0

I ended up using an API call and using javascript to populate the fields. Its not the correct solution, but it works.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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