4

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?

4 Answers 4

1

You can use the Conditional fields module.

1

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

1

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:

  1. Form gets built.
  2. The form and the form state is stored in the database, keyed by the build id.
  3. On an AJAX request, the form and form state is restored
  4. The form is rebuilt.
  5. The form and the form state is stored in the database, keyed by a new build id.
  6. The AJAX response is sent. This includes response provided by the callback and the new build id.

Both of our widgets store their respective locations in step #1 and then the other widget retrieves this in #4.

0

A solution could be creating a combo field that includes multiple select.

0

Your Answer

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