From 0bbc3f31fc1f98c51de12dc9b590b0d81e826fbe Mon Sep 17 00:00:00 2001 From: Khang Nguyen-Le Date: Tue, 19 Sep 2017 15:23:18 +0800 Subject: [PATCH] Add space-delimited list of events support for triggerMethod --- docs/marionette.functions.md | 4 + src/common/trigger-method.js | 42 +++-- test/unit/common/trigger-method.spec.js | 196 +++++++++++++++++++----- 3 files changed, 195 insertions(+), 47 deletions(-) diff --git a/docs/marionette.functions.md b/docs/marionette.functions.md index 8349ff6b5a..07e95e7df4 100644 --- a/docs/marionette.functions.md +++ b/docs/marionette.functions.md @@ -227,6 +227,10 @@ var myObj = new MyObject(); // console.log "baz" Mn.triggerMethod(myObj, 'foo', 'qux'); // console.log "qux" ``` +To trigger several events and corresponding methods split the events by the space. Example: + +* `triggerMethod('foo bar')` fires the `onFoo` and the `onBar` functions + *Note*: Some Marionette classes such as Views have an overridden `triggerMethod`. Using `Mn.triggerMethod` with a view will break event proxying. If you need to run `triggerMethod` on a Marionette class [`triggerMethodOn`](#marionette-triggermethodon) is recommended. ## Marionette.triggerMethodOn diff --git a/src/common/trigger-method.js b/src/common/trigger-method.js index 064bfb42ec..f28f45d04c 100644 --- a/src/common/trigger-method.js +++ b/src/common/trigger-method.js @@ -5,7 +5,8 @@ import _ from 'underscore'; import getOption from './get-option'; // split the event name on the ":" -const splitter = /(^|:)(\w)/gi; +const colonSplitter = /(^|:)(\w)/gi; +const spaceSplitter = /\s+/; // take the event section ("section1:section2:section3") // and turn it in to uppercase name onSection1Section2Section3 @@ -14,9 +15,33 @@ function getEventName(match, prefix, eventName) { } const getOnMethodName = _.memoize(function(event) { - return 'on' + event.replace(splitter, getEventName); + return 'on' + event.replace(colonSplitter, getEventName); }); +function isManyEvents(events) { + return events && spaceSplitter.test(events); +} + +function execEventMethod(event, args) { + // get the method name from the events name + const methodName = getOnMethodName(event); + const method = getOption.call(this, methodName); + + // call the onMethodName if it exists + if (_.isFunction(method)) { + // pass all args, except the event name + return method.apply(this, args); + } +} + +function execEventMethods(events, args) { + return events.split(spaceSplitter) + .filter(event => event.length > 0) + .map((event) => { + return execEventMethod.call(this, event, args); + }); +} + // Trigger an event and/or a corresponding method name. Examples: // // `this.triggerMethod("foo")` will trigger the "foo" event and @@ -24,16 +49,13 @@ const getOnMethodName = _.memoize(function(event) { // // `this.triggerMethod("foo:bar")` will trigger the "foo:bar" event and // call the "onFooBar" method. -export function triggerMethod(event, ...args) { - // get the method name from the event name - const methodName = getOnMethodName(event); - const method = getOption.call(this, methodName); +export function triggerMethod(events, ...args) { let result; - // call the onMethodName if it exists - if (_.isFunction(method)) { - // pass all args, except the event name - result = method.apply(this, args); + if (isManyEvents(events)) { + result = execEventMethods.call(this, events, args); + } else { + result = execEventMethod.call(this, events, args); } // trigger the event diff --git a/test/unit/common/trigger-method.spec.js b/test/unit/common/trigger-method.spec.js index a6770f34f6..e6273b1d98 100644 --- a/test/unit/common/trigger-method.spec.js +++ b/test/unit/common/trigger-method.spec.js @@ -2,24 +2,37 @@ describe('trigger event and method name', function() { 'use strict'; beforeEach(function() { - this.returnValue = 'foo'; - this.argumentOne = 'bar'; - this.argumentTwo = 'baz'; + this.returnValue1 = 'foo'; + this.returnValue2 = 'bar'; + this.argumentOne = 'baz'; + this.argumentTwo = 'bax'; - this.eventHandler = this.sinon.stub(); - this.methodHandler = this.sinon.stub().returns(this.returnValue); + this.eventHandler1 = this.sinon.stub(); + this.eventHandler2 = this.sinon.stub(); + this.methodHandler1 = this.sinon.stub().returns(this.returnValue1); + this.methodHandler2 = this.sinon.stub().returns(this.returnValue2); }); describe('triggering an event when passed options', function() { beforeEach(function() { - this.view = new Marionette.View({ - onFoo: this.methodHandler - }); + this.view = new Marionette.View({onFoo: this.methodHandler1}); this.view.triggerMethod('foo'); }); it('should trigger the event', function() { - expect(this.methodHandler).to.have.been.calledOnce; + expect(this.methodHandler1).to.have.been.calledOnce; + }); + }); + + describe('triggering events when passed options', function() { + beforeEach(function() { + this.view = new Marionette.View({onFoo: this.methodHandler1, onBar: this.methodHandler2}); + this.view.triggerMethod('foo bar'); + }); + + it('should trigger the events', function() { + expect(this.methodHandler1).to.have.been.calledOnce; + expect(this.methodHandler2).to.have.been.calledOnce; }); }); @@ -28,21 +41,21 @@ describe('trigger event and method name', function() { this.view = new Marionette.View(); this.triggerMethodSpy = this.sinon.spy(this.view, 'triggerMethod'); - this.view.onFoo = this.methodHandler; - this.view.on('foo', this.eventHandler); + this.view.onFoo = this.methodHandler1; + this.view.on('foo', this.eventHandler1); this.view.triggerMethod('foo'); }); it('should trigger the event', function() { - expect(this.eventHandler).to.have.been.calledOnce; + expect(this.eventHandler1).to.have.been.calledOnce; }); it('should call a method named on{Event}', function() { - expect(this.methodHandler).to.have.been.calledOnce; + expect(this.methodHandler1).to.have.been.calledOnce; }); it('returns the value returned by the on{Event} method', function() { - expect(this.triggerMethodSpy).to.have.been.calledOnce.and.returned(this.returnValue); + expect(this.triggerMethodSpy).to.have.been.calledOnce.and.returned(this.returnValue1); }); describe('when trigger does not exist', function() { @@ -56,53 +69,151 @@ describe('trigger event and method name', function() { }); }); + describe('when triggering events', function() { + beforeEach(function() { + this.view = new Marionette.View(); + this.triggerMethodSpy = this.sinon.spy(this.view, 'triggerMethod'); + + this.view.onFoo = this.methodHandler1; + this.view.on('foo', this.eventHandler1); + this.view.onBar = this.methodHandler2; + this.view.on('bar', this.eventHandler2); + this.view.triggerMethod('foo bar'); + }); + + it('should trigger the event', function() { + expect(this.eventHandler1).to.have.been.calledOnce; + expect(this.eventHandler2).to.have.been.calledOnce; + }); + + it('should call a method named on{Event}', function() { + expect(this.methodHandler1).to.have.been.calledOnce; + expect(this.methodHandler2).to.have.been.calledOnce; + }); + + it('returns the value returned by the on{Event} method', function() { + expect(this.triggerMethodSpy).to.have.been.calledOnce.and.returned([this.returnValue1, this.returnValue2]); + }); + + describe('when trigger does not exist', function() { + beforeEach(function() { + this.triggerNonExistantEvent = _.partial(this.view.triggerMethod, 'does:not:exist1 does:not:exist2'); + }); + + it('should do nothing', function() { + expect(this.triggerNonExistantEvent).not.to.throw; + }); + }); + }); + describe('when triggering an event with arguments', function() { beforeEach(function() { this.view = new Marionette.View(); - this.view.onFoo = this.methodHandler; - this.view.on('foo', this.eventHandler); + this.view.onFoo = this.methodHandler1; + this.view.on('foo', this.eventHandler1); this.view.triggerMethod('foo', this.argumentOne, this.argumentTwo); }); it('should trigger the event with the args', function() { - expect(this.eventHandler).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + expect(this.eventHandler1).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); }); it('should call a method named on{Event} with the args', function() { - expect(this.methodHandler).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + expect(this.methodHandler1).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + }); + }); + + describe('when triggering events with arguments', function() { + beforeEach(function() { + this.view = new Marionette.View(); + this.view.onFoo = this.methodHandler1; + this.view.onBar = this.methodHandler2; + this.view.on('foo', this.eventHandler1); + this.view.on('bar', this.eventHandler2); + this.view.triggerMethod('foo bar', this.argumentOne, this.argumentTwo); + }); + + it('should trigger the events with the args', function() { + expect(this.eventHandler1).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + expect(this.eventHandler2).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + }); + + it('should call methods named on{Event} with the args', function() { + expect(this.methodHandler1).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + expect(this.methodHandler2).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); }); }); describe('when triggering an event with : separated name', function() { beforeEach(function() { this.view = new Marionette.View(); - this.view.onFooBar = this.methodHandler; - this.view.on('foo:bar', this.eventHandler); + this.view.onFooBar = this.methodHandler1; + this.view.on('foo:bar', this.eventHandler1); this.view.triggerMethod('foo:bar', this.argumentOne, this.argumentTwo); }); it('should trigger the event with the args', function() { - expect(this.eventHandler).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + expect(this.eventHandler1).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); }); it('should call a method named with each segment of the event name capitalized', function() { - expect(this.methodHandler).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + expect(this.methodHandler1).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + }); + }); + + describe('when triggering events with : separated name', function() { + beforeEach(function() { + this.view = new Marionette.View(); + this.view.onFooBar = this.methodHandler1; + this.view.onBarFoo = this.methodHandler2; + this.view.on('foo:bar', this.eventHandler1); + this.view.on('bar:foo', this.eventHandler2); + this.view.triggerMethod('foo:bar bar:foo', this.argumentOne, this.argumentTwo); + }); + + it('should trigger the events with the args', function() { + expect(this.eventHandler1).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + expect(this.eventHandler2).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + }); + + it('should call methods named with each segment of the event name capitalized', function() { + expect(this.methodHandler1).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + expect(this.methodHandler2).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); }); }); describe('when triggering an event and no handler method exists', function() { beforeEach(function() { this.view = new Marionette.View(); - this.view.on('foo:bar', this.eventHandler); + this.view.on('foo:bar', this.eventHandler1); this.view.triggerMethod('foo:bar', this.argumentOne, this.argumentTwo); }); it('should trigger the event with the args', function() { - expect(this.eventHandler).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + expect(this.eventHandler1).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); }); it('should not call a method named with each segment of the event name capitalized', function() { - expect(this.methodHandler).not.to.have.been.calledOnce; + expect(this.methodHandler1).not.to.have.been.calledOnce; + }); + }); + + describe('when triggering events and no handler method exists', function() { + beforeEach(function() { + this.view = new Marionette.View(); + this.view.on('foo:bar', this.eventHandler1); + this.view.on('bar:foo', this.eventHandler2); + this.view.triggerMethod('foo:bar bar:foo', this.argumentOne, this.argumentTwo); + }); + + it('should trigger the events with the args', function() { + expect(this.eventHandler1).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + expect(this.eventHandler2).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + }); + + it('should not call methods named with each segment of the event name capitalized', function() { + expect(this.methodHandler1).not.to.have.been.calledOnce; + expect(this.methodHandler2).not.to.have.been.calledOnce; }); }); @@ -110,37 +221,47 @@ describe('trigger event and method name', function() { beforeEach(function() { this.view = new Marionette.View(); this.view.onFooBar = 'baz'; - this.view.on('foo:bar', this.eventHandler); - this.view.triggerMethod('foo:bar', this.argumentOne, this.argumentTwo); + this.view.onBarFoo = 'bax'; + this.view.on('foo:bar', this.eventHandler1); + this.view.on('bar:foo', this.eventHandler2); + this.view.triggerMethod('foo:bar bar:foo', this.argumentOne, this.argumentTwo); }); - it('should trigger the event with the args', function() { - expect(this.eventHandler).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + it('should trigger the events with the args', function() { + expect(this.eventHandler1).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); + expect(this.eventHandler2).to.have.been.calledOnce.and.calledWith(this.argumentOne, this.argumentTwo); }); - it('should not call a method named with each segment of the event name capitalized', function() { - expect(this.methodHandler).not.to.have.been.calledOnce; + it('should not call methods named with each segment of the event name capitalized', function() { + expect(this.methodHandler1).not.to.have.been.calledOnce; + expect(this.methodHandler2).not.to.have.been.calledOnce; }); }); describe('triggering events through a child view', function() { beforeEach(function() { this.onChildviewFooClickStub = this.sinon.stub(); + this.onChildviewBarClickStub = this.sinon.stub(); this.View = Marionette.View.extend({ template: _.template('foo'), - triggers: {'click': 'foo:click'} + triggers: { + 'click': 'foo:click bar:click' + } }); this.CollectionView = Marionette.CollectionView.extend({ childView: this.View, - onChildviewFooClick: this.onChildviewFooClickStub + onChildviewFooClick: this.onChildviewFooClickStub, + onChildviewBarClick: this.onChildviewBarClickStub }); - this.collection = new Backbone.Collection([{foo: 'bar'}]); - this.collectionView = new this.CollectionView({ - collection: this.collection - }); + this.collection = new Backbone.Collection([ + { + foo: 'bar' + } + ]); + this.collectionView = new this.CollectionView({collection: this.collection}); this.collectionView.render(); this.childView = this.collectionView.children.findByModel(this.collection.at(0)); @@ -149,6 +270,7 @@ describe('trigger event and method name', function() { it('should fire the event method once', function() { expect(this.onChildviewFooClickStub).to.have.been.calledOnce; + expect(this.onChildviewBarClickStub).to.have.been.calledOnce; }); }); });