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

Just a small town boy living in a ember modifier world #179

Merged
merged 5 commits into from
Mar 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This `ember-cli` addon adds a simple, highly performant Ember Mixin to your app.

## Demo or examples
- Dummy app (`ember serve`): https://github.com/DockYard/ember-in-viewport/tree/master/tests/dummy
- Use with Ember [Modifiers](#modifiers) and [@ember/render-modifiers](https://github.com/emberjs/ember-render-modifiers)
- [ember-infinity](https://github.com/ember-infinity/ember-infinity)
- [ember-light-table](https://github.com/offirgolan/ember-light-table)
- Tracking advertisement impressions
Expand Down Expand Up @@ -220,6 +221,53 @@ module.exports = function(environment) {

Note if you want to disable right and left in-viewport triggers, set these values to `Infinity`.
```

### Modifiers

Using with [Modifiers](https://blog.emberjs.com/2019/03/06/coming-soon-in-ember-octane-part-4.html) is easy.

1. Install [@ember/render-modifiers](https://github.com/emberjs/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.

```js
import Component from '@ember/component';
import { set } from '@ember/object';
import InViewportMixin from 'ember-in-viewport';

export default Component.extend(InViewportMixin, {
tagName: '',

// if you do have a tagName ^^, then you can use `didInsertElement` or no-op it. You choose
// didInsertElement() {},
didInsertNode(element, [instance]) {
instance.watchElement(element);
},

init() {
this._super(...arguments);

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

didEnterViewport() {
// this will only work with one element being watched in the container. This is still a TODO to enable
this.infinityLoad();
}
});
```

```hbs
<div {{did-insert this.didInsertNode this}}>
Copy link
Contributor

Choose a reason for hiding this comment

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

I like that it uses the existing implementation in a clean way, but I wonder if there's a way to write the modifier such that it doesn't need the entire component instance? I had thought that the benefit of modifiers is that they can operate on DOM nodes independently of their associated component classes.

My initial thought at implementing (before i knew about @ember/render-modifiers, was to wire up an IntersectionObserver with the element directly.

I don't quite get how modifier managers work at the moment, so I wasn't able to understand what the did-insert modifier is actually doing under the hood

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point. Added a note about a future add did-enter-viewport modifier + a service (instead of a mixin). Will think about this over the following months. Thanks for taking a look!

{{yield}}
</div>
```

## [**IntersectionObserver**'s Browser Support](https://platform-status.mozilla.org/)

### Out of the box
Expand Down
49 changes: 26 additions & 23 deletions addon/mixins/in-viewport.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@ export default Mixin.create({

const viewportEnabled = get(this, 'viewportEnabled');
if (viewportEnabled) {
this._startListening();
this.watchElement(get(this, 'element'));
}
},

willDestroyElement() {
this._super(...arguments);

this._unbindListeners();
this._unbindListeners(get(this, 'element'));
},

_buildOptions(defaultOptions = {}) {
Expand All @@ -103,45 +103,44 @@ export default Mixin.create({
}
},

_startListening() {
this._setInitialViewport();
this._addObserverIfNotSpying();
watchElement(element) {
this._setInitialViewport(element);
this._addObserverIfNotSpying(element);
this._bindScrollDirectionListener(get(this, 'viewportScrollSensitivity'));

if (!get(this, 'viewportUseIntersectionObserver') && !get(this, 'viewportUseRAF')) {
get(this, 'viewportListeners').forEach((listener) => {
let { context, event } = listener;
context = get(this, 'scrollableArea') || context;
this._bindListeners(context, event);
this._bindListeners(context, event, element);
});
}
},

_addObserverIfNotSpying() {
_addObserverIfNotSpying(element) {
if (!get(this, 'viewportSpy')) {
this.addObserver('viewportEntered', this, this._unbindIfEntered);
this.addObserver('viewportEntered', this, bind(this, '_unbindIfEntered', element));
}
},

_setInitialViewport() {
_setInitialViewport(element) {
if (get(this, 'viewportUseIntersectionObserver')) {
return scheduleOnce('afterRender', this, () => {
this._setupIntersectionObserver();
this._setupIntersectionObserver(element);
});
} else {
return scheduleOnce('afterRender', this, () => {
this._setViewportEntered();
this._setViewportEntered(element);
});
}
},

/**
* @method _setupIntersectionObserver
*/
_setupIntersectionObserver() {
_setupIntersectionObserver(element) {
const scrollableArea = get(this, 'scrollableArea') ? document.querySelector(get(this, 'scrollableArea')) : undefined;

const element = get(this, 'element');
if (!element) {
return;
}
Expand Down Expand Up @@ -170,10 +169,9 @@ export default Mixin.create({
*
* @method _setViewportEntered
*/
_setViewportEntered() {
_setViewportEntered(element) {
const scrollableArea = get(this, 'scrollableArea') ? document.querySelector(get(this, 'scrollableArea')) : undefined;

const element = get(this, 'element');
if (!element) {
return;
}
Expand All @@ -200,7 +198,7 @@ export default Mixin.create({
let elementId = get(this, 'elementId');
rAFIDS[elementId] = get(this, '_rAFAdmin').add(
elementId,
bind(this, this._setViewportEntered)
bind(this, this._setViewportEntered, element)
);
}
}
Expand Down Expand Up @@ -270,6 +268,11 @@ export default Mixin.create({
* @param hasEnteredViewport
*/
_triggerDidAccessViewport(hasEnteredViewport = false) {
const isTearingDown = this.isDestroyed || this.isDestroying;
if (isTearingDown) {
return;
}

const viewportEntered = get(this, 'viewportEntered');
const didEnter = !viewportEntered && hasEnteredViewport;
const didLeave = viewportEntered && !hasEnteredViewport;
Expand All @@ -295,9 +298,9 @@ export default Mixin.create({
*
* @method _unbindIfEntered
*/
_unbindIfEntered() {
_unbindIfEntered(element) {
if (get(this, 'viewportEntered')) {
this._unbindListeners();
this._unbindListeners(element);
this.removeObserver('viewportEntered', this, this._unbindIfEntered);
set(this, 'viewportEntered', false);
}
Expand Down Expand Up @@ -342,13 +345,13 @@ export default Mixin.create({
*
* @method _bindListeners
*/
_bindListeners(context = null, event = null) {
_bindListeners(context = null, event = null, element = null) {
assert('You must pass a valid context to _bindListeners', context);
assert('You must pass a valid event to _bindListeners', event);

let elem = findElem(context);

let evtListener = (() => this._debouncedEvent('_setViewportEntered'));
let evtListener = (() => this._debouncedEvent('_setViewportEntered', element));
this._evtListenerClosures.push({ event: event, evtListener });
elem.addEventListener(event, evtListener);
},
Expand All @@ -360,13 +363,13 @@ export default Mixin.create({
*
* @method _unbindListeners
*/
_unbindListeners() {
_unbindListeners(element) {
set(this, '_stopListening', true);

// if IntersectionObserver
if (get(this, 'viewportUseIntersectionObserver') && get(this, 'viewportEnabled')) {
get(this, '_observerAdmin').unobserve(
this.element,
element,
get(this, '_observerOptions'),
get(this, 'scrollableArea')
);
Expand Down Expand Up @@ -395,7 +398,7 @@ export default Mixin.create({
});
}

// 4.
// 4. last but not least
this._unbindScrollDirectionListener();
},
});
44 changes: 38 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,27 @@
"dependencies": {
"ember-auto-import": "^1.2.15",
"ember-cli-babel": "^7.1.2",
"intersection-observer-admin": "0.0.5",
"raf-pool": "0.0.4"
"intersection-observer-admin": "0.1.0",
"raf-pool": "0.1.0"
},
"devDependencies": {
"@ember/optional-features": "^0.7.0",
"@ember/render-modifiers": "^1.0.0",
"broccoli-asset-rev": "^3.0.0",
"ember-cli": "~3.8.1",
"ember-cli-dependency-checker": "^3.1.0",
"ember-cli-eslint": "^4.2.3",
"ember-cli-htmlbars": "^3.0.0",
"ember-cli-htmlbars-inline-precompile": "^2.0.0",
"ember-cli-inject-live-reload": "^2.0.1",
"ember-qunit": "^3.4.1",
"ember-cli-shims": "^1.2.0",
"ember-cli-sri": "^2.1.0",
"ember-cli-uglify": "^2.1.0",
"ember-disable-prototype-extensions": "^1.1.3",
"ember-export-application-global": "^2.0.0",
"ember-load-initializers": "^2.0.0",
"ember-maybe-import-regenerator": "^0.1.6",
"ember-qunit": "^3.4.1",
"ember-resolver": "^5.0.1",
"ember-source": "~3.8.0",
"ember-source-channel-url": "^1.1.0",
Expand Down
26 changes: 26 additions & 0 deletions tests/dummy/app/components/my-modifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Component from '@ember/component';
import { set } from '@ember/object';
import InViewportMixin from 'ember-in-viewport';

export default Component.extend(InViewportMixin, {
tagName: '',

// if you do have a tagName ^^, then you can use `didInsertElement` or no-op it
// didInsertElement() {},
didInsertNode(element, [instance]) {
instance.watchElement(element);
},

init() {
this._super(...arguments);

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

didEnterViewport() {
this.infinityLoad();
}
});
28 changes: 28 additions & 0 deletions tests/dummy/app/controllers/infinity-modifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Controller from '@ember/controller';
import { set, get } from '@ember/object';
import { later } from '@ember/runloop';
import { Promise } from 'rsvp';

const images = ["jarjan", "aio___", "kushsolitary", "kolage", "idiot", "gt"];

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

export default Controller.extend({
models,

actions: {
infinityLoad() {
const arr = Array.apply(null, Array(10));
const newModels = [...arr.map(() => `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);
});
}
}
});
1 change: 1 addition & 0 deletions tests/dummy/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const Router = EmberRouter.extend({

Router.map(function() {
this.route('infinity');
this.route('infinity-modifier');
this.route('infinity-scrollable');
this.route('infinity-scrollable-raf');
this.route('infinity-scrollable-scrollevent');
Expand Down
Loading