From ab9be08bb85aef6812d46c2e26933fd12ae2510b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Buscht=C3=B6ns?= Date: Tue, 14 Nov 2017 07:45:28 +0100 Subject: [PATCH 1/2] feat(wait-for/event): support native DOM EventTargets Closes #180. Also removes `.on()` from the assertion message, as it was not supported. --- addon/-wait-for.js | 30 +++++++++++++---- addon/utils.js | 8 ++--- tests/unit/wait-for-test.js | 66 +++++++++++++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 12 deletions(-) diff --git a/addon/-wait-for.js b/addon/-wait-for.js index 7d2342408..fbb74687e 100644 --- a/addon/-wait-for.js +++ b/addon/-wait-for.js @@ -27,13 +27,32 @@ class WaitForEventYieldable { } [yieldableSymbol](taskInstance, resumeIndex) { + let unbind = () => {}; let fn = (event) => { + unbind(); taskInstance.proceed(resumeIndex, YIELDABLE_CONTINUE, event); }; - this.object.one(this.eventName, fn); - return () => { - this.object.off(this.eventName, fn); - }; + + if (typeof this.object.addEventListener === 'function') { + // assume that we're dealing with a DOM `EventTarget`. + this.object.addEventListener(this.eventName, fn); + + // unfortunately this is required, because IE 11 does not support the + // `once` option: https://caniuse.com/#feat=once-event-listener + unbind = () => { + this.object.removeEventListener(this.eventName, fn); + }; + + return unbind; + } else { + // assume that we're dealing with either `Ember.Evented` or a compatible + // interface, like jQuery. + this.object.one(this.eventName, fn); + + return () => { + this.object.off(this.eventName, fn); + }; + } } } @@ -82,7 +101,6 @@ export function waitForQueue(queueName) { * @param {function} eventName the name of the event to wait for */ export function waitForEvent(object, eventName) { - assert(`${object} must include Ember.Evented (or support \`.on()\`, \`.one()\`, and \`.off()\`) to be able to use \`waitForEvent\``, isEventedObject(object)); + assert(`${object} must include Ember.Evented (or support \`.one()\` and \`.off()\`) or DOM EventTarget (or support \`addEventListener\` and \`removeEventListener\`) to be able to use \`waitForEvent\``, isEventedObject(object)); return new WaitForEventYieldable(object, eventName); } - diff --git a/addon/utils.js b/addon/utils.js index 1ac60cecc..23d390278 100644 --- a/addon/utils.js +++ b/addon/utils.js @@ -4,9 +4,10 @@ import ComputedProperty from '@ember/object/computed'; import Ember from 'ember'; export function isEventedObject(c) { - return (c && - typeof c.one === 'function' && - typeof c.off === 'function'); + return (c && ( + (typeof c.one === 'function' && typeof c.off === 'function') || + (typeof c.addEventListener === 'function' && typeof c.removeEventListener === 'function') + )); } export function Arguments(args, defer) { @@ -145,4 +146,3 @@ export function rawTimeout(ms) { } }; } - diff --git a/tests/unit/wait-for-test.js b/tests/unit/wait-for-test.js index 2d64111df..ae0911d48 100644 --- a/tests/unit/wait-for-test.js +++ b/tests/unit/wait-for-test.js @@ -49,7 +49,7 @@ test('cancelling waitForQueue works', function(assert) { assert.notOk(taskCompleted, 'Task should not have completed'); }); -test('waitForEvent works', function(assert) { +test('waitForEvent works (`Ember.Evented` interface)', function(assert) { assert.expect(4); let taskCompleted = false; @@ -77,7 +77,7 @@ test('waitForEvent works', function(assert) { assert.ok(taskCompleted, 'Task should have completed'); }); -test('canceling waitForEvent works', function(assert) { +test('canceling waitForEvent works (`Ember.Evented` interface)', function(assert) { assert.expect(4); let taskCompleted = false; @@ -105,3 +105,65 @@ test('canceling waitForEvent works', function(assert) { assert.notOk(obj.has('foo'), 'Object does not have the event listener'); assert.notOk(taskCompleted, 'Task should not have completed'); }); + +test('waitForEvent works (DOM `EventTarget` interface)', function(assert) { + assert.expect(3); + + const element = document.createElement('button'); + let taskCompleted = false; + let obj; + + const Obj = EventedObject.extend({ + task: task(function*() { + let { detail } = yield waitForEvent(element, 'foo'); + assert.equal(detail, 123); + taskCompleted = true; + }) + }); + + run(() => { + obj = Obj.create(); + obj.get('task').perform(); + }); + + run(() => { + assert.notOk(taskCompleted, 'Task should not have completed'); + element.dispatchEvent(new CustomEvent('foo', { detail: 123 })); + }); + + assert.ok(taskCompleted, 'Task should have completed'); +}); + +test('canceling waitForEvent works (DOM `EventTarget` interface)', function(assert) { + assert.expect(3); + + const element = document.createElement('button'); + element.removeEventListener = (...args) => { + removeEventListenerCalled = true; + return HTMLElement.prototype.removeEventListener.apply(element, args); + }; + let taskCompleted = false; + let removeEventListenerCalled = false; + let obj; + + const Obj = EventedObject.extend({ + task: task(function*() { + yield waitForEvent(element, 'foo'); + taskCompleted = true; + }) + }); + + run(() => { + obj = Obj.create(); + obj.get('task').perform(); + }); + + run(() => { + assert.notOk(taskCompleted, 'Task should not have completed'); + obj.get('task').cancelAll(); + element.dispatchEvent(new CustomEvent('foo')); + }); + + assert.ok(removeEventListenerCalled, '`removeEventListener` was called'); + assert.notOk(taskCompleted, 'Task should not have completed'); +}); From b18f73c13d8ffe69b593762f3bf0ec261dd3ae5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Buscht=C3=B6ns?= Date: Wed, 17 Jan 2018 19:40:09 +0100 Subject: [PATCH 2/2] docs(events): add information about native DOM event support --- tests/dummy/app/docs/controller.js | 2 +- tests/dummy/app/docs/events/controller.js | 13 ++++++++++--- tests/dummy/app/docs/events/template.hbs | 16 +++++++++++----- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/tests/dummy/app/docs/controller.js b/tests/dummy/app/docs/controller.js index c3e62256b..08db9ede6 100644 --- a/tests/dummy/app/docs/controller.js +++ b/tests/dummy/app/docs/controller.js @@ -26,7 +26,7 @@ export default Controller.extend({ { route: "docs.task-groups", title: "Task Groups" }, { route: "docs.derived-state", title: "Derived State" }, { route: "docs.encapsulated-task", title: "Encapsulated Tasks" }, - { route: "docs.events", title: "Ember / jQuery Events" }, + { route: "docs.events", title: "Ember & DOM / jQuery Events" }, { route: "docs.testing-debugging", title: "Testing & Debugging" }, { route: "docs.faq", title: "FAQ & Fact Sheet" }, diff --git a/tests/dummy/app/docs/events/controller.js b/tests/dummy/app/docs/events/controller.js index 2fd914ff7..de6dee6d5 100644 --- a/tests/dummy/app/docs/events/controller.js +++ b/tests/dummy/app/docs/events/controller.js @@ -5,13 +5,21 @@ import { task, waitForEvent, timeout } from 'ember-concurrency'; export default Controller.extend(Evented, { // BEGIN-SNIPPET waitForEvent + domEvent: null, + domEventLoop: task(function * () { + while(true) { + let event = yield waitForEvent(document.body, 'click'); + this.set('domEvent', event); + this.trigger('fooEvent', { v: Math.random() }); + } + }).on('init'), + jQueryEvent: null, jQueryEventLoop: task(function * () { let $body = $('body'); while(true) { let event = yield waitForEvent($body, 'click'); this.set('jQueryEvent', event); - this.trigger('fooEvent', { v: Math.random() }); } }).on('init'), @@ -34,9 +42,8 @@ export default Controller.extend(Evented, { }).on('init'), waiter: task(function * () { - let event = yield waitForEvent($('body'), 'click'); + let event = yield waitForEvent(document.body, 'click'); return event; }), // END-SNIPPET }); - diff --git a/tests/dummy/app/docs/events/template.hbs b/tests/dummy/app/docs/events/template.hbs index cbe3e2d78..96bf91801 100644 --- a/tests/dummy/app/docs/events/template.hbs +++ b/tests/dummy/app/docs/events/template.hbs @@ -1,8 +1,11 @@ -

Waiting for Ember / jQuery Events

+

Waiting for Ember & DOM / jQuery Events

- You can use waitForEvent to pause your task until - an Ember.Evented or jQuery Event fires. + You can use waitForEvent(object, eventName) to pause your task until + an Ember.Evented or DOM / jQuery Event fires. + object must include Ember.Evented (or support .one() + and .off()) or be a valid DOM EventTarget (or support + .addEventListener() and .removeEventListener()).

@@ -15,11 +18,15 @@

Try clicking around the page; waitForEvent will install - handlers and wait for the specified Ember or jQuery event to fire; + handlers and wait for the specified Ember, DOM or jQuery event to fire; the value returned from yield is the event that was fired.

{{!BEGIN-SNIPPET waitForEvent}} +

+ domEvent: (x={{domEvent.offsetX}}, y={{domEvent.offsetX}}) +

+

jqueryEvent: (x={{jQueryEvent.offsetX}}, y={{jQueryEvent.offsetX}})

@@ -56,4 +63,3 @@ {{code-snippet name="waitForEvent-derived-state.js"}} {{code-snippet name="waitForEvent-derived-state.hbs"}} -