Home > Blockchain >  Stimulus nested (child) targets
Stimulus nested (child) targets

Time:02-11

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?

CodePudding user response:

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

CodePudding user response:

Answer from another conversation,

A) For menuLink and menuList, you handle it by yourself: use CSS classes, and then use normal selectors. So, once you've used the menuItem target to find the menuItem you want, you would then do menuItem.querySelector('.menu-link'). Not a Stimulus solution, but it's pretty simple and it's nice to be able to "back out" and do things manually if you need to.

B) I'm not sure what your overall Stimulus controller is meant to do, but it's possible that there should be a menu-item controller that lives on the menuItem target. Depending on what you're trying to accomplish, that could replace the main-menu controller or, more likely (because I'm assuming you are doing some "work" on the top level main-menu where you want to be aware of all of the "items"), in addition to the main-menu controller. With this setup, your main-menu controller could loop over the menuItem targets and, in each one, directly use its underlying controller instance - even calling methods on it. This is not something I showed on the tutorial, but it's not an uncommon pattern: you would expose the "controller instance" of the "menu-item" controller on its element - e.g. https://www.betterstimulus.... (the big difference in that example is that both of the controllers are on the same element - so adjust accordingly).

(c) weaverryan

  • Related