Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use IntersectionObserver if available #113

Merged
merged 4 commits into from
Feb 2, 2018

Conversation

snewcomer
Copy link
Collaborator

@snewcomer snewcomer commented Nov 27, 2017

IntersectionObserver API

This PR provides an easy upgrade path to use IntersectionObserver. This also doesn't have any effect on existing users and will fallback to rAF if not in target browser.

Also added two routes - infinity and infinity-scrollable to perf-test and test.

  • use IntersectionOverride by default
  • allow for scrollable area (a configuration option on the IntersectionObserver)
  • tests (including separate tests for IntersectionObserver, rAF and scrollEvent)
  • documentation

close #106 #67
Ref prev PR #112
Partial Fix #37

@snewcomer snewcomer force-pushed the intersection-observer branch 2 times, most recently from d68b0a3 to 9b3fd6e Compare November 27, 2017 05:43
@snewcomer
Copy link
Collaborator Author

snewcomer commented Nov 27, 2017

So this solution still keeps rAF and the scrollEvent code around. ember-spaniel provides an IntersectionObserver polyfill (that doesn't fallback to native if present). But I saw this PR and thought it may be good to look at this sol'n when this is merged. Thoughts? Plus this PR wouldn't require too much of a refactor for the users whereas the IntersectionObserver only would remove a lot of the config variables and be good for a major release....

bind(this, this._setViewportEntered, context)
);
if (get(this, 'viewportUseIntersectionObserver')) {
const { top, left, bottom, right } = this.viewportTolerance;
Copy link
Collaborator Author

@snewcomer snewcomer Nov 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe can make this a separate config item specific to IntersectionObserver instead of both IntersectionObserver and rAF. Thoughts?

const models = [...Array(10).fill().map(() => `https://s3.amazonaws.com/uifaces/faces/twitter/${images[(Math.random() * images.length) | 0]}/128.jpg`)];

export default Ember.Controller.extend({
models,
Copy link
Collaborator Author

@snewcomer snewcomer Nov 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This route uses optimistic rendering. Guessing we don't want to control this behavior and leave that up to the users? i.e. 2 requests will be made - first for the 1st set of records. While that is pending, it will trigger infinityLoad().

if (get(this, 'viewportUseIntersectionObserver')) {
const { top, left, bottom, right } = this.viewportTolerance;
const options = {
root: null,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another question - if null, the root is the layout view (document.documentElement). One case I have run up against before is the scrollable area is not the window but rather a confined area in the viewport.

Would you be in favor of a scrollableArea config variable?

@snewcomer snewcomer changed the title Use IntersectionObserver if set to true Use IntersectionObserver if available Dec 1, 2017
@snewcomer
Copy link
Collaborator Author

snewcomer commented Dec 5, 2017

Hey! Sry to be a bug. A review would be great when (or more apt 'if' :) ) somebody has time!

👀

@poteto @kellyselden @langalex

const elementId = get(this, 'elementId');

const context = get(this, 'scrollableArea') ? get(this, 'scrollableArea') : window;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could probably make a variable on this mixin instead of this ternary.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const context = get(this, 'scrollableArea') || window ?

@@ -0,0 +1,4 @@
import ScrollableRoute from './infinity-scrollable';

export default ScrollableRoute.extend({
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't necessarily like this, but to avoid duplication and allow for deterministic tests, I separated out each InfinityObserver, rAF, and scrolEvent options in the lib to three separate routes with a scrollable area.

@snewcomer
Copy link
Collaborator Author

snewcomer commented Dec 5, 2017

Also just to note, this PR would benefit ember-infinity (PR - i.e. got it working with this branch) && ember-light-table (related).

@BartTK
Copy link

BartTK commented Jan 12, 2018

I've tested this change on my project and it scrolls a lot smoother then the rAF approach. However I was disappointed with respect to the loading time. When having a few thousands components on a page using this mixin, it takes several seconds to load them. In my specific case, all components used the same scrollableArea and the same options. As a result, I could re-use a single IntersectionObserver instance that observes all components. This greatly improved the loading time.

@snewcomer snewcomer force-pushed the intersection-observer branch 2 times, most recently from 3765ea4 to 463774b Compare January 19, 2018 13:33
@snewcomer snewcomer mentioned this pull request Jan 19, 2018
1 task
@simonihmig
Copy link

Would love to see this land! 👍
Anything missing here? (except for the rebase)

@simonihmig simonihmig mentioned this pull request Jan 19, 2018
@snewcomer snewcomer force-pushed the intersection-observer branch 2 times, most recently from 44e4ce4 to 5742f46 Compare January 19, 2018 20:49
@snewcomer snewcomer self-assigned this Jan 19, 2018
@snewcomer
Copy link
Collaborator Author

I'm going to upgrade to 2.18 and use the new testing apis in a separate commit. Getting a test leaking/flakey test.

@snewcomer snewcomer force-pushed the intersection-observer branch 2 times, most recently from 5752a85 to 04267ec Compare January 19, 2018 22:28
@snewcomer
Copy link
Collaborator Author

snewcomer commented Jan 19, 2018

So since I hadn't got a review yet, I updated the pkg to 2.18 and changed everything over to the new testing apis and @ember module namespace. Part of it was to fix a flakey test since the qunit container changed from 2.7 to 2.17 and then it snowballed from there 🤦‍♂️

I know it isn't a very nice thing to do from a reviewers POV, so I can easily break the last two commits into another PR if everybody would like. However, since most of main changes are new files, upgrading didn't change much:

  1. Add IntersectionObserver API in in-viewport mixin (existing file)
  2. Add acceptance tests (totally new files).
  3. New routes for testing IntersectionObserver, rAF, and scroll events (totally new files)

@cibernox
Copy link
Contributor

@snewcomer is it possible to submit the update to 2.18 in a separate PR?

I haven't reviewed this because I'm not familiar with the codebase. TBH I don't know who is the best person to review it.

@snewcomer
Copy link
Collaborator Author

@BartTK great insight btw. Curious, how did you get one IntersectionObserver to rule them all? I’m thinking your scenario might be somewhat interesting....where we could also somehow use one IntersectionObserver to ‘observe’ each element instead of many IntersectionObservers to ‘observe’ one element. Not totally sure of the perf implications, but your observation makes it seem like we should look into this. (after this PR)

@BartTK
Copy link

BartTK commented Jan 23, 2018

You only need a single IntersectionObserver per scrollableArea/options pair. When creating the observer, add it to a static administration. Future components having the same scrollableArea and options can re-use that IntersectionObserver instead of creating a new one. Just call 'observe' again for the new component.

There are a few things to keep in mind. You need to do reference counting to know when to destroy the observer. Furthermore you cannot bind '_onIntersection' to 'this' anymore; a single callback will be used by multiple components. Instead of receiving a single entry, this callback now receives a list of entries. These entries only contain the DOM element, you need to maintain another administration to map that element to its component.

In my case, this resulted in a large performance boost with respect to the loading time of the page.

@snewcomer
Copy link
Collaborator Author

@BartTK cool 😎 so you forked and modified this PR then? Once this PR gets in, and of course if you have time, it would be a great PR!

README.md Outdated
- `viewportUseRAF: boolean`

Default: Depends on browser

As it's name suggests, if this is `true`, the Mixin will use `requestAnimationFrame` instead of the Ember run loop. Unless you want to force enabling or disabling this, you won't need to override this option.
As it's 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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its instead of it's

const elementId = get(this, 'elementId');

const context = get(this, 'scrollableArea') ? get(this, 'scrollableArea') : window;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const context = get(this, 'scrollableArea') || window ?


andThen(() => {
assert.equal(findAll('.infinity-svg-scrollEvent').length, 20);
// assert.equal(find('.infinity-scrollable-scrollEvent.inactive').length, 1, 'component is inactive after fetching more data');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented out test

return [...Array(10).fill().map(() => `${images[(Math.random() * images.length) | 0]}`)];
},
setupController(controller, model) {
set(controller, 'model', model);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the default behavior of setupController? Is there a reason to overwrite?

<div class="list" style="height: 500px; overflow: scroll;">
<ul>
{{#each model as |val|}}
<div class="infinity-svg">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indent? Only to be consistent with the next file where the each block is indented

@bcardarella bcardarella merged commit b0de191 into DockYard:master Feb 2, 2018
@Duder-onomy
Copy link

BOOM!

@snewcomer snewcomer deleted the intersection-observer branch July 19, 2018 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Consider refactor using IntersectionObserver API Elements taller than viewport never trigger hooks
7 participants