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?
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.
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
I am currently writing a Nightwatch test to select a new document from a list. And I will need to be able to select the next in the list. Is there a way to manually override the child number that needs selecting?
For example the current selector being used is :
<ul class="dv-packdocs">
<li class="dv-packdoc"<div class="icon-todo"></li>
<li class="dv-packdoc"<div class="icon-todo"></li>
<li class="dv-packdoc"<div class="icon-todo"></li>
<li class="dv-packdoc"<div class="icon-todo"></li>
</ul>
and the test would be something like :
viewer.selectNewDocument([2])
would this select the second child under the ul?
Or would I have to specify each child element?
If I understand correctly, you are trying to dynamically find the appropriate child element (li) from a dynamical length list (ul, where the list is populated based on user input, or other site actions). Correct?
I see two scenarios with two different approaches:
1. You have a set/fixed condition (way of identifying your target element): for example, in your list, the second li would be targeted by the below command.
viewer.selectNewDocument('ul.dv-packdocs li:nth-child(2)') (considering you are passing a complete selector to the selectNewDocument function)
, or
viewer.selectNewDocument(2), passing a number & form the selector inside the command (if you care for aesthetics):
selectNewDocument: function(index) {
this.api.perform((done) => {
// Click the second document in the list:
let selector = `ul.dv-packdocs li:nth-child(${index})`;
this.api.click(selector);
done();
});
return this;
},
Alternatively, if you would want the last document added, then you would have to issue a elements call on the ul to retrieve the length of the list, then use that in the same way to determine which li you have to click: viewer.selectNewDocument('ul.dv-packdocs li:nth-child('+length+')') (where length is the result of your elements call).
2. You don't have a fixed condition (I'll fill this up if the first part doesn't cover it, or later today, kinda slammed after the holidays)
Hope it's what you were looking for! Cheers!
In my website, I have a dynamically growing list, (from a database) that has buttons attached to each item of the list. The button goes to the same javascript function, but each element in the list has a different id, which needs to be included inside the function when it is running.
I'm currently using onclick in the html and can pass the id through the function's parameter, as I am using template literals to render the list.
Is there any way that I can do this with event listeners?
An example of what I currently have would be:
onclick="theFunction('id')"
which changes for every item in the list, so the list could look like
<ul>
<li onclick="theFunction('id1')">name1</li>
<li onclick="theFunction('id2')">name2</li>
<li onclick="theFunction('id3')">name3</li>
</ul>
Use event delegation instead: attach one listener to the <ul>, and then on click, check to see if the clicked element was a <li>. If so, check an attribute of the <li> to figure out what parameter to call theFunction with. For example:
const theFunction = console.log;
document.querySelector('ul').addEventListener('click', (e) => {
if (!e.target.matches('li')) {
return;
}
const param = 'id' + e.target.dataset.param;
theFunction(param);
});
<ul>
<li data-param="1">name1</li>
<li data-param="2">name2</li>
<li data-param="3">name3</li>
</ul>
Note how the onclick attributes have been replaced with data-param attributes.
I have a button inside a div. When clicked, i will need to grab the length of a list that is a sibling to the parent container. maybe so even a grandparent container.
The gist of the code looks something like this below. (My actual code is actually nested deeper than this example. I would like to know how to target part a sibling of a parent or grandparents or even great great grandparent...)
HTML
<div>
<div class="button-parent">
<div class="button-containers">
<button>Click Here!</button>
</div>
</div>
<div class="list-container">
<ul class="lists">
<li>bar</li>
<li>bar</li>
<li>bar</li>
<li>bar</li>
<li>bar</li>
</ul>
</div>
</div>
jquery
$('button').parent('button-parent')
.closest('list-containers').length
.closest() finds the closest ancestor (parent), so it won't find your .list-container.
You can do this a few ways though...
Give your surrounding div a class and use .find() to locate your list if it is the only list in your parent div:
$('button').closest('.parent-div').find('.list-container');
Use next() if .list-container will always be the next item after .button-parent:
$('button').closest('.button-parent').next('.list-container');
You can use .siblings() if .list-container will always be on the same DOM level as .button-parent:
$('button').closest('.button-parent').siblings('.list-container');
From there you can fulfil the rest of your requirement to capture the length of the list by selecting all the li elements and counting them. If we use number 1 above as an example, it might look something like this:
const list = $('button').closest('.parent-div').find('.lists');
const items = list.children();
const count = items.length;
NOTE: The selection for list in this example targets ul.lists rather than .list-container, as this allows us to count using children(), which is much less intensive than find().
I suppose you can use Use parents([selector]) or closest([selector]) with siblings([selector])
parents() looks for ancestors, closest() be used as well if you want to select only the first going upward in the ancestry
siblings() select for siblings
you can pass them selectors or use eq()
one more things your jquery snippet doesn't seem to include . for class selector
Three methods that I tried and worked are
$('button').parents().eq(1).siblings('.list-container');
$('button').parents('.button-parent').siblings('.list-container');
$('button').closest('.button-parent').siblings('.list-container'));
Check out this jsfiddle, i have selected list-container using the three methods
I have multiple pages that all use a simple tab menu at the top.
The menu (aka list) is stored in a separate html file:
<ul>
<li id="Tab1"><a href='../Page1.html'> Page1 </a></li>
<li id="Tab2"><a href='../Page2.html'> Page2 </a></li>
<li id="Tab3"><a href='../Page3.html'> Page3 </a></li>
every page has <div id='tabs'> </div> and
$('#tabs').load('CommonTabs.html');
Everything works fine, my tabs load into the 'tabs' div. Now I want to set a class of one div on each page to "active", so that the proper style defined in a css can be applied.
I tried:
$('#tabs').load('CommonTabs.html');
$('#Tab1'.addClass("active");
But that doesn't seem to work. When I do "inspect element" in a browser, the li does not have the "active" class assigned.
The load function is asynchronous, so the data is not loaded when the second call executes. Perform it in a callback function instead:
$('#tabs').load('CommonTabs.html', function () {
$('#Tab1').addClass("active");
});
You have a syntax error. Try:
$('#Tab1').addClass("active");
I will give you an example to set a class to a specific element on a button click. Of course you will need to create a button with the class addClassButtonTab3 for this to work.
$('.addClassButtonTab3').click('on', function() {
$('#tab3').addClass('active');
});
We are binding an event handler to a button with class 'addClassButtonTab3' and when it is clicked we are adding a class to an element with ID tab3.
Ok it's a little hard to explain in a single title but basically I have a dynamic class added to a child element based on it's parent's dynamic class. Added so:
$('ul').each(function(key){
if ($(this).hasClass('sortable')){
$(this).addClass('parent' + key);
$(this).children().addClass('parent' + key);
};
});
The structure is pretty simple after this:
<ul class="parent0">
<li class="parent0">
<ul class="parent1">
<li class="parent1"></li>
</ul>
</li>
</ul>
Now the UI has the user move these li outside of the parent and placed elsewhere. Later on, I want to check the element and then match it to it's corresponding (original) parent. It can't be $(this) parent because it will be moved out of the parent but the classes still remain.
So the check is looking for .parent(n) and then finding the ul with .parent(n) eventually this code will live inside:
$('sortable li').appendTo($('THIS-IS-THE-DYNAMIC-CLASS'));
So I'm assuming the find will be before this but I don't know how to write that.
I would use a different attribute other than class so it can be wholly unique. Either use $(this).data or $(this).attr. And I would recommend assigning IDs to the parent (or a different attribute) that, again, can be wholly unique. This will keep things cleaner in my opinion.
For example...
Assuming:
$(this).attr('parentClass', '.parent' + key);
then
$('.sortable li').each(function() { $(this).appendTo($(this).attr('parentClass')); });