diff --git a/README.md b/README.md index 09717dcd..7381796a 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,27 @@ Note if you want to disable right and left in-viewport triggers, set these value ### Modifiers -Using with [Modifiers](https://blog.emberjs.com/2019/03/06/coming-soon-in-ember-octane-part-4.html) is easy. Note, modifiers currently only works to watch entering the viewport. +Using with [Modifiers](https://blog.emberjs.com/2019/03/06/coming-soon-in-ember-octane-part-4.html) 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)](#advanced-usage-options) + +```hbs + +``` + +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](https://github.com/emberjs/ember-render-modifiers) 2. Use the `did-insert` hook inside a component @@ -476,4 +496,4 @@ We're grateful to these wonderful contributors who've contributed to `ember-in-v -[//]: contributor-faces \ No newline at end of file +[//]: contributor-faces diff --git a/addon/services/in-viewport.js b/addon/services/in-viewport.js index 177e0491..2f720a04 100644 --- a/addon/services/in-viewport.js +++ b/addon/services/in-viewport.js @@ -56,48 +56,48 @@ export default class InViewport extends Service { * @void */ watchElement(element, configOptions = {}, enterCallback, exitCallback) { - if (get(this, 'viewportUseIntersectionObserver')) { - if (!get(this, 'observerAdmin')) { - this.startIntersectionObserver(); - } - const observerOptions = this.buildObserverOptions(configOptions); - - scheduleOnce('afterRender', this, () => { - // create IntersectionObserver instance or add to existing - this.setupIntersectionObserver( - element, - observerOptions, - enterCallback, - exitCallback - ); - }); - } else { - if (!get(this, 'rafAdmin')) { - this.startRAF(); - } - scheduleOnce('afterRender', this, () => { - // grab the user added callbacks when we enter/leave the element - const { - enterCallback = noop, - exitCallback = noop - } = this.getCallbacks(element) || {}; - // this isn't using the same functions as the mixin case, but that is b/c it is a bit harder to unwind. - // So just rewrote it with pure functions for now - startRAF( - element, - configOptions, - enterCallback, - exitCallback, - this.addRAF.bind(this, element.id), - this.removeRAF.bind(this, element.id) - ); - }); + if (get(this, 'viewportUseIntersectionObserver')) { + if (!get(this, 'observerAdmin')) { + this.startIntersectionObserver(); + } + const observerOptions = this.buildObserverOptions(configOptions); + + scheduleOnce('afterRender', this, () => { + // create IntersectionObserver instance or add to existing + this.setupIntersectionObserver( + element, + observerOptions, + enterCallback, + exitCallback + ); + }); + } else { + if (!get(this, 'rafAdmin')) { + this.startRAF(); } + scheduleOnce('afterRender', this, () => { + // grab the user added callbacks when we enter/leave the element + const { + enterCallback = noop, + exitCallback = noop + } = this.getCallbacks(element) || {}; + // this isn't using the same functions as the mixin case, but that is b/c it is a bit harder to unwind. + // So just rewrote it with pure functions for now + startRAF( + element, + configOptions, + enterCallback, + exitCallback, + this.addRAF.bind(this, element.id), + this.removeRAF.bind(this, element.id) + ); + }); + } - return { - onEnter: this.addEnterCallback.bind(this, element), - onExit: this.addExitCallback.bind(this, element) - }; + return { + onEnter: this.addEnterCallback.bind(this, element), + onExit: this.addExitCallback.bind(this, element) + }; } /** diff --git a/tests/acceptance/infinity-test.js b/tests/acceptance/infinity-test.js index d4a5f19c..9b6a7e1d 100644 --- a/tests/acceptance/infinity-test.js +++ b/tests/acceptance/infinity-test.js @@ -1,6 +1,6 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; -import { find, findAll, visit, waitFor, waitUntil } from '@ember/test-helpers'; +import { find, findAll, visit, settled, waitFor, waitUntil } from '@ember/test-helpers'; module('Acceptance | infinity-scrollable', function(hooks) { setupApplicationTest(hooks); @@ -25,6 +25,21 @@ module('Acceptance | infinity-scrollable', function(hooks) { assert.equal(findAll('.infinity-svg').length, 20); }); + test('works with in-viewport modifier', async function(assert) { + await visit('/infinity-built-in-modifiers'); + + assert.equal(findAll('.infinity-item').length, 10, 'has items to start'); + document.querySelector('.infinity-item-9').scrollIntoView(false); + + await waitUntil(() => { + return findAll('.infinity-item').length === 20; + }, { timeoutMessage: 'did not find all items in time' }); + + await settled(); + + assert.equal(findAll('.infinity-item').length, 20, 'after infinity has more items'); + }); + test('ember-in-viewport works with classes', async function(assert) { await visit('/infinity-class'); diff --git a/tests/acceptance/integration-test.js b/tests/acceptance/integration-test.js index 7a9b5516..e49fe84d 100644 --- a/tests/acceptance/integration-test.js +++ b/tests/acceptance/integration-test.js @@ -1,6 +1,6 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; -import { find, visit, waitFor } from '@ember/test-helpers'; +import { find, settled, visit, waitFor } from '@ember/test-helpers'; module('Acceptance | Intersection Observer', function(hooks) { setupApplicationTest(hooks); @@ -15,6 +15,7 @@ module('Acceptance | Intersection Observer', function(hooks) { await visit('/'); + await settled(); assert.ok(find('.my-component.top.start-enabled.active'), 'component is active'); }); diff --git a/tests/dummy/app/controllers/infinity-built-in-modifiers.js b/tests/dummy/app/controllers/infinity-built-in-modifiers.js index a4f67e23..1de63d9f 100644 --- a/tests/dummy/app/controllers/infinity-built-in-modifiers.js +++ b/tests/dummy/app/controllers/infinity-built-in-modifiers.js @@ -1,4 +1,6 @@ import Controller from '@ember/controller'; +import { later } from '@ember/runloop'; +import { action, set, get } from '@ember/object'; const images = ['jarjan', 'aio___', 'kushsolitary', 'kolage', 'idiot', 'gt']; @@ -14,18 +16,41 @@ const models = [ }) ]; -export default Controller.extend({ - queryParams: ['direction'], - direction: 'both', +export default class BuiltIn extends Controller { + queryParams = ['direction']; + direction = 'both'; - models, + init() { + super.init(...arguments); - actions: { - didEnterViewport(/*artwork, i, element*/) { - // console.log('enter', { artwork, i, element }); - }, - didExitViewport(/*artwork, i, element*/) { - // console.log('exit', { artwork, i, element }); - } + this.models = models; + set(this, 'viewportTolerance', { + bottom: 300 + }); } -}); + + @action + didEnterViewport(/*artwork, i, element*/) { + 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` + } + })]; + + return new Promise((resolve) => { + later(() => { + const models = get(this, 'models'); + models.push(...newModels); + set(this, 'models', Array.prototype.slice.call(models)); + resolve(); + }, 0); + }); + } + + @action + didExitViewport(/*artwork, i, element*/) { + // console.log('exit', { artwork, i, element }); + } +} diff --git a/tests/dummy/app/controllers/infinity-modifier.js b/tests/dummy/app/controllers/infinity-modifier.js index 8a1e11d6..9b2aaf98 100644 --- a/tests/dummy/app/controllers/infinity-modifier.js +++ b/tests/dummy/app/controllers/infinity-modifier.js @@ -7,7 +7,10 @@ const images = ["jarjan", "aio___", "kushsolitary", "kolage", "idiot", "gt"]; 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` } + return { + bgColor: 'E8D26F', + url: `https://s3.amazonaws.com/uifaces/faces/twitter/${images[(Math.random() * images.length) | 0]}/128.jpg` + } })]; export default Controller.extend({ @@ -17,7 +20,10 @@ export default Controller.extend({ 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` } + return { + bgColor: '0790EB', + url: `https://s3.amazonaws.com/uifaces/faces/twitter/${images[(Math.random() * images.length) | 0]}/128.jpg` + } })]; return new Promise((resolve) => { diff --git a/tests/dummy/app/templates/components/dummy-artwork.hbs b/tests/dummy/app/templates/components/dummy-artwork.hbs index 7fcca077..11b9f28b 100644 --- a/tests/dummy/app/templates/components/dummy-artwork.hbs +++ b/tests/dummy/app/templates/components/dummy-artwork.hbs @@ -1,46 +1,46 @@ {{#if this.actualArtwork}} - {{#if this.isUserMonogram}} - - {{this.userInitials}} - - {{else if this.isFallbackArtwork}} - - {{alt}} - - {{else if this.lazyLoad}} - {{!-- if we are lazyloading an image, we want to still show the image src if it turns out the client has no scripting --}} - {{!-- `sizes` must come before `src` and `data-srcset` to avoid double downloads --}} - {{alt}} - - {{else}} - - {{!-- responsive images --}} - {{!-- `sizes` must come before `src` and `srcset` to avoid double downloads --}} - {{alt}} - - {{/if}} + {{#if this.isUserMonogram}} + + {{this.userInitials}} + + {{else if this.isFallbackArtwork}} + + {{alt}} + + {{else if this.lazyLoad}} + {{!-- if we are lazyloading an image, we want to still show the image src if it turns out the client has no scripting --}} + {{!-- `sizes` must come before `src` and `data-srcset` to avoid double downloads --}} + {{alt}} + + {{else}} + + {{!-- responsive images --}} + {{!-- `sizes` must come before `src` and `srcset` to avoid double downloads --}} + {{alt}} + + {{/if}} {{/if}} diff --git a/tests/dummy/app/templates/infinity-built-in-modifiers.hbs b/tests/dummy/app/templates/infinity-built-in-modifiers.hbs index 303d8cf4..887db7be 100644 --- a/tests/dummy/app/templates/infinity-built-in-modifiers.hbs +++ b/tests/dummy/app/templates/infinity-built-in-modifiers.hbs @@ -5,32 +5,35 @@ {{link-to "exit" (query-params direction="exit")}} {{#if (eq this.direction "both")}} -