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 tags with custom features. Of course, that wasn't what was really happening, but it helped lower the learning curve.
Now, you can actually build your own HTML tags using web components! They are still an experimental feature -- they work in Chrome and Opera, can be enabled in FireFox, but they are still unimplemented in Safari and Edge. Once they fully roll out, they will be an even more awesome tool for building reusable components in purely vanilla JavaScript -- no library or framework needed!
Learning Process
I had a lot of difficulty finding articles and examples on web components written in Vanilla JS. There are a bunch of examples and articles on Polymer, which is a framework for writing web components that includes polyfills for browsers that don't support web components yet. The framework sounds awesome, and I may try working with it in the future, but I wanted to use just vanilla JavaScript for this particular project.
I ended up mostly using the MDN documentation on the Shadow DOM in order to build my project. I also looked through CodePen and the WebComponents site, though I didn't find too much on either that was similar to what I wanted to build.
I also really liked Joseph Moore's article on web components which came out while I was working on this project! It covered some of the benefits of using web components:they work with all frameworks and they are simple to implement and understand since they just use vanilla JavaScript.
Final Project
On a lot of my projects, I use a similar design scheme for both personal branding and to make it so that I don't have to come up with a new design! In particular, I use a heading where each letter is a different color and has a falling animation on it. My personal site alispit.tel is a pretty good example of this! I also have that text on my resume, conference slides, and I have plans to use it for other sites in the near future as well! The catch with it is that CSS doesn't allow you to target individual characters -- other than the first one. Therefore, each letter has to be wrapped in a span
. This can get pretty painful to write, so I decided this was the perfect place to use a webcomponent!
Since I had difficulty finding articles on people writing web components, I'm going to go pretty in depth with the code here.
First, the HTML code to get the web component to render looks like this:
<rainbow-text text="hello world" font-size="100"></rainbow-text>
The web component is called rainbow-text
and it has two attributes: the text, which will be what the component renders, and the font size. You can also use slots
and templates
to insert content; however, in my use case, they would have added additional overhead. I wanted to input text and then output a series of HTML elements with the text separated by a character, so the easiest way was to pass in the text via an attribute -- especially with the Shadow DOM.
So, what is the Shadow DOM? It actually isn't new and it isn't specific to web components. The shadow DOM introduces a subtree of DOM elements with its own scope. It also allows us to hide child elements. For example, a video
element actually is a collection of HTML elements; however, when we create one and inspect it, we only see the video
tag! The coolest part of the shadow DOM, for me, was that the styling was scoped! If I add a style on my document that, for example, modifies all div
s, that style won't affect any element inside the shadow DOM. Inversely, styles inside the shadow DOM won't affect elements on the outer document's DOM. This is one of my favorite features of Vue, so I was super excited that I could implement something similar without a framework!
Let's now move on to JavaScript code which implements the custom element. First, you write a JavaScript class that extends the built-in HTMLElement
class. I used an ES6 class, but you could also use the older OOP syntax for JavaScript if you wanted. I really enjoy using ES6 classes, especially since I am so used to them from React! The syntax felt familiar and simple.
The first thing that I did was write the connectedCallback
lifecycle method. This is called automatically when the element is rendered -- similar to componentDidMount
in React. You could also use a constructor
similar to any other ES6 class; however, I didn't really have a need for one since I wasn't setting any default values or anything.
Inside the connectedCallback
, I first instantiated the shadow DOM for the element by calling this.createShadowRoot()
. Now, the rainbow-text
element is the root of its own shadow DOM, so it's child elements will be hidden and have their own scope for styling and external JavaScript mutations. Then, I set attributes within the class from the HTML attributes being passed in. Within the class, you can think of this
referring to the rainbow-text
element. Instead of running document.querySelector('rainbow-text').getAttribute('text')
, you can just run this.getAttribute('text')
to get the text
attribute from the element.
class RainbowText extends HTMLElement {
connectedCallback () {
this.createShadowRoot()
this.text = this.getAttribute('text')
this.size = this.getAttribute('font-size')
this.render()
}
render
is a method that I wrote, that is called in the connectedCallback
. You can also use the disconnectedCallback
and the attributeChangedCallback
lifecycle methods if they would be helpful in your code! I just separated it out in order to adhere to Sandi Metz's rules which I adhere to pretty religiously! The one thing in this method that is different from normal vanilla DOM manipulation is that I append the elements that I create to the shadowRoot
instead of the document
or to the element directly! This just attaches the element to the shadow DOM instead of the root DOM of the document.
render () {
const div = document.createElement('div')
div.classList.add('header')
this.shadowRoot.appendChild(div)
this.addSpans(div)
this.addStyle()
}
I then added the individual spans for each letter to the DOM, this is essentially identical to vanilla JavaScript code:
addSpanEventListeners (span) {
span.addEventListener('mouseover', () => { span.classList.add('hovered') })
span.addEventListener('animationend', () => { span.classList.remove('hovered') })
}
createSpan (letter) {
const span = document.createElement('span')
span.classList.add('letter')
span.innerHTML = letter
this.addSpanEventListeners(span)
return span
}
addSpans (div) {
[...this.text].forEach(letter => {
let span = this.createSpan(letter)
div.appendChild(span)
})
}
Finally, I added the styling to the shadow DOM:
addStyle () {
const styleTag = document.createElement('style')
styleTag.textContent = getStyle(this.size)
this.shadowRoot.appendChild(styleTag)
}
This method adds a style
tag to the shadow DOM to modify the elements within it. I used a function to plug in the font-size of the header to a template literal that contained all of the CSS.
After writing the component, I had to register my new element:
try {
customElements.define('rainbow-text', RainbowText)
} catch (err) {
const h3 = document.createElement('h3')
h3.innerHTML = "This site uses webcomponents which don't work in all browsers! Try this site in a browser that supports them!"
document.body.appendChild(h3)
}
I also added a warning for users on non-webcomponent friendly browsers!
Here's how the element ended up showing up in the console:
Next Steps
I enjoyed working with web components! The idea of being able to create reusable components without a framework is awesome. The one I built will be really helpful for me since I use the multi-colored name so often. I will just include the script
in other documents. I won't convert my personal site to using the component, though, since I want that to be supported across browsers. There also isn't a clear system for state or data management, which makes sense given the goal for web components; however, it does make other frontend frameworks still necessary. I think I will keep using frontend frameworks for these reasons; however, once they are fully supported, they will be awesome to use!
Full Code
Example Use - (doesn't use webcomponents)
Part of my On Learning New Things Series
Top comments (22)
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 thetext
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.
Ah thank you for spotting that!
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 acomponentDidMount
and more of acomponentWillMount
?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!
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, thenconnectedCallback
is triggered.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:(developer.mozilla.org/en-US/docs/W...)
WebReflection on GitHub has a cross browser compatible polyfill of document.registerElement
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 ...)
works with
I love your articles and your website is cool.
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)this.render()
method. It appends newdiv
, so every time it will be called (and it is called inconnectedCallback
), new div is appended to theshadowRoot
. I assume, you should clearshadowRoot
firstly (you can callthis.shadowRoot.innerHTML = ''
).attributeChangedCallback
, but the code example is not using it, so changing attributes does not re-render component (Also then you should use static computed propertystatic 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):
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.
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...
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.
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)
Oh cool! I was just going off of the MDN docs -- I don't really use Safari! Thanks for the heads up!
Have you tried any of the other callbacks? I can't seem to get the attributeChangedCallback to fire.
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:
That'll cause attributeChangedCallback to fire for changes to either of the listed attributes.
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.
I assume that we will need to define the getStyle method, right?
OK, I found it in the complete example...