<blog-post>
  <h2 slot="post-title">#1 Blog Post</h2>
  <p slot="post-body">
    Lorem ipsum dolor sit amet consectetur,
    adipisicing elit.
  </p>
</blog-post>

<blog-post>
  <h2 slot="post-title">#2 Blog Post</h2>
  <p slot="post-body">
    Deleniti assumenda architecto aliquid
    voluptas eum quam earum quae dolore.
  </p>
</blog-post>
class WebComponent extends HTMLElement {
  constructor() {
    super();

    // Re-renders the DOM when state is set to a new value
    this._state = new Proxy(this.init(), {
      set: (target, property, value) => {
        if (!Object.is(target[property], value)) {
          target[property] = value;
          this.render();
        }
        return true;
      }
    });

    // Creates the Shadow DOM
    this.shadow = this.attachShadow({ mode: "open" });

    // Triggers the first render
    this.render();
  }

  set state(obj) {
    for (const key in obj) {
      this._state[key] = obj[key];
    }
  }

  get state() {
    return this._state;
  }

  render() {
    // Bundle styles and html
    const templateString = /* HTML */ `
      <template>
        <style>
          ${this.styles()}
        </style>
        ${this.html()}
      </template>
    `;

    // Parses the template
    const template = new DOMParser()
      .parseFromString(templateString, "text/html")
      .querySelector("template").content;

    // Clears the Shadow DOM
    this.shadow.innerHTML = "";

    // Attaches a copy of the template to the now empty Shadow DOM
    this.shadow.appendChild(template.cloneNode(true));

    // Attaches listeners
    this.listeners();
  }

  handle(id, event, handler) {
    this.shadow.getElementById(id).addEventListener(event, handler);
  }
}

class BlogPost extends WebComponent {
  constructor() {
    super();
  }

  init() {
    return {
      likes: 0
    };
  }

  listeners() {
    this.handle("btn-add-like", "click", () => {
      this.state.likes = this.state.likes + 1;
    });
  }

  styles() {
    return /* CSS */ `
        .post-title {
          color: red;
        }
        .post-body {
          color: blue;
        }
      `;
  }

  html() {
    return /* HTML */ `
      <article>
        <div class="post-title">
          <slot name="post-title">[[Post Title]]</slot>
        </div>
        <div class="post-body">
          <slot name="post-body">[[Post Body]]</slot>
          <p>Likes: ${this.state.likes}</p>
          <button id="btn-add-like">Add +1 Like</button>
        </div>
      </article>
    `;
  }
}

customElements.define("blog-post", BlogPost);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.