From f63049606c377819037fcb8c77ae89a35c2edadb Mon Sep 17 00:00:00 2001 From: Scott Newcomer Date: Mon, 26 Apr 2021 23:16:57 -0500 Subject: [PATCH] Bug: stop watching unless viewportSpy=true is passed to modifier (#270) * 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 --- README.md | 5 +- addon/modifiers/in-viewport.js | 4 + tests/acceptance/infinity-test.js | 22 +++++ tests/dummy/app/components/my-modifier.js | 32 ++++--- .../infinity-built-in-modifiers.js | 10 +++ .../app/controllers/infinity-modifier.js | 49 +++++------ .../templates/infinity-built-in-modifiers.hbs | 84 ++++++++++--------- .../dummy/app/templates/infinity-modifier.hbs | 11 +-- .../components/my-component-test.js | 9 +- 9 files changed, 138 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 777c5181..b3ae84f6 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/addon/modifiers/in-viewport.js b/addon/modifiers/in-viewport.js index 34c407f7..21c5f36a 100644 --- a/addon/modifiers/in-viewport.js +++ b/addon/modifiers/in-viewport.js @@ -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 diff --git a/tests/acceptance/infinity-test.js b/tests/acceptance/infinity-test.js index 96ea1170..8752c102 100644 --- a/tests/acceptance/infinity-test.js +++ b/tests/acceptance/infinity-test.js @@ -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) { diff --git a/tests/dummy/app/components/my-modifier.js b/tests/dummy/app/components/my-modifier.js index a4070d56..f960a4db 100644 --- a/tests/dummy/app/components/my-modifier.js +++ b/tests/dummy/app/components/my-modifier.js @@ -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); } } diff --git a/tests/dummy/app/controllers/infinity-built-in-modifiers.js b/tests/dummy/app/controllers/infinity-built-in-modifiers.js index 215bf865..4afabd20 100644 --- a/tests/dummy/app/controllers/infinity-built-in-modifiers.js +++ b/tests/dummy/app/controllers/infinity-built-in-modifiers.js @@ -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)); diff --git a/tests/dummy/app/controllers/infinity-modifier.js b/tests/dummy/app/controllers/infinity-modifier.js index 45681c6f..32187956 100644 --- a/tests/dummy/app/controllers/infinity-modifier.js +++ b/tests/dummy/app/controllers/infinity-modifier.js @@ -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 = + ''; +let circle = + ''; +let line = + ''; +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)); } } diff --git a/tests/dummy/app/templates/infinity-built-in-modifiers.hbs b/tests/dummy/app/templates/infinity-built-in-modifiers.hbs index 6814a607..cc125250 100644 --- a/tests/dummy/app/templates/infinity-built-in-modifiers.hbs +++ b/tests/dummy/app/templates/infinity-built-in-modifiers.hbs @@ -4,42 +4,48 @@ enter exit -

-{{#if (eq this.direction "both")}} -
    - {{#each this.models as |artwork i|}} -
  • - -
  • - {{/each}} -
    -
-{{else if (eq this.direction "enter")}} -
    - {{#each this.models as |artwork|}} -
  • - -
  • - {{/each}} -
    -
-{{else if (eq this.direction "exit")}} -
    - {{#each this.models as |artwork|}} -
  • - -
  • - {{/each}} -
    -
-{{/if}} +
+

+ + {{#if (eq this.direction "both")}} +
    + {{#each this.models as |artwork i|}} +
  • + +
  • + {{/each}} +
    +
+ {{else if (eq this.direction "enter")}} +
    + {{#each this.models as |artwork|}} +
  • + +
  • + {{/each}} +
    +
+ {{else if (eq this.direction "exit")}} +
    + {{#each this.models as |artwork|}} +
  • + +
  • + {{/each}} +
    +
+ {{/if}} + +
+
diff --git a/tests/dummy/app/templates/infinity-modifier.hbs b/tests/dummy/app/templates/infinity-modifier.hbs index 3ebb4a50..a3190548 100644 --- a/tests/dummy/app/templates/infinity-modifier.hbs +++ b/tests/dummy/app/templates/infinity-modifier.hbs @@ -1,9 +1,10 @@
    - {{#each this.models as |artwork|}} -
  • - -
  • - + {{#each this.models as |val|}} +
    + + {{{val}}} + +
    {{/each}}
diff --git a/tests/integration/components/my-component-test.js b/tests/integration/components/my-component-test.js index 04e341a5..d8f8019c 100644 --- a/tests/integration/components/my-component-test.js +++ b/tests/integration/components/my-component-test.js @@ -12,9 +12,9 @@ module('Integration | Component | my component', function (hooks) { top: 1, }; await render(hbs` - {{#my-component viewportEnabled=true viewportToleranceOverride=viewportToleranceOverride}} + template block text - {{/my-component}} + `); assert.equal(this.element.textContent.trim(), 'template block text'); @@ -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}} + template block text - {{/my-component}} + `); assert.equal(this.element.textContent.trim(), 'template block text');