MCPcopy Index your code
hub / github.com/DockYard/ember-in-viewport

github.com/DockYard/ember-in-viewport @v4.1.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release v4.1.0 ↗ · + Follow
153 symbols 296 edges 64 files 33 documented · 22% 1 cross-repo links updated 2y agov3.10.3 · 2021-11-29★ 24110 open issues
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

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 GitHub Actions Build Status Ember Observer Score

This Ember addon adds a simple, highly performant Service or modifier to your app. This library will allow you to check if a Component or DOM element has entered the browser's viewport. By default, this uses the IntersectionObserver API if it detects it the DOM element is 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

  • Lazy loading responsive images (see dummy-artwork for an example artwork component). Visit http://localhost:4200/infinity-modifier to see it in action
  • Dummy app (ember serve): https://github.com/DockYard/ember-in-viewport/tree/master/tests/dummy
  • Use with Ember Modifiers and @ember/render-modifiers
  • Use with Native Classes
  • ember-infinity
  • ember-light-table
  • Tracking advertisement impressions
  • Occlusion culling

Table of Contents

Installation

ember install ember-in-viewport

Usage

Usage is simple. First, inject the service to your component and start "watching" DOM elements.

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

export default class MyClass extends Component {
  @service inViewport

  @action
  setupInViewport() {
    const loader = document.getElementById('loader');
    const viewportTolerance = { bottom: 200 };
    const { onEnter, _onExit } = this.inViewport.watchElement(loader, { viewportTolerance });
    // pass the bound method to `onEnter` or `onExit`
    onEnter(this.didEnterViewport.bind(this));
  }

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

  willDestroy() {
    // need to manage cache yourself
    const loader = document.getElementById('loader');
    this.inViewport.stopWatching(loader);

    super.willDestroy(...arguments);
  }
}
<ul>
  <li></li>
  ...
</ul>





You can also use Modifiers as well. Using modifiers cleans up the boilerplate needed and is shown in a later example.

Configuration

To use with the service based approach, simply pass in the options to watchElement as the second argument.

import Component from '@glimmer/component';
import { inject as service }  from '@ember/service';

export default class MyClass extends Component {
  @service inViewport

  @action
  setupInViewport() {
    const loader = document.getElementById('loader');

    const { onEnter, _onExit } = this.inViewport.watchElement(
      loader,
      {
        viewportTolerance: { bottom: 200 },
        intersectionThreshold: 0.25,
        scrollableArea: '#scrollable-area'
      }
    );
  }
}

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: {
      viewportUseRAF                  : true,
      viewportSpy                     : false,
      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.

You can either use our built in modifier {{in-viewport}} or a more verbose, but potentially more flexible generic modifier. Let's start with the former.

  1. Use {{in-viewport}} modifier on target element
  2. Ensure you have a callbacks in context for enter and/or exit
  3. options are optional - see Advanced usage (options)
<ul class="list">
  <li></li>
  <li></li>



    List sentinel



</ul>

This modifier is useful for a variety of scenarios where you need to watch a sentinel. With template only components, functionality like this is even more important! If you have logic that currently uses the did-insert modifier to start watching an element, try this one out!

If you need more than our built in modifier...

  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 '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

export default class MyClass extends Component {
  @service inViewport

  @action
  setupInViewport() {
    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();
  }

  willDestroy() {
    // need to manage cache yourself
    const loader = document.getElementById('loader');
    this.inViewport.stopWatching(loader);

    super.willDestroy(...arguments);
  }
}



  {{yield}}



Options as the second argument to inViewport.watchElement include: - 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: string | HTMLElement

Default: null

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

  • viewportSpy: boolean

Default: false

viewportSpy: true is often useful when you have "infinite lists" that need to keep loading more data. viewportSpy: false is often useful for one time loading of artwork, metrics, etc when the come into the viewport.

If you support IE11 and detect and run logic onExit, then it is necessary to have this true to that the requestAnimationFrame watching your sentinel is not torn down.

When true, the library 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 intersection observer will only watch the Component until it enters the viewport once, and then it 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 < 12.1) that don't currently support IntersectionObserver, we fallback to rAF. Depending on your use case, the default of false may be acceptable.

  • 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 (the watched element) is a zero-height element, ensure that the sentinel actually is able to enter the viewport.

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

  • ember serve
  • Visit your app at http://localhost:4200.

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

Contributors

We're grateful to these wonderful contributors who've contributed to ember-in-viewport:

<im

Core symbols most depended-on inside this repo

add
called by 12
addon/-private/raf-admin.js
isInViewport
called by 9
addon/utils/is-in-viewport.js
stopWatching
called by 9
addon/services/in-viewport.js
watchElement
called by 8
addon/services/in-viewport.js
setupWatcher
called by 4
addon/modifiers/in-viewport.js
destroyWatcher
called by 4
addon/modifiers/in-viewport.js
infinityLoad
called by 4
tests/dummy/app/controllers/infinity.js
remove
called by 3
addon/-private/raf-admin.js

Shape

Method 97
Class 40
Function 16

Languages

TypeScript100%

Modules by API surface

tests/dummy/app/components/dummy-artwork.js27 symbols
addon/services/in-viewport.js20 symbols
addon/modifiers/in-viewport.js14 symbols
addon/-private/raf-admin.js11 symbols
tests/dummy/app/controllers/infinity-custom-element.js8 symbols
tests/dummy/app/controllers/infinity-built-in-modifiers.js8 symbols
addon/-private/observer-admin.js8 symbols
tests/dummy/app/components/my-modifier.js6 symbols
tests/dummy/app/components/my-component.js6 symbols
tests/dummy/app/components/my-class.js5 symbols
tests/dummy/app/controllers/infinity-modifier.js4 symbols
tests/dummy/app/controllers/infinity-class.js4 symbols

Used by 1 indexed graphs manifest dependencies, hub-wide

For agents

$ claude mcp add ember-in-viewport \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact