Search code examples
javascriptweb-componentnative-web-component

Stop event from propagating to another instance of same web component?


I've got a child web component (exercise-input) with a custom event that's sending some data over to my parent web component (muscle-group) it's nested in. The issue is that when I add additional instances of my parent component to the page, the button in the child component that updates the value in the parent component will update that value on every instance of the parent component.

I used document.dispatchEvent(numSetsDecrease) in the child and document.addEventListener in the parent - is there something I need to replace "document" with? Do I need to stop propagation somehow?

The child component:

    const exerciseInputTemplate = document.createElement('template');
    exerciseInputTemplate.innerHTML = ` 
    
      <div class="exercises">
        <hr>
        <div class="exerciseSetsRepRange">
          <div class="sideBySide">
              <input type="text" placeholder="Exercise">
              <div class="numSetsBtns">
                <button class="subtractSetBtn">-</button>
                <span class="numSets"></span>
                <button class="addSetBtn">+</button>
              </div>
          </div>
          <div class="sideBySide">
            <input type="number" placeholder="Rep Range Low" min="1">
            <input type="number" placeholder="Rep Range High" min="1">
          </div>
        </div>
      </div>
    `;
     
    class ExerciseInput extends HTMLElement {
     constructor() {
       super(); 
       this.attachShadow({ mode: 'open'});
       this.shadowRoot.appendChild(exerciseInputTemplate.content.cloneNode(true));
     }
    
      connectedCallback() {
        const subtractSetBtn = this.shadowRoot.querySelector('.subtractSetBtn');
        const numSets = this.shadowRoot.querySelector('.numSets');
        const addSetBtn = this.shadowRoot.querySelector('.addSetBtn');
        numSets.textContent = 3 + ' sets';
    
        // Click listner for subtract set btn with nested custom event
        subtractSetBtn.addEventListener('click', function(event){
          let numSetsInt = parseInt(numSets.textContent);
          numSetsInt -= 1;
          numSets.textContent = numSetsInt + ' sets';
    
          if (parseInt(numSets.textContent) === 0) {
            subtractSetBtn.disabled = true;
          }
    
          // Custom event sending -1 to the parent
          const numSetsDecrease = new CustomEvent('numSetsDecrease', {
            bubbles: false,
            detail: {
              numSetValue: -1
            }
          });
          document.dispatchEvent(numSetsDecrease);
        });
    
        // Click listner for add set btn with nested custom event
        addSetBtn.addEventListener('click', function(event){
          let numSetsInt = parseInt(numSets.textContent);
          numSetsInt += 1;
          numSets.textContent = numSetsInt + ' sets';
    
          if (parseInt(numSets.textContent) > 0) {
            subtractSetBtn.disabled = false;
          }
    
          // Custom event sending +1 to the parent
          const numSetsIncrease = new CustomEvent('numSetsIncrease', {
            bubbles: false,
            detail: {
              numSetValue: 1
            }
          });
          document.dispatchEvent(numSetsIncrease);
        });
      }
    }
   
    window.customElements.define('exercise-input', ExerciseInput);

The parent component:

    const muscleGroupTemplate = document.createElement('template');
    muscleGroupTemplate.innerHTML = ` 
      <form class="programBuilder">
        <div class="inputDiv muscleGroupDiv">
          <input id="muscleGroupInput" name="muscleGroupInput" list="muscleGroupsList" placeholder="Muscle Group">
          <datalist id="muscleGroupsList">
          </datalist>
        </div>
    
        <div class="inputDiv setsDiv">
          <label for="setsRange" class="setsValue"></label>
          <input type="range" min="1" max="30" class="setsRange" value="15">
          <p class="numSetsOutput"></p>
        </div>
    
        <exercise-input></exercise-input>
        <exercise-input></exercise-input>
        <exercise-input></exercise-input>
    
        <button class="addExerciseBtn">Add Exercise</button>
      </form>
    `;
     
    class MuscleGroup extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: 'open'});
        this.shadowRoot.appendChild(muscleGroupTemplate.content.cloneNode(true));
      }
    
      connectedCallback() {
        // Updating the number sets output with data from the child elements
        const numSetsOutput = this.shadowRoot.querySelector('.numSetsOutput');
        numSetsOutput.textContent = '9 total sets';
    
        document.addEventListener('numSetsDecrease', function(event){
          let numSetsOutputValue = parseInt(numSetsOutput.textContent);
          numSetsOutputValue += event.detail.numSetValue;
          numSetsOutput.textContent = `${numSetsOutputValue} total sets`; 
        });
        
        document.addEventListener('numSetsIncrease', function(event){
          let numSetsOutputValue = parseInt(numSetsOutput.textContent);
          numSetsOutputValue += event.detail.numSetValue;
          numSetsOutput.textContent = `${numSetsOutputValue} total sets`;
        });
      }
    }
     
    window.customElements.define('muscle-group', MuscleGroup);

Any help would be greatly appreciated! I've been trying to figure this out all day.


Solution

  • An Event bubbles up the DOM.

    https://javascript.info/bubbling-and-capturing

    document.addEventListener captures all named Events from its children

    Instead you:

    • dispatchEvent from your buttons
    • The Events need bubbles:true and composed:true to cross shadowRoots
    • a Listener at <muscle-group> then captures the bubbling Events and processes them
      it only receives Events from its children!
    • if you do not have nested groups you don't have use stopPropagation, Events bubbling on to document do no harm

    PS. Your <exercise-iput> has 2 buttons which do the same, one adds, one substracts.

    The moment you copy/pasted that first button code, a "This Needs A Component" alarm should have warned you to create a <exercise-input-button add="-1">