• Stars
    star
    246
  • Rank 164,726 (Top 4 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 9 years ago
  • Updated 6 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

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

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>
<div id="loader"></div>

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>
  <div {{in-viewport onEnter=(fn this.onEnter artwork) onExit=this.onExit scrollableArea=".list"}}>
    List sentinel
  </div>
</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);
  }
}
<div {{did-insert this.setupInViewport}}>
  {{yield}}
</div>

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

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:

More Repositories

1

ember-composable-helpers

Composable helpers for declarative templating in Ember
JavaScript
636
star
2

elixir-mail

Build composable mail messages
Elixir
386
star
3

ember-route-action-helper

Bubble closure actions in routes
JavaScript
330
star
4

ember-admin

Admin backend for ember-cli projects
JavaScript
241
star
5

ember-service-worker

A pluggable approach to Service Workers for Ember.js
JavaScript
238
star
6

flame_on

Flame Graph LiveView Component and LiveDashboard plugin
Elixir
228
star
7

ember-router-scroll

πŸ—” Scroll to top with preserved browser history scroll position.
JavaScript
204
star
8

ember-async-button

Async Button Component for Ember CLI apps
JavaScript
173
star
9

inquisitor

Composable query builder for Ecto
Elixir
170
star
10

ecto_fixtures

Fixtures for Elixir apps
Elixir
168
star
11

openid_connect

Elixir
66
star
12

eslint-plugin-ember-suave

DockYard's ESLint plugin for Ember apps
JavaScript
53
star
13

courier

Elixir
53
star
14

ember-cart

Shopping cart primitives for Ember
JavaScript
53
star
15

valid_field

Elixir
48
star
16

rein

Reinforcement Learning tooling built with Nx
Elixir
42
star
17

json_api_assert

Composable assertions for JSON API payload
Elixir
38
star
18

live_view_demo

Forkable repo for entries in Phoenix Phrenzy (https://phoenixphrenzy.com/)
Elixir
35
star
19

ember-service-worker-asset-cache

JavaScript
28
star
20

svelte-inline-compile

JavaScript
27
star
21

ember-cli-custom-assertions

Add custom QUnit assertions to your ember-cli test suite
JavaScript
26
star
22

ember-app-shell

JavaScript
24
star
23

easing

Elixir
22
star
24

design-sprints

HTML
22
star
25

ember-i18n-to-intl-migrator

Migrate ember-i18n to ember-intl
JavaScript
21
star
26

ember-service-worker-index

An Ember Service Worker plugin that caches the index.html file
JavaScript
20
star
27

ember-cli-deploy-compress

Compress your assets automatically choosing the best compression available for your browser targets
JavaScript
18
star
28

laptop-install

Shell
17
star
29

narwin-pack

Package of PostCSS plugins DockYard utilizes for PostCSS based projects!
JavaScript
16
star
30

ember-maybe-in-element

Conditionally render content elsewhere using #-in-element on ember apps
JavaScript
15
star
31

ember-service-worker-cache-fallback

JavaScript
15
star
32

inquisitor_jsonapi

JSON API Matchers for Inquisitor
Elixir
14
star
33

ember-one-way-select

JavaScript
10
star
34

svelte-inline-component

Utility and vite plugin to allow to create your own inline svelte components in tests
JavaScript
9
star
35

live_view_events

A simple library to unify sending and receiving messages in LiveView components
JavaScript
8
star
36

disklavier

Elixir
8
star
37

canon

All the must-read articles and must-watch videos for the DockYard engineering team.
8
star
38

plausible_proxy

An Elixir Plug to proxy calls to Plausible through your server
Elixir
7
star
39

netcdf

Elixir NetCDF Bindings
Rust
7
star
40

ember-service-worker-cache-first

JavaScript
7
star
41

qunit-notifications

Web Notifications support for QUnit in-browser test suites
JavaScript
6
star
42

auth_test_support

Authentication and authorization test functions
Elixir
4
star
43

broccoli-json-concat

JavaScript
3
star
44

boat-tracker

Elixir
3
star
45

ember-load-css

Ember CLI wrapper for loadCSS
JavaScript
3
star
46

drive-in-privacy-policy

2
star
47

boston_elixir

LiveView Native Workshop for Boston Elixir
Elixir
2
star
48

ketch

Simple proof-of-concept web application built with Next.js, Storybook, and Firebase.
JavaScript
1
star
49

courier_web

JavaScript
1
star
50

stylelint-config-narwin

DockYard stylelint configuration
JavaScript
1
star
51

liveview_tailwind_demo

Demo showing TailWind 3 integration in a Phoenix LiveView project
Elixir
1
star
52

ember-qunit-notifications

tomster-ified qunit-notifications
1
star
53

formation

Example of Phoenix LiveView Form logic
Elixir
1
star