Skip to content

Commit

Permalink
Bug: stop watching unless viewportSpy=true is passed to modifier (#270)
Browse files Browse the repository at this point in the history
* Bug: stop watching unless viewportSpy=true is passed to modifier

* mv around

* fix teset

* fixup modifer case

* fxi lint

* add test

* fix name

* beef up README
  • Loading branch information
snewcomer authored Apr 27, 2021
1 parent 7c0f821 commit f630496
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 88 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,14 @@ export default Component.extend(InViewportMixin, {

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 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.
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.

- `viewportDidScroll: boolean`

Expand Down
4 changes: 4 additions & 0 deletions addon/modifiers/in-viewport.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export default class InViewportModifier extends Modifier {
if (this.args.named.onEnter) {
this.args.named.onEnter.call(null, this.element);
}

if (!this.options.viewportSpy) {
this.inViewport.stopWatching(this.element);
}
}

@action
Expand Down
22 changes: 22 additions & 0 deletions tests/acceptance/infinity-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,28 @@ module('Acceptance | infinity-scrollable', function (hooks) {
'{{in-viewport}} modifier',
'has title'
);

document.querySelector('.infinity-item-19').scrollIntoView(false);

await waitUntil(
() => {
return findAll('.infinity-item').length === 30;
},
{ timeoutMessage: 'did not find all items in time' }
);

await settled();

assert.equal(
findAll('.infinity-item').length,
30,
'after infinity has more items'
);
assert.equal(
find('h1').textContent.trim(),
'{{in-viewport}} modifier',
'has title'
);
});

test('works with in-viewport modifier (rAF)', async function (assert) {
Expand Down
32 changes: 22 additions & 10 deletions tests/dummy/app/components/my-modifier.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import Component from '@glimmer/component';
import { action, set } from '@ember/object';
import InViewportMixin from 'ember-in-viewport';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

export default class MyModifier extends Component {
@service inViewport;

export default class MyModifier extends Component.extend(InViewportMixin) {
@action
setupInViewport(element) {
this.watchElement(element);
const viewportSpy = true;
const viewportTolerance = {
bottom: 300,
};
const { onEnter } = this.inViewport.watchElement(element, {
viewportSpy,
viewportTolerance,
});
onEnter(this.didEnterViewport.bind(this));
}

constructor() {
super(...arguments);

set(this, 'viewportSpy', true);
set(this, 'viewportTolerance', {
bottom: 300,
});
}

didEnterViewport() {
this.infinityLoad();
this.args.infinityLoad();
}

willDestroy() {
super.willDestroy(...arguments);

const loader = document.getElementById('loader');
this.inViewport.stopWatching(loader);
}
}
10 changes: 10 additions & 0 deletions tests/dummy/app/controllers/infinity-built-in-modifiers.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ export default class InfinityBuiltInModifiers extends Controller {
element.textContent = '{{in-viewport}} modifier';
}

@action
setTitleGreen() {
document.querySelector('h1#green-target').style = 'color: green';
}

@action
removeTitleGreen() {
document.querySelector('h1#green-target').style = '';
}

@action
didEnterViewport(/*artwork, i, element*/) {
const arr = Array.apply(null, Array(10));
Expand Down
49 changes: 20 additions & 29 deletions tests/dummy/app/controllers/infinity-modifier.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,37 @@
import Controller from '@ember/controller';
import { set, action } from '@ember/object';
import { later } from '@ember/runloop';
import { Promise } from 'rsvp';
import { action, set } from '@ember/object';

const images = ['jarjan', 'aio___', 'kushsolitary', 'kolage', 'idiot', 'gt'];
let rect =
'<rect x="10" y="10" width="30" height="30" stroke="black" fill="transparent" stroke-width="5"/>';
let circle =
'<circle cx="25" cy="75" r="20" stroke="red" fill="transparent" stroke-width="5"/>';
let line =
'<line x1="10" x2="50" y1="110" y2="150" stroke="orange" stroke-width="5"/>';

const images = [rect, circle, line];
const arr = Array.apply(null, Array(10));
const models = [
...arr.map(() => {
return {
bgColor: 'E8D26F',
url: `https://s3.amazonaws.com/uifaces/faces/twitter/${
images[(Math.random() * images.length) | 0]
}/128.jpg`,
};
}),
...arr.map(() => `${images[(Math.random() * images.length) | 0]}`),
];

export default class InfinityModifier extends Controller {
constructor() {
super(...arguments);
this.viewportToleranceOverride = {
bottom: 200,
};
}

models = models;

@action
infinityLoad() {
const arr = Array.apply(null, Array(10));
const newModels = [
...arr.map(() => {
return {
bgColor: '0790EB',
url: `https://s3.amazonaws.com/uifaces/faces/twitter/${
images[(Math.random() * images.length) | 0]
}/128.jpg`,
};
}),
...arr.map(() => `${images[(Math.random() * images.length) | 0]}`),
];

return new Promise((resolve) => {
later(() => {
const models = this.models;
models.push(...newModels);
set(this, 'models', Array.prototype.slice.call(models));
resolve();
}, 0);
});
const models = this.models;
models.push(...newModels);
set(this, 'models', Array.prototype.slice.call(models));
}
}
84 changes: 45 additions & 39 deletions tests/dummy/app/templates/infinity-built-in-modifiers.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,48 @@
<LinkTo @route="infinity-built-in-modifiers" @query={{hash direction="enter"}}>enter</LinkTo>
<LinkTo @route="infinity-built-in-modifiers" @query={{hash direction="exit"}}>exit</LinkTo>
</nav>
<h1 {{in-viewport onEnter=this.setTitle viewportTolerance=this.other}}></h1>
{{#if (eq this.direction "both")}}
<ul class="infinity-container">
{{#each this.models as |artwork i|}}
<li
class="infinity-item infinity-item-{{i}}"
>
<DummyArtwork @artwork={{artwork}} @artworkProfile="dummy" />
</li>
{{/each}}
<div
class="sentinel"
{{in-viewport
onEnter=this.didEnterViewport
onExit=this.didExitViewport
viewportTolerance=this.viewportTolerance
scrollableArea=".infinity-container"
}}
></div>
</ul>
{{else if (eq this.direction "enter")}}
<ul>
{{#each this.models as |artwork|}}
<li>
<DummyArtwork @artwork={{artwork}} @artworkProfile="dummy" />
</li>
{{/each}}
<div class="sentinel" {{in-viewport onEnter=this.didEnterViewport}}></div>
</ul>
{{else if (eq this.direction "exit")}}
<ul>
{{#each this.models as |artwork|}}
<li>
<DummyArtwork @artwork={{artwork}} @artworkProfile="dummy" />
</li>
{{/each}}
<div class="sentinel" {{in-viewport onExit=this.didExitViewport}}></div>
</ul>
{{/if}}
<div style="height: 2500px;">
<h1 id="green-target" {{in-viewport onEnter=this.setTitle viewportTolerance=this.other}}></h1>

{{#if (eq this.direction "both")}}
<ul class="infinity-container">
{{#each this.models as |artwork i|}}
<li
class="infinity-item infinity-item-{{i}}"
>
<DummyArtwork @artwork={{artwork}} @artworkProfile="dummy" />
</li>
{{/each}}
<div
class="sentinel"
{{in-viewport
onEnter=this.didEnterViewport
onExit=this.didExitViewport
viewportTolerance=this.viewportTolerance
scrollableArea=".infinity-container"
viewportSpy=true
}}
></div>
</ul>
{{else if (eq this.direction "enter")}}
<ul>
{{#each this.models as |artwork|}}
<li>
<DummyArtwork @artwork={{artwork}} @artworkProfile="dummy" />
</li>
{{/each}}
<div class="sentinel" {{in-viewport onEnter=this.didEnterViewport}}></div>
</ul>
{{else if (eq this.direction "exit")}}
<ul>
{{#each this.models as |artwork|}}
<li>
<DummyArtwork @artwork={{artwork}} @artworkProfile="dummy" />
</li>
{{/each}}
<div class="sentinel" {{in-viewport onExit=this.didExitViewport}}></div>
</ul>
{{/if}}

<div {{in-viewport onEnter=this.setTitleGreen onExit=this.removeTitleGreen viewportSpy=true}}></div>
</div>
11 changes: 6 additions & 5 deletions tests/dummy/app/templates/infinity-modifier.hbs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<ul>
{{#each this.models as |artwork|}}
<li>
<DummyArtwork @artwork={{artwork}} @artworkProfile="dummy" />
</li>

{{#each this.models as |val|}}
<div class="infinity-class-item">
<svg width="200" height="250" version="1.1" xmlns="http://www.w3.org/2000/svg">
{{{val}}}
</svg>
</div>
{{/each}}
</ul>

Expand Down
9 changes: 5 additions & 4 deletions tests/integration/components/my-component-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ module('Integration | Component | my component', function (hooks) {
top: 1,
};
await render(hbs`
{{#my-component viewportEnabled=true viewportToleranceOverride=viewportToleranceOverride}}
<MyComponent @viewportEnabled={{true}} @viewportToleranceOverride={{this.viewportToleranceOverride}}>
template block text
{{/my-component}}
</MyComponent>
`);

assert.equal(this.element.textContent.trim(), 'template block text');
Expand All @@ -28,10 +28,11 @@ module('Integration | Component | my component', function (hooks) {
bottom: 0,
};
this.intersectionThreshold = 1.0;

await render(hbs`
{{#my-component viewportEnabled=true viewportTolerance=viewportTolerance intersectionThreshold=intersectionThreshold}}
<MyComponent @viewportEnabled={{true}} @viewportToleranceOverride={{this.viewportToleranceOverride}} @intersectionThreshold={{this.intersectionThreshold}}>
template block text
{{/my-component}}
</MyComponent>
`);

assert.equal(this.element.textContent.trim(), 'template block text');
Expand Down

0 comments on commit f630496

Please sign in to comment.