I have created this very common scenario of menu links, in a native vanilla JS web-component format.
<side-nav>
<nav-item id="1">1</nav-item>
<nav-item id="2">2</nav-item>
<nav-item id="3">3</nav-item>
<nav-item id="4">4</nav-item>
</side-nav>
Naturally, when I click on any of nav-item, I need to
- make the clicked item active/selected
- check if there is another item already active, and if there is, make that nav-item inactive
With web-components I have observed that I can handle that in two ways.
Method 1:
- dispatch an event when
<nav-item>gets clicked - Add an event listener on parent node
<side-nav> - on each click event, loop through all child
<nav-item>components of side-nav and deactivate an already active<nav-item>if it is not the event.target node.
code for <nav-item>
async connectedCallback() {
this.addEventListener('click' , (event) => {
this.setAttribute('selected', true);// immediately mark current component selected
this.dispatchEvent(new CustomEvent('navitem-selected', { bubbles: true, composed: true , detail: { id: this.id} })); // dispatch event, so that parent can loop and deselect other items
});
}
and code for the parent <side-nav>
async connectedCallback() {
this.addEventListener('navitem-selected', (event) => {
let items = this.shadowRoot.querySelector('slot').assignedElements();
items.forEach((item) => {
if(item.getAttribute('id') !== event.detail.id) {
item.removeAttribute('selected');
}
});
});
}
Method 2:
Completely ignore parent and handle the activation logic only in the <nav-item>
static get observedAttributes() {
return ['id', 'selected'];
}
attributeChangedCallback(name, oldValue, newValue) {
if(name == 'id'){
this.id = newValue;
}
if(name == 'selected')
{
if(newValue){
this.shadowRoot.querySelector('.root').classList.add('selected');
} else {
this.shadowRoot.querySelector('.root').classList.remove('selected');
}
}
}
async connectedCallback() {
this.addEventListener('click' , (event) => {
this.dispatchEvent(new CustomEvent('navitem-selected', { bubbles: true, composed: true , detail: { id: this.id} }));
});
const hostNode = this.shadowRoot.querySelector('.root').getRootNode().host.parentNode;
hostNode.addEventListener('navitem-selected', (event) => {
if(this.id == event.detail.id){
this.setAttribute('selected', true);
} else if(this.hasAttribute('selected')){
this.removeAttribute('selected');
}
});
}
Both methods work fine but for the life of me I can not figure out which one is better when considering good programming practices and performance.
I have a general concept that child should not know about siblings or parents, but parents should manage children. I am bit blur on the best practices here and would love to be pulled to light.