I have two list fields with select widgets and the second select depends on the first one.
How would you do this? Is there a module already doing this at least in code?
Drupal Answers is a question and answer site for Drupal developers and administrators. It only takes a minute to sign up.
Sign up to join this communityI recently had your doubt, in my search I came upon a very interesting module, Reference field option limit, which depends on Entity Reference and is only available for Drupal 7.
It solved my problem; I hope it will be useful to you.
This is quite complex but doable. I have two fields, one is called parent_menu_name
, this is an entity reference field into menus and a parent_menu_link
which is simply a string -- menu tree entries are not entities (custom menu links are but not at all tree entries are those).
I started with moving the code to a class for encapsulation:
function group_content_submenu_field_widget_single_element_form_alter(array &$element, FormStateInterface $form_state, array $context) {
\Drupal::classResolver(GroupContentSubmenuAlter::class)
->widget($element, $form_state, $context);
}
This calls my widget
method:
public function widget(array &$element, FormStateInterface $form_state, array $context) {
/** @var \Drupal\Core\Field\FieldItemListInterface $items */
$items = $context['items'];
$gcm = $items->getEntity();
if ($gcm instanceof GroupContentMenuInterface && $gcm->hasField('parent_menu_link') && $gcm->hasField('parent_menu_name')) {
$prefix = sprintf('gcs-%s-%d-',
$gcm->getFieldDefinition('parent_menu_link')->getUniqueIdentifier(),
$context['delta']
);
$parent_menu_link_wrapper_id = $prefix . 'link';
$parent_menu_name_wrapper_id = $prefix . 'name';
switch ($items->getFieldDefinition()->getName()) {
case 'parent_menu_name':
$element['#prefix'] = "<div id='$parent_menu_name_wrapper_id'>";
$element['#suffix'] = '</div>';
$element['#after_build'][] = [static::class, 'afterBuildName'];
$element['#ajax'] = [
'callback' => [static::class, 'ajax'],
'parent_menu_link_wrapper_id' => $parent_menu_link_wrapper_id,
];
break;
case 'parent_menu_link':
$options = $this->getParentSelectOptions($gcm, $form_state);
$element['value']['#type'] = 'select';
$element['value']['#options'] = $options;
$element['value']['#size'] = min(count($options), 50);
$element['value']['#prefix'] = "<div id='$parent_menu_link_wrapper_id'>";
$element['value']['#suffix'] = '</div>';
$element['value']['#states']['invisible'][] = ["#$parent_menu_name_wrapper_id select" => ['value' => '_none']];
$element['value']['#after_build'][] = [static::class, 'afterBuildLink'];
break;
}
}
}
it adds HTML wrappers around both widgets. Initially #states
hides the parent menu link widget using the wrapper ID of the parent menu name widget. Then #ajax
on the parent menu name widget replaces the entire parent menu link widget using the wrapper ID of it. So the frontend is done, now we need to tie them together on the backend. Let's see the method generating the options for the second widget:
public function getParentSelectOptions(GroupContentMenuInterface $gcm, FormStateInterface $form_state): array {
$menu_name = $gcm->parent_menu_name->target_id;
// This is set in ::afterBuildName so this will only be set in form
// rebuild which is exactly when we need it. The form parents do not
// change from one form build to the next (hopefully).
if ($menu_name_form_parents = $form_state->get(self::PARENT_MENU_NAME_FORM_PARENTS)) {
$menu_name = $form_state->getValue($menu_name_form_parents);
}
if ($menu_name && $menu_name !== '_none' && ($menu = Menu::load($menu_name))) {
return $this->menuParentFormSelector->getParentSelectOptions('', [$menu_name => $menu->label()]);
}
return [];
}
As you can see it reads the value of the first widget after an AJAX submit thanks to this small after build method:
public static function afterBuildName(array $element, FormStateInterface $form_state): array {
$parents = $element['#parents'];
$parents[] = 0;
$parents[] = 'target_id';
$form_state->set(self::PARENT_MENU_NAME_FORM_PARENTS, $parents);
return $element;
}
so when the parent menu name widget is built, it stores #parents
into $form_state
and when the form is rebuilt, the parent menu link uses this information to find the parent menu name value.
The AJAX callback is simple:
public static function ajax(array &$form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
// The right element to replace was set in ::afterBuildLink.
$parent_menu_link_widget = NestedArray::getValue($form, $form_state->get(self::PARENT_MENU_LINK_ARRAY_PARENTS));
$parent_menu_link_wrapper_id = $triggering_element['#ajax']['parent_menu_link_wrapper_id'];
return (new AjaxResponse())
->addCommand(new ReplaceCommand('#' . $parent_menu_link_wrapper_id, $parent_menu_link_widget));
}
Remember, this AJAX callback is called by the parent menu name widget and needs to replace the parent menu link widget. The frontend is handled already, this is where another #after_build
method comes to help us: the parent menu link widget conveniently stores its parent in $form_state
-- but since we are retrieving from $form
this needs to be #array_parents
:
public static function afterBuildLink(array $element, FormStateInterface $form_state): array {
$form_state->set(self::PARENT_MENU_LINK_ARRAY_PARENTS, $element['#array_parents']);
return $element;
}
To recap, the lifecycle of an AJAX form is:
Both of our widgets store their respective locations in step #1 and then the other widget retrieves this in #4.