Skip to content

Commit 2878c1d

Browse files
brandonocaseygkatsev
authored andcommitted
feat(events): add any function (#5977)
This new events function allows you to listen to a list of events and know that only one handler will ever be called for the group. With just one event, it'll function similarly to `.one`. Examples: Single event ``` const player = videojs('some-player-id'); player.any('a', (e) => console.log(e.type + ' triggered'); player.trigger('a'); // logs 'a triggered' player.trigger('a'); // logs nothing as the listener has been removed. ``` Multiple Events ``` const player = videojs('some-player-id'); player.any(['a', 'b', 'c', 'd'], (e) => console.log(e.type + ' triggered'); player.trigger('d'); // logs 'd triggered' player.trigger('a'); player.trigger('b'); player.trigger('c'); player.trigger('d'); // all triggers above log nothing as the listener is removed after the first 'd' trigger. ```
1 parent c2bea31 commit 2878c1d

File tree

5 files changed

+227
-6
lines changed

5 files changed

+227
-6
lines changed

src/js/event-target.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ EventTarget.prototype.removeEventListener = EventTarget.prototype.off;
110110
* The function to be called once for each event name.
111111
*/
112112
EventTarget.prototype.one = function(type, fn) {
113-
// Remove the addEventListener alialing Events.on
113+
// Remove the addEventListener aliasing Events.on
114114
// so we don't get into an infinite type loop
115115
const ael = this.addEventListener;
116116

@@ -119,6 +119,16 @@ EventTarget.prototype.one = function(type, fn) {
119119
this.addEventListener = ael;
120120
};
121121

122+
EventTarget.prototype.any = function(type, fn) {
123+
// Remove the addEventListener aliasing Events.on
124+
// so we don't get into an infinite type loop
125+
const ael = this.addEventListener;
126+
127+
this.addEventListener = () => {};
128+
Events.any(this, type, fn);
129+
this.addEventListener = ael;
130+
};
131+
122132
/**
123133
* This function causes an event to happen. This will then cause any `event listeners`
124134
* that are waiting for that event, to get called. If there are no `event listeners`

src/js/mixins/evented.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ const EventedMixin = {
242242

243243
/**
244244
* Add a listener to an event (or events) on this object or another evented
245-
* object. The listener will only be called once and then removed.
245+
* object. The listener will be called once per event and then removed.
246246
*
247247
* @param {string|Array|Element|Object} targetOrType
248248
* If this is a string or array, it represents the event type(s)
@@ -272,6 +272,10 @@ const EventedMixin = {
272272

273273
// Targeting another evented object.
274274
} else {
275+
// TODO: This wrapper is incorrect! It should only
276+
// remove the wrapper for the event type that called it.
277+
// Instead all listners are removed on the first trigger!
278+
// see https://github.com/videojs/video.js/issues/5962
275279
const wrapper = (...largs) => {
276280
this.off(target, type, wrapper);
277281
listener.apply(null, largs);
@@ -284,6 +288,51 @@ const EventedMixin = {
284288
}
285289
},
286290

291+
/**
292+
* Add a listener to an event (or events) on this object or another evented
293+
* object. The listener will only be called once for the first event that is triggered
294+
* then removed.
295+
*
296+
* @param {string|Array|Element|Object} targetOrType
297+
* If this is a string or array, it represents the event type(s)
298+
* that will trigger the listener.
299+
*
300+
* Another evented object can be passed here instead, which will
301+
* cause the listener to listen for events on _that_ object.
302+
*
303+
* In either case, the listener's `this` value will be bound to
304+
* this object.
305+
*
306+
* @param {string|Array|Function} typeOrListener
307+
* If the first argument was a string or array, this should be the
308+
* listener function. Otherwise, this is a string or array of event
309+
* type(s).
310+
*
311+
* @param {Function} [listener]
312+
* If the first argument was another evented object, this will be
313+
* the listener function.
314+
*/
315+
any(...args) {
316+
const {isTargetingSelf, target, type, listener} = normalizeListenArgs(this, args);
317+
318+
// Targeting this evented object.
319+
if (isTargetingSelf) {
320+
listen(target, 'any', type, listener);
321+
322+
// Targeting another evented object.
323+
} else {
324+
const wrapper = (...largs) => {
325+
this.off(target, type, wrapper);
326+
listener.apply(null, largs);
327+
};
328+
329+
// Use the same function ID as the listener so we can remove it later
330+
// it using the ID of the original listener.
331+
wrapper.guid = listener.guid;
332+
listen(target, 'any', type, wrapper);
333+
}
334+
},
335+
287336
/**
288337
* Removes listener(s) from event(s) on an evented object.
289338
*

src/js/utils/events.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,3 +476,29 @@ export function one(elem, type, fn) {
476476
func.guid = fn.guid = fn.guid || Guid.newGUID();
477477
on(elem, type, func);
478478
}
479+
480+
/**
481+
* Trigger a listener only once and then turn if off for all
482+
* configured events
483+
*
484+
* @param {Element|Object} elem
485+
* Element or object to bind to.
486+
*
487+
* @param {string|string[]} type
488+
* Name/type of event
489+
*
490+
* @param {Event~EventListener} fn
491+
* Event listener function
492+
*/
493+
export function any(elem, type, fn) {
494+
const func = function() {
495+
off(elem, type, func);
496+
fn.apply(this, arguments);
497+
};
498+
499+
// copy the guid to the new function so it can removed using the original function's ID
500+
func.guid = fn.guid = fn.guid || Guid.newGUID();
501+
502+
// multiple ons, but one off for everything
503+
on(elem, type, func);
504+
}

test/unit/events.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,39 @@ QUnit.test('retrigger with an object should use the old element as target', func
347347
Events.off(el1, 'click');
348348
Events.off(el2, 'click');
349349
});
350+
351+
QUnit.test('should listen only once for any', function(assert) {
352+
const el = document.createElement('div');
353+
let triggered = 0;
354+
const listener = () => triggered++;
355+
356+
Events.any(el, 'click', listener);
357+
assert.equal(triggered, 0, 'listener was not yet triggered');
358+
// 1 click
359+
Events.trigger(el, 'click');
360+
361+
assert.equal(triggered, 1, 'listener was triggered');
362+
// No click should happen.
363+
Events.trigger(el, 'click');
364+
assert.equal(triggered, 1, 'listener was not triggered again');
365+
});
366+
367+
QUnit.test('only the first event should call listener via any', function(assert) {
368+
const el = document.createElement('div');
369+
let triggered = 0;
370+
const listener = () => triggered++;
371+
372+
Events.any(el, ['click', 'event1', 'event2'], listener);
373+
assert.equal(triggered, 0, 'listener was not yet triggered');
374+
375+
// 1 click
376+
Events.trigger(el, 'click');
377+
assert.equal(triggered, 1, 'listener was triggered');
378+
// nothing below here should trigger the Callback
379+
Events.trigger(el, 'click');
380+
Events.trigger(el, 'event1');
381+
Events.trigger(el, 'event1');
382+
Events.trigger(el, 'event2');
383+
Events.trigger(el, 'event2');
384+
assert.equal(triggered, 1, 'listener was not triggered again');
385+
});

test/unit/mixins/evented.test.js

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ QUnit.test('evented() with custom element', function(assert) {
6161
);
6262
});
6363

64-
QUnit.test('on() and one() errors', function(assert) {
64+
QUnit.test('on(), one(), and any() errors', function(assert) {
6565
const targeta = this.targets.a = evented({});
6666
const targetb = this.targets.b = evented({});
6767

68-
['on', 'one'].forEach(method => {
68+
['on', 'one', 'any'].forEach(method => {
6969
assert.throws(() => targeta[method](), errors.type, 'the expected error is thrown');
7070
assert.throws(() => targeta[method](' '), errors.type, 'the expected error is thrown');
7171
assert.throws(() => targeta[method]([]), errors.type, 'the expected error is thrown');
@@ -165,6 +165,63 @@ QUnit.test('one() can add a listener to an array of event types on this object',
165165
});
166166
});
167167

168+
QUnit.test('one() can add a listener to an array of event types on this object', function(assert) {
169+
const a = this.targets.a = evented({});
170+
const spy = sinon.spy();
171+
172+
a.one(['x', 'y'], spy);
173+
a.trigger('x');
174+
a.trigger('y');
175+
a.trigger('x');
176+
a.trigger('y');
177+
178+
assert.strictEqual(spy.callCount, 2, 'the listener was called the expected number of times');
179+
180+
validateListenerCall(spy.getCall(0), a, {
181+
type: 'x',
182+
target: a.eventBusEl_
183+
});
184+
185+
validateListenerCall(spy.getCall(1), a, {
186+
type: 'y',
187+
target: a.eventBusEl_
188+
});
189+
});
190+
191+
QUnit.test('any() can add a listener to one event type on this object', function(assert) {
192+
const a = this.targets.a = evented({});
193+
const spy = sinon.spy();
194+
195+
a.any('x', spy);
196+
a.trigger('x');
197+
a.trigger('x');
198+
199+
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
200+
201+
validateListenerCall(spy.getCall(0), a, {
202+
type: 'x',
203+
target: a.eventBusEl_
204+
});
205+
});
206+
207+
QUnit.test('any() can add a listener to an array of event types on this object', function(assert) {
208+
const a = this.targets.a = evented({});
209+
const spy = sinon.spy();
210+
211+
a.any(['x', 'y'], spy);
212+
a.trigger('x');
213+
a.trigger('y');
214+
a.trigger('x');
215+
a.trigger('y');
216+
217+
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
218+
219+
validateListenerCall(spy.getCall(0), a, {
220+
type: 'x',
221+
target: a.eventBusEl_
222+
});
223+
});
224+
168225
QUnit.test('on() can add a listener to one event type on a different target object', function(assert) {
169226
const a = this.targets.a = evented({});
170227
const b = this.targets.b = evented({});
@@ -229,8 +286,9 @@ QUnit.test('one() can add a listener to one event type on a different target obj
229286
});
230287
});
231288

232-
// The behavior here unfortunately differs from the identical case where "a"
233-
// listens to itself. This is something that should be resolved...
289+
// TODO: This test is incorrect! this listener should be called twice,
290+
// but instead all listners are removed on the first trigger!
291+
// see https://github.com/videojs/video.js/issues/5962
234292
QUnit.test('one() can add a listener to an array of event types on a different target object', function(assert) {
235293
const a = this.targets.a = evented({});
236294
const b = this.targets.b = evented({});
@@ -254,6 +312,48 @@ QUnit.test('one() can add a listener to an array of event types on a different t
254312
});
255313
});
256314

315+
QUnit.test('any() can add a listener to one event type on a different target object', function(assert) {
316+
const a = this.targets.a = evented({});
317+
const b = this.targets.b = evented({});
318+
const spy = sinon.spy();
319+
320+
a.any(b, 'x', spy);
321+
b.trigger('x');
322+
323+
// Make sure we aren't magically binding a listener to "a".
324+
a.trigger('x');
325+
326+
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
327+
328+
validateListenerCall(spy.getCall(0), a, {
329+
type: 'x',
330+
target: b.eventBusEl_
331+
});
332+
});
333+
334+
QUnit.test('any() can add a listener to an array of event types on a different target object', function(assert) {
335+
const a = this.targets.a = evented({});
336+
const b = this.targets.b = evented({});
337+
const spy = sinon.spy();
338+
339+
a.any(b, ['x', 'y'], spy);
340+
b.trigger('x');
341+
b.trigger('y');
342+
b.trigger('x');
343+
b.trigger('y');
344+
345+
// Make sure we aren't magically binding a listener to "a".
346+
a.trigger('x');
347+
a.trigger('y');
348+
349+
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
350+
351+
validateListenerCall(spy.getCall(0), a, {
352+
type: 'x',
353+
target: b.eventBusEl_
354+
});
355+
});
356+
257357
QUnit.test('off() with no arguments will remove all listeners from all events on this object', function(assert) {
258358
const a = this.targets.a = evented({});
259359
const spyX = sinon.spy();

0 commit comments

Comments
 (0)