Search code examples
javascriptreactjsweb-component

How to add style or class name and remove other element from web component?


I have two components where one is the parent component and the other is the child component. Now the parent has two children within it. which child click for add a style in it, i would require to remove the style in other children. so at a time only one child will keep the style. how to do this?

LiveDemo - click on the button. I am not able to remove the style back.

here is my code :

class Parent extends HTMLElement {
    shadowRoot;
  constructor(){
    super();
    this.shadowRoot = this.attachShadow({mode: 'open'});
  }

  connectedCallback(){
    this.render();
    }

  render() {
    this.shadowRoot.innerHTML = `<div>
        <children-holder></children-holder>
      <children-holder></children-holder>
      <children-holder></children-holder>
    </div>`
  }

}

customElements.define('parent-holder', Parent);


class Children extends HTMLElement {
    shadowRoot;
  constructor(){
    super()
    this.shadowRoot = this.attachShadow({mode: 'open'});
  }

  connectedCallback(){
    this.render();
    this.shadowRoot.querySelector('button').addEventListener('click', () => {
        this.shadowRoot.querySelector('button').style.border = "";
        this.shadowRoot.querySelector('button').style.border = "3px solid red";
    })
    }

  render() {
    this.shadowRoot.innerHTML = `
    <div><button class="button">Click me!</button></div>`
  }

}

customElements.define('children-holder', Children);

Solution

  • a long answer for (eventually) 3 lines of code...

    If you make Custom Element children access a parentNode, and loop its DOM elements..

    You are creating a dependency between components

    Event Driven solution:

    • The click on a button bubbles up the DOM
    • so the parent can capture that click event
    • The evt.target will be the button clicked
    • The parent then emits a custom event
    • The Children are listening for that Event, there is NO dependency on the parent
    • Since the Event contains the button clicked, each listening element can do its select/unselect code
    • And it is less and clearer code
    class Parent extends HTMLElement {
      constructor() {
        super()
          .attachShadow({mode: 'open'})
          .shadowRoot.innerHTML = `<div>` +
             `<children-holder></children-holder>`.repeat(3) +
             `</div>`
      }
    
      connectedCallback() {
        this.shadowRoot.addEventListener('click', evt => {
          if (evt.target.nodeName === 'CHILDREN-HOLDER')
            document.dispatchEvent(new CustomEvent('myStateEvent', {
              detail: evt.target // THE BUTTON CLICKED
            }));
        });
      }
    }
    
    customElements.define('parent-holder', Parent);
    
    class Children extends HTMLElement {
      constructor() {
        super()
        this.attachShadow({mode: 'open'});
      }
    
      connectedCallback() {
        this.shadowRoot.innerHTML = `<div><button class="button">Click me!</button></div>`;
        document.addEventListener('myStateEvent', evt => {
          let IwasClicked = evt.detail === this;
          this.shadowRoot.querySelector('button').style.border = IwasClicked ? "3px solid red" : ""
        });
      }
    }
    customElements.define('children-holder', Children);
    

    Notes

    • dispatch and listen are both on the document, you can attach them anywhere

    • events bubble UP, not down

    • the default events like click bubble out of shadow DOM

    • Custom Events require composed:true
      read: https://developer.mozilla.org/en-US/docs/Web/API/Event/Event

    • I did the dispatch in the Parent for clearity (A DEPENDENCY!)

    It might be better to make the Child do the dispatchEvent, So it becomes:

    Yo! everyone listening! I was clicked, WE ALL do whatever WE need to do

    And keep all logic in one component:

      connectedCallback() {
        let root = this.shadowRoot;
        let eventName = "myStateEvent";
        root.innerHTML = `<div><button class="button">Click me!</button></div>`;
        document.addEventListener(eventName, evt => {
          let button = root.querySelector("button");
          button.style.border = evt.detail === button ? "3px solid red" : "";
        });
    
        root.addEventListener("click", evt =>
          document.dispatchEvent(
            new CustomEvent(eventName, {
              detail: evt.target // THE BUTTON CLICKED
            })
          )
        );
      }
    

    Now you understand Event driven solutions

    And you might now ask: Why not use the click event?

    That is possible once you understand that event.target is NOT what you might think it is.

    When events originate from shadow DOM, the event.target value is the last shadowDOM it pierced

    So your button click sets different event.target values:

        Listener on <children-holder>   event.target = button
        Listener on <parent-holder>     event.target = <children-holder>
        Listener on document            event.target = <parent-holder>
    

    To solve your Button-Select-Color use-case with one click event
    the button click is the dispatcher, sending a click event UP the DOM,
    through all shadowDOM boundaries

    You have to check the event.composedPath() function which retuns an Array of ALL DOM elements the Event passed.
    (note: event.path is Chrome only!!)

    So all code required for your style question is:

      connectedCallback() {
        let root = this.shadowRoot;
        root.innerHTML = `<div><button>Click me!</button></div>`;
        root.host.getRootNode().addEventListener("click", evt => {
          let button = root.querySelector("button");
          button.style.border = evt.composedPath().includes(button) ? "3px solid red" : "";
        });
      }
    

    Notes

    • root.host.getRootNode() allows one selected button per parent Component
    • change to document and it is one button per page
    • evt.composedPath().includes(root) identifies the child-component

    Working Fiddle: https://jsfiddle.net/WebComponents/bc9tw1qa/