Adding an intersectionObserver to any web component
Tags: webcomponents, superclass, intersectionobserver, javascript
Published: 2020-02-07 15:30:03

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](https://caniuse.com/#search=IntersectionObserver) in order to detect when the user could see the element.

![count-up element](https://dev-to-uploads.s3.amazonaws.com/i/njdislcc3c8vh2ayn6fw.png)

```bash
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](https://github.com/PolymerEl/type-writer) to convert it to [LitElement](https://lit-element.polymer-project.org/) as well as use it in an up coming marketing site.
![type-writer element](https://dev-to-uploads.s3.amazonaws.com/i/vxgs04o5e4bovfvm7ui8.png)

```bash
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:
```js
class IntersectionObserver extends LitElement {}
```
But this isn't just a dependency problem because if I wrote this..
```js
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

```bash
yarn add @lrnwebcomponents/intersection-element
```
```js
/**
 * `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`](https://github.com/elmsln/lrnwebcomponents/blob/master/elements/type-writer/type-writer.js#L281-L287) web component (and [`count-up`](https://github.com/elmsln/lrnwebcomponents/blob/master/elements/count-up/count-up.js#L16) is now and identical integration)
```js
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](https://haxtheweb.org) we're engaged in.