I'm commonly asked the following when doing web components trainings: "When do I know it's time to make a new element?". While a loaded question with many nuanced answers, here's a perfect mini-case study in when I arrived at this answer recently.
count-up
I wrote an element called count-up
a few months ago while does the typical "start up company" thing where it presents a number and then counts up. I didn't want this to start until you could see it, so I utilized the reasonably widely available class IntersectionObserver in order to detect when the user could see the element.
yarn add @lrnwebcomponents/count-up
The code this involved a connectedCallback
, disconnectedCallback
for clean up, and roping in a series of common callbacks / settings (illustrated lower in the article).
fast forward to today..
I was working on an element called type-writer
which is a fork of this Polymer 1 element to convert it to LitElement as well as use it in an up coming marketing site.
yarn add @lrnwebcomponents/type-writer
type-writer
had a problem though. It would start typing as soon as it was connected to the DOM so you might miss what it's doing. I made it work by itself, porting it from PolymerV1 to LitElement in about 20 minutes (it's a very simple element). And then I went about wiring up the IntersectionObserver in an identical pattern as I had before with count-up.
That phrase, if you think it or say it, is a potential indicator that it is time for a new element.
creating IntersectionObserverSuper.js
It's important to understand the difference between what is #usetheplatform vs #LitElementRocks and when you need to write a special type of class vs extending a base class.
Consider the following. If I wrote this, it would require LitElement:
class IntersectionObserver extends LitElement {}
But this isn't just a dependency problem because if I wrote this..
class IntersectionObserver extends HTMLElement {}
now you wouldn't be able to use my code in your PolymerElement, SkateJS, LitElement and any other baseclasses you've written (not even a HTMLVideoElement class extension).
So how do we solve this?
SuperClass
This calls for SuperClass
! A SuperClass
allows you to effectively mix bits of one element into another. In old school Polymer (v1/v2) these were called behaviors
but now #usetheplatform has provided us the ability to do this natively!
So what's it look like
yarn add @lrnwebcomponents/intersection-element
/**
* `IntersectionElementSuper`
* `Wiring to provide basic IntersectionObserver support to any web component`
*/
const IntersectionElementSuper = function(SuperClass) {
// SuperClass so we can write any web component library / base class
return class extends SuperClass {
/**
* Constructor
*/
constructor() {
super();
// listen for this to be true in your element
this.elementVisible = false;
// threasholds to check for, every 25%
this.IOThresholds = [0.0, 0.25, 0.5, 0.75, 1.0];
// margin from root element
this.IORootMargin = "0px";
// wait till at least 50% of the item is visible to claim visible
this.IOVisibleLimit = 0.5;
// drop the observer once we are visible
this.IORemoveOnVisible = true;
// delay in observing, performance reasons for minimum at 100
this.IODelay = 100;
}
/**
* HTMLElement specification
*/
connectedCallback() {
if (super.connectedCallback) {
super.connectedCallback();
}
// setup the intersection observer, only if we are not visible
if (!this.elementVisible) {
this.intersectionObserver = new IntersectionObserver(
this.handleIntersectionCallback.bind(this),
{
root: document.rootElement,
rootMargin: this.IORootMargin,
threshold: this.IOThresholds,
delay: this.IODelay
}
);
this.intersectionObserver.observe(this);
}
}
/**
* HTMLElement specification
*/
disconnectedCallback() {
// if we have an intersection observer, disconnect it
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
}
/**
* Very basic IntersectionObserver callback which will set elementVisible to true
*/
handleIntersectionCallback(entries) {
for (let entry of entries) {
let ratio = Number(entry.intersectionRatio).toFixed(2);
// ensure ratio is higher than our limit before trigger visibility
if (ratio >= this.IOVisibleLimit) {
this.elementVisible = true;
// remove the observer if we've reached our target of being visible
if (this.IORemoveOnVisible) {
this.intersectionObserver.disconnect();
}
}
}
}
};
};
export { IntersectionElementSuper };
How you implement this
Here's the relevant parts of the type-writer
web component (and count-up
is now and identical integration)
import { IntersectionElementSuper } from "@lrnwebcomponents/intersection-element/lib/IntersectionElementSuper.js";
class TypeWriter extends IntersectionElementSuper(LitElement) {
// properties available to the custom element for data binding
static get properties() {
return {
...
elementVisible: {
type: Boolean
},
...
};
}
/**
* LitElement life cycle - property changed
*/
updated(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
if (["text", "delay", "elementVisible"].includes(propName)) {
this._observeText(this.text, this.delay, this.elementVisible);
}
});
}
}
As you can see, now we just wrap our implementing class in IntersectionElementSuper()
and notice changes to the elementVisible
Boolean and we have the ability to notice and run callback functions based on the element being in the end user's viewport.
I hope this explains a real world example of making a new element, how to write and leverage SuperClass's in modern JavaScript, and the power of writing pieces of web components. Hopefully you'll be seeing count-up, type-writer and experiencing our intersection-element on the redesign of haxtheweb.org we're engaged in.
Top comments (2)
Nice article 👍 and a very good and real usecase 💪
I think this pattern is usually called a JavaScript Class Mixin 🤔
In our codebase we have a convention and call them what they do + Mixin. e.g. in this case it would probably be
ElementVisibleMixin
...Our reasoning for that is:
Why add
Mixin
? so it's easily find and distinguishable from normal classes.Why not
IntersectionObserverMixin
? what you want is to find out if an element is visible - that an Intersection Observer is used is an implementation detail and as a user I should not need to know this.Nice, I didn't have a convention for this as I've only written a few general purpose
SuperClass
. Renaming toIntersectionObserverMixin
, I like that better :)