0.0.16 • Published 5 years ago

@smoovy/scroller-core v0.0.16

Weekly downloads
15
License
MIT
Repository
github
Last release
5 years ago

@smoovy/scroller-core

Version Size

Core architecture to create any scroll experience you want!

Installation

npm install --save @smoovy/scroller-core

Usage

Extend from the base scroller and set your module:

import { Scroller } from '@smoovy/scroller-core';

class YourScroller extends Scroller {
  get moduleCtor() {
    return YourModuleCtor;
  }
}

Create your scroller like this (automatic initialization):

const target = document.querySelector('#scroller-target');
const scroller = new YourScroller(
  target,
  {
    // Optional config
  }
);

Read more about transformers, inputs and outputs in the architecture explanation below to learn how to properly configure your scroller

(Re)mapping delta

To merge two directions into one use the mapDelta method in your config:

{
  mapDelta: (delta) => {
    // This will force both X and Y values to be added onto X.
    // Also locking Y. No more changes in Y will occur from here on
    delta.x += delta.y;
    delta.y = 0;

    return delta;
  }
}

Scroll to

Programmatically scrolling to a specific position is also possible:

scroller.scrollTo({ y: 200 });

If your transformers and outputs support temporary overwriting of their config, you can pass it as the second argument.

As an example: I want to overwrite the animation speed in my TweenTransformer while the programmatic scrolling is processing:

scroller.scrollTo(
  { y: 200 },
  {
    transformer: {
      tween: {
        duration: 1000
      }
    }
  }
);

This will overwrite the duration until the transformer tells the module it's finished.

Trigger an update/recalc

To tell all the components to update you can simply use this:

scroller.update();

This will automatically be called if DOM mutations or resizes occur.

Disabling/Enabling input emissions

To disable or enable all further input emissions:

scroller.disableInputs();     // Disable
scroller.enableInputs(false); // Disable
scroller.enableInputs();      // Enable
scroller.enableInputs(true);  // Enable

Destroying the scroller

To throw away your beautiful scroller:

scroller.destroy();

The architecture

Architecture

📦 The Scroller Module

The core of each Scroller wrapper class is the ScrollerModule. The Scroller module consists of Transformers, Inputs and Outputs. It takes care of the data flow across the components registered by you, so you just need to extend the base module class and register your inputs, outputs and transformers:

import { ScrollerModule } from '@smoovy/scroller-core';

class YourCustomScrollerModule extends ScrollerModule {
  constructor(dom, config) {
    super(dom, config);

    // Register all inputs
    this.inputs = {
      dummyInput: new DummyInput(
        this.dom,
        this.config.input.dummyInput
      )
    };

    // Register all outputs
    this.outpus = {
      dummyOutput: new DummyOutput(
        this.dom,
        this.config.output.dummyOutput
      )
    };

    // Register all transformers
    this.transformers = {
      dummyTransformer: new DummyTransformer(
        this.dom,
        this.config.transformer.dummyTransformer
      )
    };
  }
}

You can take a look at this module for more details

📜 The Scroller itself

After you've written your custom ScrollerModule you can create a custom Scroller class like this:

import { Scroller } from '@smoovy/scroller-core';

class YourScroller extends Scroller {
  get moduleCtor() {
    return YourCustomModule;
  }
}

To get awesome auto completion support while using typescript you should set the module class as the first generic for the CoreScroller:

class YourScroller extends CoreScroller<YourCustomModule> {}

And to use your scroller:

const targetElement = document.body;

new Scroller(targetElement, {
  input: {
    inp1: { /* Config for inp1 */ }
  },
  output: {
    out1: { /* Config for out1 */ }
  },
  transformer: {
    tran1: { /* Config for tran1 */ }
  }
});

🔢 The input

An input is basically the component which is going to emit the delta changes. As an example, here's a simple input that's just emitting a delta change after one second:

import { ScrollerInput } from '@smoovy/scroller-core';

class TimeoutInput extends ScrollerInput {
  get defaultConfig() {
    return { duration: 1000 };
  }

  attach() {
    this.timeout = setTimeout(() => {
      this.emit({ x: 0, y: 200 });
    }, this.config.duration);
  }

  detach() {
    cancelTimeout(this.timeout);
  }
}

Registering the input in your module:

class YourCustomScrollerModule extends ScrollerModule {
  constructor(dom, config) {
    super(dom, config);

    this.inputs = {
      timeout: new TimeoutInput(
        this.dom,
        this.config.input.timeout
      )
    };
  }
}

Changing configuration for you input:

new YourScroller(document.body, {
  input: {
    timeout: {
      duration: 1500
    }
  }
})

Take a look at this input component for more details

🤖 The transformer

The transformer acts as a "middleware" between the input and output. It is responsible for notifying the output about any positon change. To get started with the transformer you need to understand how to use the virtualPosition to update the outputPosition.

Virtual position

The virtual position is basically the most up-to-date position regarding all the delta changes. So each time a delta change gets emitted it will be immediately added to the current virtualPosition in the ScrollerModule. So the virtual position is always ahead of the output position and should be used if you want to know the position the user is anticipating.

Output position

The output position is used by all outputs. This should be the position perceived by the user. E.g. if you have an animation this will be the current state (position) of it. Each Output is responsible for handling this value on its own.

Back to the transformer

So in your transformer you get the chance to manipulate the virtual position and output position. As an example, here is how you limit the virtual position to a range of your choice. In this case I've used the height and width of the scrollable content from the dom:

import { ScrollerTransformer } from '@smoovy/scroller-core';
import { clamp } from '@smoovy/utils';

class ClampTransformer extends ScrollerTransformer {
  /**
   * @param position The reference to the current virtual position
   */
  virtualTransform(position) {
    const wrapperSize = this.dom.wrapper.size;
    const containerSize = this.dom.container.size;
    const maxScrollX = Math.max(wrapperSize.width - containerSize.width, 0);
    const maxScrollY = Math.max(wrapperSize.height - containerSize.height, 0);

    position.x = clamp(position.x, 0, maxScrollX);
    position.y = clamp(position.y, 0, maxScrollY);
  }

  /**
   * @param position The reference to the current output position
   * @param update Update callback to call if you've made changes to the position
   * @param complete Complete callback to call if you're done making changes
   */
  outputTransform(position, update, complete) {
    // Since we don't want to make any changes to the output position
    // we can leave that to other transformers and resolve immediately
    complete();
  }
}

As an example on how to animate the output position you can take a look at this

🖨️ The output

The output is pretty simple. You get the output position so do something with it. Here's an example:

import { ScrollerOutput } from '@smoovy/scroller-core';

class CssTranslateOutput extends ScrollerOutput {
  update(position) {
    this.dom.wrapper.element.style.transform = `
      translate3d(${x}px, ${y}px, 0)
    `;
  }
}

For a more detailed version of the css translation output you can take a look at this

👂 Listening for changes

To listen for changes regarding the outputs, inputs and recalcs you can either register them in the config directly like this:

{
  on: {
    recalc: (position) => {},
    output: (position) => {},
    input: (position) => {}
  }
}

These will exist as long as the scroller is alive

You can also attach/detach the listeners later in your application:

const scroller = new Scroller(target, { ... });

scroller.onRecalc(() => {});
scroller.onOutput((position) => {});
scroller.onInput((position) => {});

To remove a previous mentioned listener you can simply call the remove method on your listener:

const listener = scroller.onOutput((position) => {});

listener.remove();
listener = undefined; // optional

☊ The DOM

Each output, input and transformer will be provided with a dom provider. This is just the base DOM structure which wraps around the target element's children. You should use this to get information from the elements inside the scrollable element. The rendered DOM looks like this:

<div id="your-target">
  <div class="smoovy-container">
    <div class="smoovy-wrapper">
      <!-- Previous children of "#your-target" -->
    </div>
  </div>
</div>

Lets say you want to read the height of the wrapper inside your Input:

this.dom.wrapper.size.height;

The dimensions will be updated via viewport changes, DOM mutations or manual updates

Configuring the DOM

You register your own wrapper and container by providing them in the config accordingly:

{
  dom: {
    elements: {
      container: document.querySelector('#custom-container'),
      wrapper: document.querySelector('#custom-wrapper')
    }
  }
}

Make sure to pass both, the container and the wrapper, if you're going to override the default scroller DOM elements

Configuring DOM observers

Since the scroller uses the @smoovy/observer package to trigger updates, you can configure that by passing the config to the options of the dom object:

{
  dom: {
    config: {
      observer: {
        mutationThrottle: 100,
        viewportThrottle: 100,
        mutators: [
          {
            target: document.documentElement,
            options: {
              subtree: true,
              characterData: true
            }
          }
        ]
      }
    }
  }
}

You can also disable the observer completely and handle all the updates manually:

{
  dom: {
    config: {
      observer: false
    }
  }
}

You can read more about the observer configuration here

⚠️ Attention

You should always be aware what your transformers etc. are doing, since there isn't a mechanism to detect collisions.

Development commands

// Serve with parcel
npm run serve

// Build with rollup
npm run build

// Run Jest unit tests
npm run test

// Run TSLinter
npm run lint

License

See the LICENSE file for license rights and limitations (MIT).

0.0.16

5 years ago

0.0.15

5 years ago

0.0.14

5 years ago

0.0.13

5 years ago

0.0.12

5 years ago

0.0.11

5 years ago

0.0.10

5 years ago

0.0.9

5 years ago

0.0.8

5 years ago

0.0.7

5 years ago

0.0.6

5 years ago

0.0.5

5 years ago

0.0.3

5 years ago

0.0.2

5 years ago

0.0.1

5 years ago