Create your own behavior

This is an advanced topic and requires JavaScript and DOM knowledge. You can use Scrollmeister without writing any JavaScript at all. However, if you need something custom or integrate with libraries like D3.js, Three.js or Google Maps, then a custom behavior is the way to go.

In this tutorial you'll learn how to create your own behavior and get familiar with the behavior lifecycle and API.

What do we want our behavior to do?

Let's recreate the iconic skrollr demo of rotating gradients. What we want is a behavior that changes the background of the element it is attached to as you scroll. The background will be a linear gradient that rotates and changes color.

You can follow along this tutorial using CodePen. It has the basic HTML set up and Scrollmeister included. Make sure to open the JavaScript console because we are going to interact with the DOM.

Your first behavior

Let's dive right into it and add the following JavaScript:

class RotatingGradientBehavior extends Scrollmeister.Behavior {
  static get behaviorSchema() {
    return {};
  }

  static get behaviorName() {
    return 'rotating-gradient';
  }

  static get behaviorDependencies() {
    return [];
  }

  behaviorDidAttach() {
    alert('attached!');
  }
}

Now add the rotating-gradient attribute to the element:

<element-meister rotating-gradient layout="height: 50vh; spacing: 50vh;"></element-meister>

Once the page reloads you should see the alert pop up. What happened?

  1. You created a new behavior class by extending Scrollmeister.Behavior
    • The name of the behavior is rotating-gradient, which defines the name of the HTML attribute
  2. You registered the behavior with Scrollmeister so it can be used
  3. You added the attribute to the element in your HTML
  4. By adding the attribute Scrollmeister automatically attached the behavior to the element
    • It created an instance of RotatingGradientBehavior and set everything up for you
    • It made the instance available in the DOM as element.rotatingGradient
    • It called the behaviorDidAttach method to let you now the behavior is now attached to an element

Go ahead, open the JavaScript console and enter the following

document.querySelector('element-meister').removeAttribute('rotating-gradient');
document.querySelector('element-meister').setAttribute('rotating-gradient', '');

You will see the alert pop up again. Scrollmeister detached and destroyed the behavior and then attached a new instance.

Changing styles

Now let's actually add a gradient background in behaviorDidAttach:

class RotatingGradientBehavior extends Scrollmeister.Behavior {
  static get behaviorSchema() {
    return {};
  }

  static get behaviorName() {
    return 'rotating-gradient';
  }

  static get behaviorDependencies() {
    return [];
  }

  behaviorDidAttach() {
    this.el.style.backgroundImage = 'linear-gradient(180deg, green, blue)';
  }
}

Scrollmeister.registerBehavior(RotatingGradientBehavior);

Boom, colors. Now if you go ahead and remove the behavior

document.querySelector('element-meister').removeAttribute('rotating-gradient');

you will notice that the background is still a gradient.

Cleaning up

The rule for Scrollmeister behaviors is that they have to leave the DOM as clean as they found it. No matter if you change styles, add new elements or subscribe to a WebSocket, you need to clean up once the behavior is detached. Coincidentally every behavior can have a behaviorWillDetach method which is called once the attribute is removed from the element. Let's reset the background:

class RotatingGradientBehavior extends Scrollmeister.Behavior {
  static get behaviorSchema() {
    return {};
  }

  static get behaviorName() {
    return 'rotating-gradient';
  }

  static get behaviorDependencies() {
    return [];
  }

  behaviorDidAttach() {
    this.el.style.backgroundImage = 'linear-gradient(180deg, green, blue)';
  }

  behaviorWillDetach() {
    this.el.style.backgroundImage = '';
  }
}

Scrollmeister.registerBehavior(RotatingGradientBehavior);

now if you remove the behavior the gradient will disappear as well.

document.querySelector('element-meister').removeAttribute('rotating-gradient');

Don't repeat yourself

There is an even easier way to clean up styles in Scrollmeister. Every behavior has a magic style property. You can use you just like el.style but it has super powers. It will autoprefix CSS transforms, merge CSS transforms from multiple behaviors, warn you when multiple behaviors update the same style and it will remove the styles when the behavior is detached. Scrollmeister offers the same magic for event listeners and appending new elements, but that's another story.

Let's simplify our behavior:

class RotatingGradientBehavior extends Scrollmeister.Behavior {
  static get behaviorSchema() {
    return {};
  }

  static get behaviorName() {
    return 'rotating-gradient';
  }

  static get behaviorDependencies() {
    return [];
  }

  behaviorDidAttach() {
    this.style.backgroundImage = 'linear-gradient(180deg, green, blue)';
  }
}

Scrollmeister.registerBehavior(RotatingGradientBehavior);

DJ, spin that shit!

So far all we got is a linear gradient at 180deg, which is boring af. What we wanted was a gradient that rotates as you scroll. To animate something in Scrollmeister you will come along the interpolate behavior a lot. It does all the nitty-gritty for you and exposes the interpolated values. You will also find that Scrollmeister uses composition over inheritance. You basically add all the behaviors to your element that you want and they can also depend on each other.

Let's start by telling Scrollmeister that our behavior depends on the interpolate behavior:

class RotatingGradientBehavior extends Scrollmeister.Behavior {
  static get behaviorSchema() {
    return {};
  }

  static get behaviorName() {
    return 'rotating-gradient';
  }

  static get behaviorDependencies() {
    return ['interpolate'];
  }

  behaviorDidAttach() {
    this.style.backgroundImage = 'linear-gradient(180deg, green, blue)';
  }
}

Scrollmeister.registerBehavior(RotatingGradientBehavior);

Once the page reloads you should see an error message in the console because we haven't added the interpolate behavior yet.

The static dependencies getter returns an unordered list of behaviors. It serves two purposes:

  1. Scrollmeister will make sure to initialize behaviors in the correct order. When your behaviorDidAttach method is called, this.el.interpolate is guaranteed to be defined.
  2. Scrollmeister will throw if you forget a dependency. This is mostly useful for the users of your behavior and causes the big red error message.

Let's add the interpolate behavior to get rid of the error message:

<element-meister interpolate rotating-gradient layout="height: 50vh; spacing: 50vh;"></element-meister>

Error message gone, still not spinning. Bummer.

Connecting behaviors

The interpolate behavior has a list of parameters that can be freely interpolated as the element moves across the viewport. These parameters are named progress, opacity, rotate, scale, alpha, beta, gamma, delta and epsilon. By default the progress parameter is already defined and interpolates from 0 to 1 as the element progressed through the viewport. Let's hook our gradient up to the progress parameter.

The interpolate behavior exposes the current parameter values as a values instance variable. So you can access this.el.interpolate.values.progress at all times to get the current progress. However, we want to get notified when the value changes to updated our gradient.

Your first though might be to add an event listener to the interpolate:change event in the behaviorDidAttach method. And that's actually how behaviors are connected in Scrollmeister. However, there are some edge cases that need to be handled, for example if the rotating-gradient behavior is attached lazily using setAttribute. But don't worry, Scrollmeister got you covered. Connecting the two behaviors is as simple as:

class RotatingGradientBehavior extends Scrollmeister.Behavior {
  static get behaviorSchema() {
    return {};
  }

  static get behaviorName() {
    return 'rotating-gradient';
  }

  static get behaviorDependencies() {
    return ['interpolate'];
  }

  behaviorDidAttach() {
    this.connectTo('interpolate', interpolateBehavior => {
      let angle = interpolateBehavior.values.progress * 360;
      this.style.backgroundImage = `linear-gradient(${angle}deg, green, blue)`;
    });
  }
}

Scrollmeister.registerBehavior(RotatingGradientBehavior);

It...rotates...what the hell just happened?

  1. We connected the rotating-gradient behavior to the interpolate behavior. This guarantees that the function is always executed when something changes and we are always in sync.
  2. The function gets the instance of the interpolate behavior as a convenience. We could always access it via this.el.interpolate as well.
  3. We access values.progress to get the current value of the progress parameter.
  4. We update the angle of the gradient depending on the parameter. It will transition between 0deg and 360deg.

Props to the behavior

So far our behavior is pretty simple. I mean I like rotating gradients as much as everyone else, but if we would add the behavior to three elements we would get the same look three times. Let's make the two colors configurable by the users of our behavior.

To pass data from HTML to the behavior we use the value of the attribute using key-value-pairs. We call these props. Here's what we want the API of the rotating-gradient behavior to look like:

<element-meister
  interpolate
  rotating-gradient="startColor: green; endColor: purple;"
  layout="height: 50vh; spacing: 50vh;"
></element-meister>

Once the page reloads you should see a big red error message because the behavior does not expect these props. Let's define them in our schema:

class RotatingGradientBehavior extends Scrollmeister.Behavior {
  static get behaviorSchema() {
    return {
      startColor: {
        type: 'string',
        default: 'green'
      },
      endColor: {
        type: 'string',
        default: 'blue'
      }
    };
  }

  static get behaviorName() {
    return 'rotating-gradient';
  }

  static get behaviorDependencies() {
    return ['interpolate'];
  }

  behaviorDidAttach() {
    this.connectTo('interpolate', interpolateBehavior => {
      let angle = interpolateBehavior.values.progress * 360;
      this.style.backgroundImage = `linear-gradient(${angle}deg, green, blue)`;
    });
  }
}

Scrollmeister.registerBehavior(RotatingGradientBehavior);

The error message is gone, but so far the gradient is still the same. We can access the props using this.props.startColor anywhere in our behavior. Let's use them!

class RotatingGradientBehavior extends Scrollmeister.Behavior {
  static get behaviorSchema() {
    return {
      startColor: {
        type: 'string',
        default: 'green'
      },
      endColor: {
        type: 'string',
        default: 'blue'
      }
    };
  }

  static get behaviorName() {
    return 'rotating-gradient';
  }

  static get behaviorDependencies() {
    return ['interpolate'];
  }

  behaviorDidAttach() {
    this.connectTo('interpolate', interpolateBehavior => {
      let angle = interpolateBehavior.values.progress * 360;
      this.style.backgroundImage = `linear-gradient(${angle}deg, ${this.props.startColor}, ${this.props.endColor})`;
    });
  }
}

Scrollmeister.registerBehavior(RotatingGradientBehavior);

The colors can now be changed by updating the attribute/props in your HTML.

Updating props at runtime

So far the props only update when we reload the page. What if we want to change them on-the-fly, for example in reaction to a user click? Don't worry, Scrollmeister got you covered. The naΓ―ve way would be to call setAttribute and overwrite all the props. This would work and the behavior would update (the same instance). However, you likely don't want to change all the props all the time and you also don't want the overhead of parsing all of them. That's why all props are defined as getters and setters directly on the instance. Run the following in your console and see the gradient change:

document.querySelector('element-meister').rotatingGradient.startColor = 'yellow';

If you need to react to prop changes manually you can also define a update(prevProps) method on your behavior. But as I already said connectTo handles all the nitty-gritty already and makes sure prop changes are handled as well. Magical.

Rainbows

If you're like me then a two-color gradient doesn't excite you as much anymore. What if...we could accept arbitrary color stops? I want it to look like this in our HTML:

<element-meister
  interpolate
  rotating-gradient="colors: red, yellow, green, blue, purple;"
  layout="height: 50vh; spacing: 50vh;"
></element-meister>

Let's update the schema and rendering:

class RotatingGradientBehavior extends Scrollmeister.Behavior {
  static get behaviorSchema() {
    return {
      colors: {
        type: ['string'],
        default: 'green, blue'
      }
    };
  }

  static get behaviorName() {
    return 'rotating-gradient';
  }

  static get behaviorDependencies() {
    return ['interpolate'];
  }

  behaviorDidAttach() {
    this.connectTo('interpolate', interpolateBehavior => {
      let angle = interpolateBehavior.values.progress * 360;
      this.style.backgroundImage = `linear-gradient(${angle}deg, ${this.props.colors.join(',')})`;
    });
  }
}

Scrollmeister.registerBehavior(RotatingGradientBehavior);

Mind. Blown.

Here's how the result looks on my end https://codepen.io/Prinzhorn/pen/OZpVYy?editors=1010

Make the parameters configurable

We are basically done with our behavior. However, we have hard-coded the interpolate parameter to be progress. This works great in our limited test case, but as you might have guessed an element can have any number of behaviors and there should be as few conflicts as possible. Say someone wants to combine your rotating-gradient behavior with the scrub behavior. This will work out-of-the-box, but the progress parameter is used for both scrubbing through the video progress as well as rotating the gradient. What if you want to decouple them? The solution is simple: add a prop to your behavior so users of it can define a mapping for which parameter they want to use. For example they could map gamma to the rotation of the gradient. I'll leave this open as an exercise for the reader.

Conclusion

Behaviors are the shit.