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 anitem
target. - Within each
item
we may have alink
target OR another sub-menu which itself is anothermenu
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 sublink
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 adata-menu-target="item"
as this will remove theitem
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