0

I'm learning the firsts concepts of js web components. I pretty nob in these and try to make a simple example. My component is just a button that pretends to change color to a div.

My example work I expected but I noticed that my approach is not too much "component way", if the element that I attempt to change is in my web component to instead outside of it.

This is my html file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="index.css" />
  </head>
  <body>
    <div class="main">
      <h1>Yeah Web Components!</h1>
      <my-component></my-component>
    </div>

    <script src="myComponent.js"></script>
  </body>
</html>

This is my component .js:

const template = `
  <style>
    .container{
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    .box{
        width: 100px;
        height: 100px;
        background: red;
    }
    .hi-button{
      margin-top: 10px;
    }
    
  </style>
  <div class="container">
    <div class="box"></div>
    <button class="hi-button">Change</button>
  </div>
 
`;

class MyFirstTest extends HTMLElement{
  constructor(){
    super()
    const shadowRoot = this.attachShadow({ mode: 'open' }); 
    shadowRoot.innerHTML = template; 
  }

  changeButtonColor(){
      const box = this.shadowRoot.querySelector(".box");
      if(box.style.background === 'red'){
        box.style.background = 'blue';
      }else{
        box.style.background = 'red';
      }
  }

  connectedCallback(){
      const event = this.shadowRoot.querySelector(".hi-button");
      event.addEventListener('click', () => this.changeButtonColor());
  }

  disabledCallback(){
    const event = this.shadowRoot.querySelector(".button-test");
    event.removeEventListener();
  }

}

customElements.define('my-component', MyFirstTest);

As I said the functionality works fine but I don't want my div to be in my web component but in my html file and mi component be only the button.

For example, my html file would be something like this:

.......

  <link rel="stylesheet" href="index.css" />
  </head>
  <body>
    <div class="main">
      <h1>Yeah Web Components!</h1>
      <div class="my-div-to-change"></div>
      <my-component></my-component>
    </div>
  </body>

.......

Is possible for web components to work in this way?

Iván
  • 401
  • 2
  • 6
  • 17

2 Answers2

2

There are some notes about the published code:

CSS & shadowDOM

   .container{
      display: flex;
      flex-direction: column;
      align-items: center;
    }
   .hi-button{
      margin-top: 10px;
    }

This global CSS is never applied to HTML inside the Element shadowDOM.

Note you do not have to use shadowDOM.

  • Templates
  • Custom Elements API
  • shadowDOM

are 3 distinct technologies you can use without each other.

To apply the style, declare it inside the shadowDOM:

const template = `
  <style>
   .container{
      display: flex;
      flex-direction: column;
      align-items: center;
    }
   .hi-button{
      margin-top: 10px;
    }
  </style>
  <div class="container">
    <button class="hi-button">Change</button>
  </div>

Templates

It is just a string, you are not using or declaring a <template>

Click event

The click event on the button, by default, bubbles up the DOM.

There is no need to set the handler on the <button> itself, you can set in on any parent.

Garbage Collection

No need to remove Listeners inside/on the Custom Element. The JavaScript Garbage Collection process will do that for you

RemoveListeners are required when you set Listeners on other DOM elements.
eg. When a Component sets a Listener on document

disabledCallback

Does not exist; its called disconnectedCallback()

composed = true

   this.dispatchEvent(new CustomEvent("button-clicked", { 
      bubbles: true,
   }));

This only works because the Event is not dispatched from shadowDOM.

But dispatch from inside shadowDOM, or wrap the Element in another Element with shadowDOM and the code will no longer work, because Custom Events, by default, do not 'escape' shadowDOM(s).

For that, you need to use:

   this.dispatchEvent(new CustomEvent("button-clicked", { 
      bubbles: true,
      composed: true
   }));

Note: default Events like click do escape shadowDOM by default

Solutions

Without shadowDOM the only code required is:

  • global CSS now styles the button

customElements.define('my-component', class MyFirstTest extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<button class="hi-button">Change</button>`;
    this.onclick = (evt) => document.querySelector(".box").classList.toggle("blue");
  }
});
<style>
  .box {
    width: 80px;
    height: 80px;
    background: red;
  }
  
  .blue {
    background: blue;
  }
  
  .hi-button {
    margin-top: 10px;
  }
</style>
<div class="main">
  <h1>Yeah Web Components!</h1>
  <div class="box"></div>
  <my-component></my-component>
</div>

With shadowDOM

  • There is less need for CSS classes, because all HTML is isolated in the shadowDOM

  • <template> can be declared as HTML (so your IDE neatly formats everything inside) Referenced by this.nodeName, so you can re-use code

  • super() & attachShadow() are functions setting and returning this scope and this.shadowRoot; so they can be chained

  • Custom Events are great; but this code can do with the default click event; the listener checks if the correct button was clicked

  • These are just Web Component technical enhancements, loads more functional enhancements possible, all depends on the use case

  • Do read ::slotted CSS selector for nested children in shadowDOM slot before you continue with <slot> content

Code could look something like this:

<template id="MY-COMPONENT">
  <style>
    div { display: flex; flex-direction: column; align-items: center }
    button { margin-top: 10px }
  </style>
  <div>
    <button><slot></slot></button>
  </div>
</template>
<div class="main">
  <my-component color="blue">Blue!</my-component>
  <my-component color="gold">Gold!</my-component>
  <my-component color="rebeccapurple">Purple!</my-component>
</div>
<script>
  document.addEventListener("click", (evt) => {
    if (evt.target.nodeName == 'MY-COMPONENT')
      document.querySelector(".main").style.background = evt.target.getAttribute("color");
  });

  customElements.define('my-component', class extends HTMLElement {
    constructor() {
      let template = (id) => document.getElementById(id).content.cloneNode(true)
      super()
        .attachShadow({mode: 'open' })
        .append( template(this.nodeName) );
    }
  });
</script>
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
1

The best way to maintain the independence between components is use events.

Web components dispatch an event that is listened by the parent container. And then, parent do the necessary action.

If you want to talk "directly" between component and parent, the system is pretty coupled and is not a good way. Of course you can, but is not recommended.

You can check this answer which is clearer.

Also, answering your question using events. Is as simple as this:

First, into your component you have to dispatch the event in this way:

this.dispatchEvent(new CustomEvent("button-clicked", { 
    bubbles: true,
}));

You can check here the use of this.

Also, the parent will listen using:

document.addEventListener("button-clicked", changeColor);

So, when the button is clicked, the parent will trigger the function changeColor with the logic inside you want.

In this way you are using a Web Component to do actions into parent container but the system is not coupled. Parent can work without child and child can work without parent. Both can be used separately. Of course, the event will not be dispatch or listen but there is no a dependency between components.

Also this is an example how it works.

const template = `

  <div class="container">

    <button class="hi-button">Change</button>
  </div>
 
`;

class MyFirstTest extends HTMLElement{
  constructor(){
    super()
    const shadowRoot = this.attachShadow({ mode: 'open' }); 
    shadowRoot.innerHTML = template; 
  }

  changeButtonColor(){
      //Call parent
      this.dispatchEvent(new CustomEvent("button-clicked", { 
            bubbles: true,
        }));
  }

  connectedCallback(){
      const event = this.shadowRoot.querySelector(".hi-button");
      event.addEventListener('click', () => this.changeButtonColor());
  }

  disabledCallback(){
    const event = this.shadowRoot.querySelector(".button-test");
    event.removeEventListener();
  }

}

customElements.define('my-component', MyFirstTest);
<!DOCTYPE html>
<html lang="en">
  <head>
   <style>
    .container{
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    .box{
        width: 100px;
        height: 100px;
        background: red;
    }
    .hi-button{
      margin-top: 10px;
    }
    
  </style>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="index.css" />
  </head>
  <body>
    <div class="main">
      <h1>Yeah Web Components!</h1>
      <div class="box"></div>
      <my-component></my-component>
    </div>

    <script src="myComponent.js"></script>
    <script>
    //Add listener to do the action
    document.addEventListener("button-clicked", changeColor);
    function changeColor(){
      var box = document.querySelector(".box");
      if(box.style.backgroundColor === 'red'){
        box.style.backgroundColor = 'blue';
      }else{
        box.style.backgroundColor = 'red';
      }
    }
    </script>
  </body>
</html>
J.F.
  • 13,927
  • 9
  • 27
  • 65
  • So I understand because of your answer and one of link you put here, that my initial idea is bad practice for this web components architecture? – Iván Jan 18 '21 at 19:04
  • Yes, the best way is not talk between components/parent directly because this do a coupled application. Just the opposite you want using web components. – J.F. Jan 18 '21 at 19:07