Skip to content
Detect if an Ember View or Component is in the viewport @ 60FPS
Branch: master
Clone or download
Latest commit 10fa9de May 24, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.github add-templates Aug 21, 2016
addon Fix didScroll (#196) May 24, 2019
app Begin unwinding use of mixin to support modifiers (#183) Apr 20, 2019
config Update 3.8 and prep for modifiers (#178) Mar 22, 2019
tests Bump IO library to 0.2.2 (#197) May 24, 2019
vendor Initial Commit from Ember CLI v0.2.3 Apr 13, 2015
.editorconfig Update ember-cli Jul 27, 2016
.ember-cli Initial Commit from Ember CLI v0.2.3 Apr 13, 2015
.eslintignore Extract static admin && update deps (#169) Nov 21, 2018
.eslintrc.js New API: watchElement for classes with no mixin (#185) Apr 23, 2019
.gitignore Update 3.8 and prep for modifiers (#178) Mar 22, 2019
.npmignore Update 3.8 and prep for modifiers (#178) Mar 22, 2019
.template-lintrc.js Update to 3.4 and resolve audit warning (#163) Sep 11, 2018
.travis.yml Bump IO library to 0.2.2 (#197) May 24, 2019
.watchmanconfig
CODE_OF_CONDUCT.md add-templates Aug 21, 2016
CONTRIBUTING.md add-templates Aug 21, 2016
LICENSE.md Update Ember CLI and deps Feb 5, 2016
README.md Update README May 14, 2019
ember-cli-build.js Update to 3.4 and resolve audit warning (#163) Sep 11, 2018
index.js Update to 3.4 and resolve audit warning (#163) Sep 11, 2018
package-lock.json 3.5.8 May 24, 2019
package.json 3.5.8 May 24, 2019
testem.js Update to 3.4 and resolve audit warning (#163) Sep 11, 2018

README.md

ember-in-viewport

Detect if an Ember View or Component is in the viewport @ 60FPS

ember-in-viewport is built and maintained by DockYard, contact us for expert Ember.js consulting.

Read the blogpost

Download count all time npm version Build Status Ember Observer Score

This Ember addon adds a simple, highly performant Ember Mixin to your app. This Mixin, when added to a Component, will allow you to check if that Component has entered the browser's viewport. By default, the Mixin uses the IntersectionObserver API if it detects it in your user's browser – failing which, it falls back to using requestAnimationFrame, then if not available, the Ember run loop and event listeners.

We utilize pooling techniques to reuse Intersection Observers and rAF observers in order to make your app as performant as possible and do as little works as possible.

Demo or examples

Installation

ember install ember-in-viewport

Usage

Usage is simple. First, add the Mixin to your Component:

import Component from '@ember/component';
import InViewportMixin from 'ember-in-viewport';

export default Component.extend(InViewportMixin, {
  // ...
});

Basic usage

Available hooks

didEnterViewport, didExitViewport

These hooks fire once whenever the Component enters or exits the viewport. You can handle them the same way you would handle any other native Ember hook:

import Component from '@ember/component';
import InViewportMixin from 'ember-in-viewport';

export default Component.extend(InViewportMixin, {
  didEnterViewport() {
    console.log('entered');
  },

  didExitViewport() {
    console.log('exited');
  }
});
didScroll(up,down,left,right)

The didScroll hook fires when an element enters the viewport. For example, if you scrolled down in order to move the element in the viewport, the didScroll hook would fire and also receive the direction as a string. You can then handle it like another hook as in the above example.

import Component from '@ember/component';
import InViewportMixin from 'ember-in-viewport';

export default Component.extend(InViewportMixin, {
  didScroll(direction) {
    console.log(direction); // 'up' || 'down' || 'left' || 'right'
  }
});
viewportEntered

To apply an .active class to your Component when it enters the viewport, you can simply bind the active class to the mixed in property viewportEntered, like so:

import Component from '@ember/component';
import InViewportMixin from 'ember-in-viewport';

export default Component.extend(InViewportMixin, {
  classNameBindings: [ 'viewportEntered:active' ]
});
viewportExited

This hook fires whenever the Component leaves the viewport.

Advanced usage (options)

The mixin comes with some options. Due to the way listeners and IntersectionObserver API or requestAnimationFrame is setup, you'll have to override the options this way:

import Component from '@ember/component';
import InViewportMixin from 'ember-in-viewport';
import { setProperties }  from '@ember/object';

export default Component.extend(InViewportMixin, {
  init() {
    this._super(...arguments);

    setProperties(this, {
      viewportEnabled                 : true,
      viewportUseRAF                  : true,
      viewportSpy                     : false,
      viewportScrollSensitivity       : 1,
      viewportRefreshRate             : 150,
      intersectionThreshold           : 0,
      scrollableArea                  : null,
      viewportTolerance: {
        top    : 50,
        bottom : 50,
        left   : 20,
        right  : 20
      }
    });
  }
});
  • viewportEnabled: boolean

    Default: true

    Set to false to have no listeners registered. Useful if you have components that function with either viewport listening on or off.

  • viewportUseIntersectionObserver: boolean

    Default: Depends on browser

    Read-only

    The Mixin by default will use the IntersectionObserver API. If IntersectionObserver is not supported in the target browser, ember-in-viewport will fallback to rAF. We prevent users from explicitly setting this to true as browsers lacking support for IntersectionObserver will throw an error.

    (https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) (https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/thresholds#Browser_compatibility)

  • intersectionThreshold: decimal or array

    Default: 0

    A single number or array of numbers between 0.0 and 1.0. A value of 0.0 means the target will be visible when the first pixel enters the viewport. A value of 1.0 means the entire target must be visible to fire the didEnterViewport hook. Similarily, [0, .25, .5, .75, 1] will fire didEnterViewport every 25% of the target that is visible. (https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Thresholds)

    Some notes:

    • If the target is offscreen, you will get a notification via didExitViewport that the target is initially offscreen. Similarily, this is possible to notify if onscreen when your site loads.
    • If intersectionThreshold is set to anything greater than 0, you will not see didExitViewport hook fired due to our use of the isIntersecting property. See last comment here: https://bugs.chromium.org/p/chromium/issues/detail?id=713819 for purpose of isIntersecting
    • To get around the above issue and have didExitViewport fire, set your intersectionThreshold to [0, 1.0]. When set to just 1.0, when the element is 99% visible and still has isIntersecting as true, when the element leaves the viewport, the element isn't applicable to the observer anymore, so the callback isn't called again.
    • If your intersectionThreshold is set to 0 you will get notified if the target didEnterViewport and didExitViewport at the appropriate time.
  • scrollableArea

    Default: null

    A CSS selector for the scrollable area. e.g. ".my-list"

  • viewportUseRAF: boolean

    Default: Depends on browser

    As its name suggests, if this is true and the IntersectionObserver API is not available in the target browser, the Mixin will use requestAnimationFrame. Unless you want to force enabling or disabling this, you won't need to override this option.

  • viewportSpy: boolean

    Default: false

    When true, the Mixin will continually watch the Component and re-fire hooks whenever it enters or leaves the viewport. Because this is expensive, this behaviour is opt-in. When false, the Mixin will only watch the Component until it enters the viewport once, and then it sets viewportEntered to true (permanently), and unbinds listeners. This reduces the load on the Ember run loop and your application.

    NOTE: If using IntersectionObserver (default), viewportSpy wont put too much of a tax on your application. However, for browsers (Safari) that don't currently support IntersectionObserver, we fallback to rAF. Depending on your use case, the default of false may be acceptable.

  • viewportDidScroll: boolean

    Default: true

    When true, the Mixin enables listening to the didScroll hook. This will become by default false in a future major release

  • viewportScrollSensitivity: number

    Default: 1

    This value determines the degree of sensitivity (in px) in which a DOM element is considered to have scrolled into the viewport. For example, if you set viewportScrollSensitivity to 10, the didScroll{...} hooks would only fire if the scroll was greater than 10px. Only applicable if IntersectionObserver and rAF are not applied.

  • viewportRefreshRate: number

    Default: 100

    If IntersectionObserver and requestAnimationFrame is not present, this value determines how often the Mixin checks your component to determine whether or not it has entered or left the viewport. The lower this number, the more often it checks, and the more load is placed on your application. Generally, you'll want this value between 100 to 300, which is about the range at which people consider things to be "real-time".

    This value also affects how often the Mixin checks scroll direction.

  • viewportTolerance: object

    Default: { top: 0, left: 0, bottom: 0, right: 0 }

    This option determines how accurately the Component needs to be within the viewport for it to be considered as entered. Add bottom margin to preemptively trigger didEnterViewport.

    For IntersectionObserver, this property interpolates to rootMargin. For rAF, this property will use bottom tolerance and measure against the height of the container to determine when to trigger didEnterViewport.

    Also, if your sentinel (component that uses this mixin) is a zero-height element, ensure that the sentinel actually is able to enter the viewport.

Global options

You can set application wide defaults for ember-in-viewport in your app (they are still manually overridable inside of a Component). To set new defaults, just add a config object to config/environment.js, like so:

module.exports = function(environment) {
  var ENV = {
    // ...
    viewportConfig: {
      viewportEnabled                 : false,
      viewportUseRAF                  : true,
      viewportSpy                     : false,
      viewportScrollSensitivity       : 1,
      viewportRefreshRate             : 100,
      viewportListeners               : [],
      intersectionThreshold           : 0,
      scrollableArea                  : null,
      viewportTolerance: {
        top    : 0,
        left   : 0,
        bottom : 0,
        right  : 0
      }
    }
  };
};

Note if you want to disable right and left in-viewport triggers, set these values to `Infinity`.

Modifiers

Using with Modifiers is easy. Note, modifiers currently only works to watch entering the viewport.

  1. Install @ember/render-modifiers
  2. Use the did-insert hook inside a component
  3. Wire up the component like so

Note - This is in lieu of a did-enter-viewport modifier, which we plan on adding in the future. Compared to the solution below, did-enter-viewport won't need a container (this) passed to it. But for now, to start using modifiers, this is the easy path.

import Component from '@ember/component';
import { set } from '@ember/object';
import InViewportMixin from 'ember-in-viewport';

export default Component.extend(InViewportMixin, {
  tagName: '',

  didInsertNode(element, [instance]) {
    instance.watchElement(element);
  },

  didInsertElement() {
    this._super(...arguments);
    set(this, 'viewportSpy', true);
    set(this, 'viewportTolerance', {
      bottom: 300
    });

    this._super(...arguments);
  },

  didEnterViewport() {
    // this will only work with one element being watched in the container. This is still a TODO to enable
    this.infinityLoad();
  }
});
<div {{did-insert this.didInsertNode this}}>
  {{yield}}
</div>

Classes

This allows you to absolve yourself from using a mixin in native classes!

import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import { inject as service } from '@ember/service'; // with polyfill

@tagName('')
export default class MyClass extends Component {
  @service inViewport

  didInsertElement() {
    super();
    const loader = document.getElementById('loader');
    const viewportTolerance = { bottom: 200 };
    const { onEnter, onExit } = this.inViewport.watchElement(loader, { viewportTolerance });
    onEnter(this.didEnterViewport.bind(this));
  }

  didEnterViewport() {
    // do some other stuff
    this.infinityLoad();
  },

  willDestroyElement() {
    // need to manage cache yourself if you don't use the mixin
    const loader = document.getElementById('loader');
    this.inViewport.stopWatching(loader);
  }
}

And with Classes + Modifiers!

import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import { inject as service } from '@ember/service'; // with polyfill

@tagName('')
export default class MyClass extends Component {
  @service inViewport

  didInsertNode(element, [instance]) {
    const viewportTolerance = { bottom: 200 };
    const { onEnter, onExit } = instance.inViewport.watchElement(element, { viewportTolerance });
    onEnter(instance.didEnterViewport.bind(instance));
  }

  didEnterViewport() {
    // do some other stuff
    this.infinityLoad();
  },

  willDestroyElement() {
    // need to manage cache yourself if you don't use the mixin
    const loader = document.getElementById('loader');
    this.inViewport.stopWatching(loader);
  }
}
<div {{did-insert this.didInsertNode this}}>
  {{yield}}
</div>

Options as the second argument to inViewport.watchElement include intersectionThreshold, scrollableArea, viewportSpy && viewportTolerance

IntersectionObserver's Browser Support

Out of the box

Chrome 51 [1]
Firefox (Gecko) 55 [2]
MS Edge 15
Internet Explorer Not supported
Opera [1] 38
Safari Safari Technology Preview
Chrome for Android 59
Android Browser 56
Opera Mobile 37
  • [1] Reportedly available, it didn't trigger the events on initial load and lacks isIntersecting until later versions.
  • [2] This feature was implemented in Gecko 53.0 (Firefox 53.0 / Thunderbird 53.0 / SeaMonkey 2.50) behind the preference dom.IntersectionObserver.enabled.

Running

Running Tests

  • ember test
  • ember test --serve

Building

  • ember build

For more information on using ember-cli, visit http://www.ember-cli.com/.

Legal

DockYard, Inc © 2015

@dockyard

Licensed under the MIT license

You can’t perform that action at this time.