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.
$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.