event listening on child or parent for reacting to changes in siblings

Viewed 884

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:

jsfiddle

  • 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:

jsfiddle

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.

1 Answers

Think of this like you would if it were a set of standard components. Most standard components do not know anything about any other components. So it is the job of a parent component to listen to the events from the child components and make changes to all of the children, as needed.

The only odd-ball component is the <label> component with the for attribute. You set the for attribute to the id of the component that will receive focus when you click on the <label>. The label that has a click event handler and, if there is a for attribute it looks for a component with that id. If it finds that component then it calls the .focus() function on that component.

Doing this your components are not tied together but can connect together.

If I were writing the code I would do it the first way.

Related