loading...
Cover image for Adding an intersectionObserver to any web component

Adding an intersectionObserver to any web component

btopro profile image Bryan Ollendyke Updated on ・4 min read

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.

count-up 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.
type-writer element

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.

Posted on by:

btopro profile

Bryan Ollendyke

@btopro

@elmsln @haxcamp @btopro #HAXTheWeb #drupal #webcomponents #edtech ✻ Full stack unicorn Adjunct professor teaching about webdev, ethics, and everything in between

Discussion

markdown guide
 

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 to IntersectionObserverMixin, I like that better :)