Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stimulus nested (child) targets

Is it possible (and is it appropriate) to use nested/child targets of parent target?

For example i have “N+“ menu items, each item (wrapper) contain link and list. I could add data-main-menu-target="menuItem" to each of parent items and then iterate over them in controller loop using this.menuItemTargets.forEach(...)

But what's the best practice to find menu-item-link and menu-item-list for each menu-item target on each loop iteration?

In general i could add targets also for those elements, e.g. menuItemLink & menuItemList, but how then i could select them from parent menuItem target, Is it possible to do something like menuItemTarget.find(this.this.menuListTarget)?

To visualise the structure is the following:

data-controller="main-menu"
    data-main-menu-target="menuItem"
        data-main-menu-target="menuLink"
        data-main-menu-target="menuList"
....
    data-main-menu-target="menuItem"
        data-main-menu-target="menuLink"
        data-main-menu-target="menuList"
....

How then select "menuLink" for certain "menuItem" target on each loop?

like image 667
kxc Avatar asked Nov 18 '25 12:11

kxc


1 Answers

You could structure your controller so that you have one menu controller that gets used on both the root menu and also the sub-menus within them. This could be recursively accessed from whatever is deemed to to be the root.

Example Code

  • In the HTML below we have a nav which contains ul for a menu, each child should be an item target.
  • Within each item we may have a link target OR another sub-menu which itself is another menu controller and the pattern continues.
<nav>
  <ul class="menu-list" data-controller="menu" data-menu-target="root">
    <li data-menu-target="item">
      <a data-menu-target="link">Team Settings</a>
    </li>
    <li data-menu-target="item">
      <ul data-controller="menu">
        <li data-menu-target="item">
          <a data-menu-target="link">Members</a>
        </li>
        <li data-menu-target="item">
          <a data-menu-target="link">Plugins</a>
        </li>
        <li data-menu-target="item">
          <a data-menu-target="link">Add a member</a>
        </li>
      </ul>
    </li>
    <li data-menu-target="item">
      <a data-menu-target="link">Invitations</a>
    </li>
    <li data-menu-target="item">
      <a data-menu-target="link">Cloud Storage Environment Settings</a>
    </li>
  </ul>
</nav>
  • In our controller we first determine if this controller's instance is the root, simply by checking this.hasRootTarget.
  • Controllers only get access to their 'scoped' elements so the root can only 'see' the children outside of the nested data-controller='menu'.
  • We need to use setTimeout to wait for any sub-controllers to connect, there may be a nicer event propagation way to do this.
  • You can access a controller on an element via the getControllerForElementAndIdentifier method.
  • From here we can determine the menu structure as an array of either a link target OR a nested array which itself will contain the sub link targets.
  • We can use the Node.contains method to map through each item and see what links are 'contained' within it.
  • This approach could be refined to get you the structure you need to work with.
class MenuController extends Controller {
  static targets = ['item', 'link', 'root'];

  connect() {
    if (this.hasRootTarget) {
      setTimeout(() => {
        // must use setTimeout to ensure any sub-menus are connected
        // alternative approach would be to fire a 'ready' like event on submenus
        console.log('main menu', this.getMenuStructure());
      });
    }
  }

  getMenuStructure() {
    const links = this.linkTargets;

    return this.itemTargets.map((item) => {
      const child = item.firstElementChild;
      const subMenu = this.application.getControllerForElementAndIdentifier(
        child,
        this.identifier
      );
      const menuLinks = links.filter((link) => item.contains(link));
      return subMenu ? subMenu.getMenuStructure() : menuLinks;
    });
  }
}

Notes

  • We are accessing the DOM via the firstElementChild and this may not be the way we want to do things in Stimulus, but you could simply add another target type of 'sub-menu' to be more explicit and follow the pattern of finding the 'link' within each item this way.
  • A reminder that you cannot put the data-controller="menu" on a data-menu-target="item" as this will remove the item from the parent scope. As per the docs on scopes.

that element and all of its children make up the controller’s scope.

like image 129
LB Ben Johnston Avatar answered Nov 20 '25 01:11

LB Ben Johnston