DEV Community

Building Web Components with Vanilla JavaScript

Ali Spittel on February 11, 2018

Back in 2015, I was in the midst of learning my first front-end framework -- AngularJS. The way I thought of it was that I was building my own HTML...
Collapse
 
nickytonline profile image
Nick Taylor • Edited

I did a hello world in web components a couple of years ago, but haven't touched them since. Thanks for the refresher!

I'd just make one suggestion about adding event listeners. This is not web component specific, but for adding event listeners in general. In the case of the rainbow-text component, the number of <span />s increases for every additional letter in the text attribute, so the number of event listeners per instance of the component is n letters * 2 (mouse over + animation end events).

You can end up with a lot of events very quickly just for one instance of the component. What you can do is add an event listener for each event type on the parent <div /> you create in aspittel/rainbow-word-webcomponent and then the power of event bubbling is your friend.

e.g.

class RainbowText extends HTMLElement {
  …

  addEventListeners(div) {
    div.addEventListener("mouseover", e => {
      const { target } = e;

      if (target.tagName === "SPAN") {
        target.classList.add("hovered");
        console.log(`mousing over ${target.tagName}`);
      }
    });

    div.addEventListener("animationend", e => {
      const { target } = e;

      if (target.tagName === "SPAN") {
        target.classList.remove("hovered");
        console.log(`mousing over ${target.tagName}`);
      }
    });
  }

  …

  render() {
    const div = document.createElement("div");
    div.classList.add("header");
    this.addEventListeners(div);
    this.shadowRoot.appendChild(div);
    this.addSpans(div);
    this.addStyle();
  }
}
Collapse
 
aspittel profile image
Ali Spittel

Ah thank you for spotting that!

Collapse
 
washingtonsteven profile image
Steven Washington

Thanks for the intro to Web Components!

I'm curious how the browser knows what to render when it decides to "connect" your component. It seems like, from the naming, connectedCallback is called after the Dom expects something to exist, so there's a period of time while your function runs that there is nothing (not even a container) rendered? Or is that less a componentDidMount and more of a componentWillMount?

Also, is there any sort of poly fill or transplanting to get this in older browsers...or even just some automatic graceful degradation (like in your try..catch block).

Thanks again! This was very clear and straightforward intro!

Collapse
 
aspittel profile image
Ali Spittel

Awesome! Polymer is the best way to create web components that are cross-browser compatible. The API is somewhat different, but they are much more usable!

connectedCallback is called when the custom element is first connected to the document's DOM (according to the MDN documentation). So the container mounts to the DOM, then connectedCallback is triggered.

Collapse
 
smalluban profile image
Dominik Lubański

connectedCallback is called every time, when an element is attached to document, so also when you move an element from one parent to another. MDN definition:

connectedCallback: Invoked each time the custom element is appended into a document-connected element. This will happen each time the node is moved, and may happen before the element's contents have been fully parsed.

(developer.mozilla.org/en-US/docs/W...)

Collapse
 
jochemstoel profile image
Jochem Stoel • Edited

WebReflection on GitHub has a cross browser compatible polyfill of document.registerElement

Collapse
 
thipages_50 profile image
tit pa • Edited

Hi, thanks for this clear post.

Note that when running (Chrome 70), it says
[Deprecation] Element.createShadowRoot is deprecated
and will be removed in M73, around March 2019.
Please use Element.attachShadow instead.

Perhpas you may update the code (I couldnt ...)

Collapse
 
thipages_50 profile image
tit pa

works with

    if (!this.shadowRoot) {
        this.attachShadow({mode: 'open'});
    }
    //this.createShadowRoot();
Collapse
 
belhassen07 profile image
Belhassen Chelbi

I love your articles and your website is cool.
clapping

Collapse
 
smalluban profile image
Dominik Lubański • Edited

Very nice example of creating web components using only browser APIs :) If I may, in your full code example is few missing things:

  • this.createShadowRoot() is not defined, and it should be resistant to multiple calls (connectedCallback can be called multiple times)
  • The same problem is with your this.render() method. It appends new div, so every time it will be called (and it is called in connectedCallback), new div is appended to the shadowRoot. I assume, you should clear shadowRoot firstly (you can call this.shadowRoot.innerHTML = '').
  • Your post mentions about attributeChangedCallback, but the code example is not using it, so changing attributes does not re-render component (Also then you should use static computed property static get observedAttributes() - someone already wrote about it in a comment here)

Using raw APIs is really cool because we don't need any external tools for creating web components. However, then you and only you are responsible for matching standards, which ensure your custom element will work and will be usable :)

Did you consider using a 6kb library with an absolutely simple and unique API for building web components?

More or less, your example could be written like this (using hybrids library):

import { html, define } from 'hybrids';

function toggleSpanHover(host, { target }) {
  if (target.tagName.toLowerCase() === 'span') {
    target.classList.toggle('hovered');
  }
}

const RainbowText = {
  text: '',
  fontSize: 50,
  render: ({ text, fontSize }) => html`
    <div
      style="${{ fontSize: `${fontSize}px` }}"
      onmouseover="${toggleSpanHover}"
      onanimationend="${toggleSpanHover}"
    >
      ${text.split('').map(letter => html`
        <span>${letter}</span>
      `.key(letter))}
    </div>

    <style>
      /* your styles here */
    </style>
  `,
}

define('rainbow-text', RainbowText);

I know, that your whole idea was to not use libraries, but after all, they give you a solid abstraction on top of web APIs and ensures, that components work as expected.

Collapse
 
equinusocio profile image
Mattia Astorino

Hi, I wrote and article about the real scope of web components, which is extend the HTML.

You can read more about this point of view, that is also shared by Google Web Fundamentals

dev.to/clabuxd/web-componentsthe-r...

Collapse
 
vitalyt profile image
Vitaly Tomilov

This can help with writing DOM components in Vanilla JS:

github.com/vitaly-t/excellent

It simply turns all your DOM code into components, for better reusability, isolation, and instantiation patterns.

Collapse
 
007lva profile image
luigi

Safari already has web components

webkit.org/blog/4096/introducing-s...
webkit.org/blog/7027/introducing-c...

(I didn't try Shadow DOM yet, but at least native custom elements works fine to me)

Collapse
 
aspittel profile image
Ali Spittel

Oh cool! I was just going off of the MDN docs -- I don't really use Safari! Thanks for the heads up!

Collapse
 
maccabee profile image
Maccabee

Have you tried any of the other callbacks? I can't seem to get the attributeChangedCallback to fire.

Collapse
 
thatjoemoore profile image
Joseph Moore

For performance reasons, the browsers require you to define the list of attributes you want to listen to. You can do that by defining a static method called observedAttributes, which returns an array:

static get observedAttributes() {return ['my-attr', 'another-value']; }

That'll cause attributeChangedCallback to fire for changes to either of the listed attributes.

Collapse
 
maccabee profile image
Maccabee

Thanks a lot. I did get it to work with a setter but i still wanted to know how the callback works. I'll try it out when I can.

Collapse
 
adripanico profile image
Adrián Caballero

I assume that we will need to define the getStyle method, right?

Collapse
 
adripanico profile image
Adrián Caballero

OK, I found it in the complete example...

Collapse
 
plainjavascript profile image
plainJavaScript

You can use a CSS .header span instead of a .letter class for each span.

Collapse
 
annarankin profile image
Anna Rankin

Awesome!! This is the clearest writeup I've read of what web components actually do - thanks for the excellent run-through :D

Collapse
 
mashablair profile image
Maria Blair

This is such a great article! It serves as a very gentle intro to web components which I actually wanted to try for a while. Great job, Ali!