diff --git a/src/Ractive/prototype/fire.js b/src/Ractive/prototype/fire.js index c3ec05929a..82728ae4e9 100644 --- a/src/Ractive/prototype/fire.js +++ b/src/Ractive/prototype/fire.js @@ -1,19 +1,10 @@ -export default function Ractive$fire ( eventName, event ) { - var args, i, len, originalEvent, stopEvent = false, subscribers = this._subs[ eventName ]; +import fireEvent from 'Ractive/prototype/shared/fireEvent'; - if ( !subscribers ) { - return; - } +export default function Ractive$fire ( eventName ) { - args = Array.prototype.slice.call( arguments, 1 ); + var options = { + args: Array.prototype.slice.call( arguments, 1 ) + }; - for ( i=0, len=subscribers.length; i= 0; i-- ) { + subscribers = ractive._subs[ eventNames[ i ] ]; + + if ( subscribers ) { + bubble = notifySubscribers( ractive, subscribers, event, args ) && bubble; + } + } + + if ( ractive._parent && bubble ) { + + if ( initialFire && ractive.component ) { + let fullName = ractive.component.name + '.' + eventNames[ eventNames.length-1 ]; + eventNames = getPotentialWildcardMatches( fullName ); + + if( event ) { + event.component = ractive; + } + } + + fireEventAs( ractive._parent, eventNames, event, args ); + } +} + +function notifySubscribers ( ractive, subscribers, event, args ) { + + var originalEvent = null, stopEvent = false; + + if ( event ) { + args = [ event ].concat( args ); + } + + for ( let i = 0, len = subscribers.length; i < len; i += 1 ) { + if ( subscribers[ i ].apply( ractive, args ) === false ) { + stopEvent = true; + } + } + + if ( event && stopEvent && ( originalEvent = event.original ) ) { + originalEvent.preventDefault && originalEvent.preventDefault(); + originalEvent.stopPropagation && originalEvent.stopPropagation(); + } + + return !stopEvent; +} + + diff --git a/src/Ractive/prototype/teardown.js b/src/Ractive/prototype/teardown.js index a577f91fc7..9e472491be 100644 --- a/src/Ractive/prototype/teardown.js +++ b/src/Ractive/prototype/teardown.js @@ -1,3 +1,4 @@ +import fireEvent from 'Ractive/prototype/shared/fireEvent'; import removeFromArray from 'utils/removeFromArray'; import Promise from 'utils/Promise'; @@ -7,7 +8,7 @@ import Promise from 'utils/Promise'; export default function Ractive$teardown ( callback ) { var promise; - this.fire( 'teardown' ); + fireEvent( this, 'teardown' ); this.fragment.unbind(); this.viewmodel.teardown(); diff --git a/src/Ractive/prototype/update.js b/src/Ractive/prototype/update.js index 34b46b0fdd..b1cbe81c58 100644 --- a/src/Ractive/prototype/update.js +++ b/src/Ractive/prototype/update.js @@ -1,3 +1,4 @@ +import fireEvent from 'Ractive/prototype/shared/fireEvent'; import runloop from 'global/runloop'; export default function Ractive$update ( keypath, callback ) { @@ -15,7 +16,7 @@ export default function Ractive$update ( keypath, callback ) { this.viewmodel.mark( keypath ); runloop.end(); - this.fire( 'update', keypath ); + fireEvent( this, 'update', { args: [ keypath ] }); if ( callback ) { promise.then( callback.bind( this ) ); diff --git a/src/config/defaults/options.js b/src/config/defaults/options.js index 67bb8b59dc..ff5153ece5 100644 --- a/src/config/defaults/options.js +++ b/src/config/defaults/options.js @@ -1,8 +1,3 @@ -// These are both the values for Ractive.defaults -// as well as the determination for whether an option -// value will be placed on Component.defaults -// (versus directly on Component) during an extend operation - var defaultOptions = { // render placement: diff --git a/src/config/errors.js b/src/config/errors.js index fe13783287..7d60619d44 100644 --- a/src/config/errors.js +++ b/src/config/errors.js @@ -33,5 +33,8 @@ export default { 'A function was specified for "{name}" {registry}, but no {registry} was returned', defaultElSpecified: - 'The <{name}/> component has a default `el` property; it has been disregarded' + 'The <{name}/> component has a default `el` property; it has been disregarded', + + noElementProxyEventWildcards: + 'Only component proxy-events may contain "*" wildcards, <{element} on-{event}/> is not valid.' }; diff --git a/src/global/runloop.js b/src/global/runloop.js index 33209d7ca4..d9a754621d 100644 --- a/src/global/runloop.js +++ b/src/global/runloop.js @@ -1,4 +1,5 @@ import circular from 'circular'; +import fireEvent from 'Ractive/prototype/shared/fireEvent'; import removeFromArray from 'utils/removeFromArray'; import Promise from 'utils/Promise'; import resolveRef from 'shared/resolveRef'; @@ -88,7 +89,7 @@ function flushChanges () { changeHash = thing.applyChanges(); if ( changeHash ) { - thing.ractive.fire( 'change', changeHash ); + fireEvent( thing.ractive, 'change', { args: [ changeHash ] }); } } batch.viewmodels.length = 0; diff --git a/src/parse/converters/element.js b/src/parse/converters/element.js index 253296653b..319563321d 100644 --- a/src/parse/converters/element.js +++ b/src/parse/converters/element.js @@ -10,7 +10,7 @@ import processDirective from 'parse/converters/element/processDirective'; var tagNamePattern = /^[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/, validTagNameFollower = /^[\s\n\/>]/, onPattern = /^on/, - proxyEventPattern = /^on-([a-zA-Z$_][a-zA-Z$_0-9\-]+)$/, + proxyEventPattern = /^on-([a-zA-Z\\*\\.$_][a-zA-Z\\*\\.$_0-9\-]+)$/, reservedEventNames = /^(?:change|reset|teardown|update)$/, directives = { 'intro-outro': 't0', intro: 't1', outro: 't2', decorator: 'o' }, exclude = { exclude: true }, diff --git a/src/shared/notifyPatternObservers.js b/src/shared/notifyPatternObservers.js index 99782a8f24..20780466cf 100644 --- a/src/shared/notifyPatternObservers.js +++ b/src/shared/notifyPatternObservers.js @@ -1,5 +1,6 @@ -var lastKey = /[^\.]+$/, - starMaps = {}; +import getPotentialWildcardMatches from 'utils/getPotentialWildcardMatches'; + +var lastKey = /[^\.]+$/; // TODO split into two functions? i.e. one for the top-level call, one for the cascade export default function notifyPatternObservers ( ractive, registeredKeypath, actualKeypath, isParentOfChangedKeypath, isTopLevelCall ) { @@ -48,68 +49,3 @@ export default function notifyPatternObservers ( ractive, registeredKeypath, act } } -// This function takes a keypath such as 'foo.bar.baz', and returns -// all the variants of that keypath that include a wildcard in place -// of a key, such as 'foo.bar.*', 'foo.*.baz', 'foo.*.*' and so on. -// These are then checked against the dependants map (ractive.viewmodel.depsMap) -// to see if any pattern observers are downstream of one or more of -// these wildcard keypaths (e.g. 'foo.bar.*.status') -function getPotentialWildcardMatches ( keypath ) { - var keys, starMap, mapper, i, result, wildcardKeypath; - - keys = keypath.split( '.' ); - starMap = getStarMap( keys.length ); - - result = []; - - mapper = function ( star, i ) { - return star ? '*' : keys[i]; - }; - - i = starMap.length; - while ( i-- ) { - wildcardKeypath = starMap[i].map( mapper ).join( '.' ); - - if ( !result[ wildcardKeypath ] ) { - result.push( wildcardKeypath ); - result[ wildcardKeypath ] = true; - } - } - - return result; -} - -// This function returns all the possible true/false combinations for -// a given number - e.g. for two, the possible combinations are -// [ true, true ], [ true, false ], [ false, true ], [ false, false ]. -// It does so by getting all the binary values between 0 and e.g. 11 -function getStarMap ( num ) { - var ones = '', max, binary, starMap, mapper, i; - - if ( !starMaps[ num ] ) { - starMap = []; - - while ( ones.length < num ) { - ones += 1; - } - - max = parseInt( ones, 2 ); - - mapper = function ( digit ) { - return digit === '1'; - }; - - for ( i = 0; i <= max; i += 1 ) { - binary = i.toString( 2 ); - while ( binary.length < num ) { - binary = '0' + binary; - } - - starMap[i] = Array.prototype.map.call( binary, mapper ); - } - - starMaps[ num ] = starMap; - } - - return starMaps[ num ]; -} diff --git a/src/utils/getPotentialWildcardMatches.js b/src/utils/getPotentialWildcardMatches.js new file mode 100644 index 0000000000..6f0129cf7d --- /dev/null +++ b/src/utils/getPotentialWildcardMatches.js @@ -0,0 +1,72 @@ +var starMaps = {}; + +// This function takes a keypath such as 'foo.bar.baz', and returns +// all the variants of that keypath that include a wildcard in place +// of a key, such as 'foo.bar.*', 'foo.*.baz', 'foo.*.*' and so on. +// These are then checked against the dependants map (ractive.viewmodel.depsMap) +// to see if any pattern observers are downstream of one or more of +// these wildcard keypaths (e.g. 'foo.bar.*.status') +export function getPotentialWildcardMatches ( keypath ) { + var keys, starMap, mapper, i, result, wildcardKeypath; + + keys = keypath.split( '.' ); + if( !( starMap = starMaps[ keys.length ]) ) { + starMap = getStarMap( keys.length ); + } + + result = []; + + mapper = function ( star, i ) { + return star ? '*' : keys[i]; + }; + + i = starMap.length; + while ( i-- ) { + wildcardKeypath = starMap[i].map( mapper ).join( '.' ); + + if ( !result[ wildcardKeypath ] ) { + result.push( wildcardKeypath ); + result[ wildcardKeypath ] = true; + } + } + + return result; +} + +// This function returns all the possible true/false combinations for +// a given number - e.g. for two, the possible combinations are +// [ true, true ], [ true, false ], [ false, true ], [ false, false ]. +// It does so by getting all the binary values between 0 and e.g. 11 +function getStarMap ( num ) { + var ones = '', max, binary, starMap, mapper, i; + + if ( !starMaps[ num ] ) { + starMap = []; + + while ( ones.length < num ) { + ones += 1; + } + + max = parseInt( ones, 2 ); + + mapper = function ( digit ) { + return digit === '1'; + }; + + for ( i = 0; i <= max; i += 1 ) { + binary = i.toString( 2 ); + while ( binary.length < num ) { + binary = '0' + binary; + } + + starMap[i] = Array.prototype.map.call( binary, mapper ); + } + + starMaps[ num ] = starMap; + } + + return starMaps[ num ]; +} + + + diff --git a/src/virtualdom/Fragment/prototype/bubble.js b/src/virtualdom/Fragment/prototype/bubble.js index 05ec936275..e783304c33 100644 --- a/src/virtualdom/Fragment/prototype/bubble.js +++ b/src/virtualdom/Fragment/prototype/bubble.js @@ -1,7 +1,7 @@ export default function Fragment$bubble () { this.dirtyValue = this.dirtyArgs = true; - if ( this.inited && this.owner.bubble ) { + if ( this.inited && typeof this.owner.bubble === 'function' ) { this.owner.bubble(); } } diff --git a/src/virtualdom/items/Component/initialise/propagateEvents.js b/src/virtualdom/items/Component/initialise/propagateEvents.js index f896f40e9a..a7dd0a8649 100644 --- a/src/virtualdom/items/Component/initialise/propagateEvents.js +++ b/src/virtualdom/items/Component/initialise/propagateEvents.js @@ -1,3 +1,5 @@ +import circular from 'circular'; +import fireEvent from 'Ractive/prototype/shared/fireEvent'; import log from 'utils/log'; // TODO how should event arguments be handled? e.g. @@ -6,7 +8,13 @@ import log from 'utils/log'; // when 'foo' fires on the child, but the 1,2,3 arguments // will be lost -export default function ( component, eventsDescriptor ) { +var Fragment; + +circular.push( function () { + Fragment = circular.Fragment; +}); + +export default function propagateEvents ( component, eventsDescriptor ) { var eventName; for ( eventName in eventsDescriptor ) { @@ -19,7 +27,6 @@ export default function ( component, eventsDescriptor ) { function propagateEvent ( childInstance, parentInstance, eventName, proxyEventName ) { if ( typeof proxyEventName !== 'string' ) { - log.error({ debug: parentInstance.debug, message: 'noComponentEventArguments' @@ -27,9 +34,24 @@ function propagateEvent ( childInstance, parentInstance, eventName, proxyEventNa } childInstance.on( eventName, function () { - var args = Array.prototype.slice.call( arguments ); - args.unshift( proxyEventName ); + var options; + + // semi-weak test, but what else? tag the event obj ._isEvent ? + if ( arguments[0].node ) { + options = { + event: Array.prototype.shift.call( arguments ), + args: arguments + }; + } + else { + options = { + args: Array.prototype.slice.call( arguments ) + }; + } + + fireEvent( parentInstance, proxyEventName, options ); - parentInstance.fire.apply( parentInstance, args ); + // cancel bubbling + return false; }); } diff --git a/src/virtualdom/items/Component/prototype/unrender.js b/src/virtualdom/items/Component/prototype/unrender.js index 3a56c598f4..16a13d052d 100644 --- a/src/virtualdom/items/Component/prototype/unrender.js +++ b/src/virtualdom/items/Component/prototype/unrender.js @@ -1,5 +1,7 @@ +import fireEvent from 'Ractive/prototype/shared/fireEvent'; + export default function Component$unrender ( shouldDestroy ) { - this.instance.fire( 'teardown' ); + fireEvent( this.instance, 'teardown', { reserved: true }); this.shouldDestroy = shouldDestroy; this.instance.unrender(); diff --git a/src/virtualdom/items/Element/EventHandler/prototype/fire.js b/src/virtualdom/items/Element/EventHandler/prototype/fire.js index f91c9aaaf1..7a718b1cae 100644 --- a/src/virtualdom/items/Element/EventHandler/prototype/fire.js +++ b/src/virtualdom/items/Element/EventHandler/prototype/fire.js @@ -1,5 +1,7 @@ +import fireEvent from 'Ractive/prototype/shared/fireEvent'; + // This function may be overwritten, if the event directive // includes parameters export default function EventHandler$fire ( event ) { - this.root.fire( this.getAction(), event ); + fireEvent( this.root, this.getAction(), { event: event } ); } diff --git a/src/virtualdom/items/Element/EventHandler/prototype/init.js b/src/virtualdom/items/Element/EventHandler/prototype/init.js index c08c420900..ca2578aca7 100644 --- a/src/virtualdom/items/Element/EventHandler/prototype/init.js +++ b/src/virtualdom/items/Element/EventHandler/prototype/init.js @@ -3,6 +3,8 @@ import getFunctionFromString from 'shared/getFunctionFromString'; import resolveRef from 'shared/resolveRef'; import Unresolved from 'shared/Unresolved'; import circular from 'circular'; +import fireEvent from 'Ractive/prototype/shared/fireEvent'; +import log from 'utils/log'; var Fragment, getValueOptions = { args: true }, eventPattern = /^event(?:\.(.+))?/; @@ -17,6 +19,19 @@ export default function EventHandler$init ( element, name, template ) { handler.root = element.root; handler.name = name; + if( name.indexOf( '*' ) !== -1 ) { + log.error({ + debug: this.root.debug, + message: 'noElementProxyEventWildcards', + args: { + element: element.tagName, + event: name + } + }); + + this.invalid = true; + } + if ( template.m ) { // This is a method call handler.method = template.m; @@ -142,7 +157,7 @@ function fireMethodCall ( event ) { } function fireEventWithParams ( event ) { - this.root.fire.apply( this.root, [ this.getAction(), event ].concat( this.params ) ); + fireEvent( this.root, this.getAction(), { event: event, args: this.params } ); } function fireEventWithDynamicParams ( event ) { @@ -153,5 +168,5 @@ function fireEventWithDynamicParams ( event ) { args = args.substr( 1, args.length - 2 ); } - this.root.fire.apply( this.root, [ this.getAction(), event ].concat( args ) ); + fireEvent( this.root, this.getAction(), { event: event, args: args } ); } diff --git a/src/virtualdom/items/Element/EventHandler/prototype/listen.js b/src/virtualdom/items/Element/EventHandler/prototype/listen.js index 0623951fdd..1203275730 100644 --- a/src/virtualdom/items/Element/EventHandler/prototype/listen.js +++ b/src/virtualdom/items/Element/EventHandler/prototype/listen.js @@ -1,6 +1,6 @@ -import warn from 'utils/warn'; import config from 'config/config'; import genericHandler from 'virtualdom/items/Element/EventHandler/shared/genericHandler'; +import log from 'utils/log'; var customHandlers = {}; @@ -8,12 +8,21 @@ export default function EventHandler$listen () { var definition, name = this.name; + if ( this.invalid ) { return; } + if ( definition = config.registries.events.find( this.root, name ) ) { this.custom = definition( this.node, getCustomHandler( name ) ); } else { // Looks like we're dealing with a standard DOM event... but let's check if ( !( 'on' + name in this.node ) && !( window && 'on' + name in window ) ) { - warn( 'Missing "' + this.name + '" event. You may need to download a plugin via http://docs.ractivejs.org/latest/plugins#events' ); + log.error({ + debug: this.root.debug, + message: 'missingPlugin', + args: { + plugin: 'event', + name: name + } + }); } this.node.addEventListener( name, genericHandler, false ); diff --git a/src/virtualdom/items/Element/Transition/prototype/animateStyle/createTransitions.js b/src/virtualdom/items/Element/Transition/prototype/animateStyle/createTransitions.js index 2ffaddbb28..833a76eb40 100644 --- a/src/virtualdom/items/Element/Transition/prototype/animateStyle/createTransitions.js +++ b/src/virtualdom/items/Element/Transition/prototype/animateStyle/createTransitions.js @@ -55,6 +55,7 @@ if ( !isClient ) { checkComplete = function () { if ( jsTransitionsComplete && cssTransitionsComplete ) { + // will changes to events and fire have an unexpected consequence here? t.root.fire( t.name + ':end', t.node, t.isIntro ); resolve(); } diff --git a/test/modules/elements.js b/test/modules/elements.js index 26a139db14..cf94146097 100644 --- a/test/modules/elements.js +++ b/test/modules/elements.js @@ -75,5 +75,15 @@ define([ 'ractive' ], function ( Ractive ) { t.equal( ractive.toHTML(), '' ); }); + + test( 'Wildcard proxy-events invalid on elements', function ( t ) { + throws( function () { + var ractive = new Ractive({ + el: fixture, + debug: true, + template: '

' + }); + }, /wildcards/ ); + }); }; }); diff --git a/test/modules/events.js b/test/modules/events.js index fc0da4bef5..e53adb655a 100644 --- a/test/modules/events.js +++ b/test/modules/events.js @@ -1,8 +1,3 @@ -// EVENT TESTS -// =========== -// -// TODO: add moar tests - define([ 'ractive' ], function ( Ractive ) { return function () { @@ -11,7 +6,7 @@ define([ 'ractive' ], function ( Ractive ) { module( 'Events' ); - test( 'on-click="someEvent" fires an event when user clicks the element', function ( t ) { + test( 'on-click="someEvent" fires an event when user clicks the element', t => { var ractive; expect( 2 ); @@ -104,7 +99,7 @@ define([ 'ractive' ], function ( Ractive ) { }); - test( 'Standard events have correct properties: node, original, keypath, context, index', function ( t ) { + test( 'Standard events have correct properties: node, original, keypath, context, index', t => { var ractive, fakeEvent; expect( 5 ); @@ -127,8 +122,18 @@ define([ 'ractive' ], function ( Ractive ) { simulant.fire( ractive.nodes.test, fakeEvent ); }); + test( 'Empty event names are safe, though do not fire', t => { + var ractive = new Ractive(); + + expect( 1 ); + ractive.on( '', function ( event ) { + throw new Error( 'Empty event name should not fire' ); + }); + ractive.fire( '' ); + t.ok( true ); + }); - test( 'preventDefault and stopPropagation if event handler returned false', function ( t ) { + test( 'preventDefault and stopPropagation if event handler returned false', t => { var ractive, preventedDefault = false, stoppedPropagation = false; expect( 9 ); @@ -187,7 +192,7 @@ define([ 'ractive' ], function ( Ractive ) { }); - test( 'event.keypath is set to the innermost context', function ( t ) { + test( 'event.keypath is set to the innermost context', t => { var ractive; expect( 2 ); @@ -208,7 +213,7 @@ define([ 'ractive' ], function ( Ractive ) { simulant.fire( ractive.nodes.test, 'click' ); }); - test( 'event.index stores current indices against their references', function ( t ) { + test( 'event.index stores current indices against their references', t => { var ractive; expect( 4 ); @@ -231,7 +236,7 @@ define([ 'ractive' ], function ( Ractive ) { simulant.fire( ractive.nodes.item_2, 'click' ); }); - test( 'event.index reports nested indices correctly', function ( t ) { + test( 'event.index reports nested indices correctly', t => { var ractive; expect( 2 ); @@ -261,7 +266,7 @@ define([ 'ractive' ], function ( Ractive ) { simulant.fire( ractive.nodes.test_001, 'click' ); }); - test( 'proxy events can have dynamic names', function ( t ) { + test( 'proxy events can have dynamic names', t => { var ractive, last; expect( 2 ); @@ -290,7 +295,7 @@ define([ 'ractive' ], function ( Ractive ) { t.equal( last, 'bar' ); }); - test( 'proxy event parameters are correctly parsed as JSON, or treated as a string', function ( t ) { + test( 'proxy event parameters are correctly parsed as JSON, or treated as a string', t => { var ractive, last; expect( 3 ); @@ -316,7 +321,7 @@ define([ 'ractive' ], function ( Ractive ) { t.deepEqual( last, [ 1, 2, 3 ] ); }); - test( 'proxy events can have dynamic arguments', function ( t ) { + test( 'proxy events can have dynamic arguments', t => { var ractive; ractive = new Ractive({ @@ -336,7 +341,7 @@ define([ 'ractive' ], function ( Ractive ) { simulant.fire( ractive.nodes.foo, 'click' ); }); - test( 'proxy events can have multiple arguments', function ( t ) { + test( 'proxy events can have multiple arguments', t => { var ractive; ractive = new Ractive({ @@ -368,7 +373,7 @@ define([ 'ractive' ], function ( Ractive ) { simulant.fire( ractive.nodes.baz, 'click' ); }); - test( 'Splicing arrays correctly modifies proxy events', function ( t ) { + test( 'Splicing arrays correctly modifies proxy events', t => { var ractive; expect( 4 ); @@ -395,7 +400,7 @@ define([ 'ractive' ], function ( Ractive ) { t.equal( ractive.findAll( 'button' ).length, 2 ); }); - test( 'Splicing arrays correctly modifies two-way bindings', function ( t ) { + test( 'Splicing arrays correctly modifies two-way bindings', t => { var ractive, items; expect( 25 ); @@ -468,7 +473,7 @@ define([ 'ractive' ], function ( Ractive ) { t.equal( ractive.findAll( 'input' ).length, 2 ); }); - test( 'Calling ractive.off() without a keypath removes all handlers', function ( t ) { + test( 'Calling ractive.off() without a keypath removes all handlers', t => { var ractive = new Ractive({ el: fixture, template: 'doesn\'t matter' @@ -495,7 +500,7 @@ define([ 'ractive' ], function ( Ractive ) { ractive.fire( 'baz' ); }); - test( 'Changes triggered by two-way bindings propagate properly (#460)', function ( t ) { + test( 'Changes triggered by two-way bindings propagate properly (#460)', t => { var changes, ractive = new Ractive({ el: fixture, template: '{{#items}}{{/items}}

{{ items.filter( completed ).length }}

{{# items.filter( completed ).length }}

foo

{{/ items.filter( completed ).length }}', @@ -527,7 +532,7 @@ define([ 'ractive' ], function ( Ractive ) { t.htmlEqual( ractive.find( '.result' ).innerHTML, '0' ); }); - test( 'Multiple events can share the same directive', function ( t ) { + test( 'Multiple events can share the same directive', t => { var ractive, count = 0; ractive = new Ractive({ @@ -546,7 +551,7 @@ define([ 'ractive' ], function ( Ractive ) { t.equal( count, 2 ); }); - test( 'Superfluous whitespace is ignored', function ( t ) { + test( 'Superfluous whitespace is ignored', t => { var ractive, fooCount = 0, barCount = 0; ractive = new Ractive({ @@ -574,7 +579,7 @@ define([ 'ractive' ], function ( Ractive ) { t.equal( barCount, 1 ); }); - test( 'Multiple space-separated events can be handled with a single callback (#731)', function ( t ) { + test( 'Multiple space-separated events can be handled with a single callback (#731)', t => { var ractive, count = 0; ractive = new Ractive({}); @@ -611,7 +616,7 @@ define([ 'ractive' ], function ( Ractive ) { t.equal( returnedValue, ractive ); }); - test( 'Events really do not call addEventListener when no proxy name', function ( t ) { + test( 'Events really do not call addEventListener when no proxy name', t => { var ractive, addEventListener = Element.prototype.addEventListener, errorAdd = function(){ @@ -648,7 +653,7 @@ define([ 'ractive' ], function ( Ractive ) { }); - test( '@index can be used in proxy event directives', function ( t ) { + test( '@index can be used in proxy event directives', t => { var ractive = new Ractive({ el: fixture, template: '{{#each letters}}{{/each}}', @@ -777,6 +782,289 @@ define([ 'ractive' ], function ( Ractive ) { }); + var Component, Middle, View, setup; + + setup = { + setup: function(){ + Component = Ractive.extend({ + template: 'click me' + }); + + Middle = Ractive.extend({ + template: '' + }); + + View = Ractive.extend({ + el: fixture, + template: '', + components: { + component: Component, + middle: Middle + } + }); + + }, + teardown: function(){ + Component = Middle = View = void 0; + } + }; + + function fired ( event ) { + ok( true ); + } + + function goodEvent( event ) { + ok( event.context || event === 'foo' ); + } + + function goodEventWithArg( event, arg ) { + equal( arg || event, 'foo' ); + } + + function shouldNotFire () { + throw new Error( 'This event should not fire' ); + } + + function notOnOriginating () { + throw new Error( 'Namespaced event should not fire on originating component' ); + } + + function shouldBeNoBubbling () { + throw new Error( 'Event bubbling should not have happened' ); + } + + function testEventBubbling( fire ) { + + test( 'Events bubble under "eventname", and also "component.eventname" above firing component', t => { + var ractive, middle, component; + + expect( 3 ); + + ractive = new View(); + middle = ractive.findComponent( 'middle' ); + component = ractive.findComponent( 'component' ); + + component.on( 'someEvent', goodEvent ); + component.on( 'component.someEvent', notOnOriginating ); + + middle.on( 'someEvent', shouldNotFire ); + middle.on( 'component.someEvent', goodEvent ); + + ractive.on( 'someEvent', shouldNotFire ); + ractive.on( 'component.someEvent', goodEvent ); + + fire( ractive.findComponent( 'component' ) ); + }); + + test( 'arguments bubble', t => { + var ractive, middle, component; + + expect( 3 ); + + Component.prototype.template = 'click me' + + ractive = new View(); + middle = ractive.findComponent( 'middle' ); + component = ractive.findComponent( 'component' ); + + component.on( 'someEvent', goodEventWithArg ); + component.on( 'component.someEvent', notOnOriginating ); + + middle.on( 'someEvent', shouldNotFire ); + middle.on( 'component.someEvent', goodEventWithArg ); + + ractive.on( 'someEvent', shouldNotFire ); + ractive.on( 'component.someEvent', goodEventWithArg ); + + fire( ractive.findComponent( 'component' ) ); + }); + + test( 'bubbling events can be stopped by returning false', t => { + var ractive, middle, component; + + expect( 2 ); + + ractive = new View(); + middle = ractive.findComponent( 'middle' ); + component = ractive.findComponent( 'component' ); + + component.on( 'someEvent', goodEvent ); + component.on( 'component.someEvent', notOnOriginating ); + + middle.on( 'component.someEvent', function( event ) { + return false; + }); + // still fires on same level + middle.on( 'component.someEvent', goodEvent ); + + ractive.on( 'component.someEvent', shouldBeNoBubbling ); + + fire( ractive.findComponent( 'component' ) ); + }); + + test( 'bubbling events with event object have component reference', t => { + var ractive, middle, component; + + expect( 3 ); + + ractive = new View(); + middle = ractive.findComponent( 'middle' ); + component = ractive.findComponent( 'component' ); + + function hasComponentRef( event, arg ) { + event.original ? t.equal( event.component, component ) : t.ok( true ); + } + + component.on( 'someEvent', function( event ) { + t.ok( !event.component ); + }); + middle.on( 'component.someEvent', hasComponentRef ); + ractive.on( 'component.someEvent', hasComponentRef ); + + fire( ractive.findComponent( 'component' ) ); + }); + + } + + + module( 'Component events bubbling proxy events', setup ) + + testEventBubbling( function ( component ) { + simulant.fire( component.nodes.test, 'click' ); + }); + + module( 'Component events bubbling fire() events', setup ) + + testEventBubbling( function ( component ) { + component.fire( 'someEvent', 'foo' ); + }); + + module( 'Event pattern matching' ); + + test( 'handlers can use pattern matching', t => { + var ractive; + + expect( 4 ); + + ractive = new Ractive({ + el: fixture, + template: 'click me' + }); + + ractive.on( '*.*', fired); + ractive.on( 'some.*', fired); + ractive.on( '*.event', fired); + ractive.on( 'some.event', fired); + + simulant.fire( ractive.nodes.test, 'click' ); + }); + + test( 'bubbling handlers can use pattern matching', t => { + var Component, component, ractive; + + expect( 4 ); + + Component = Ractive.extend({ + template: 'click me' + }); + + ractive = new Ractive({ + el: fixture, + template: '', + components: { + component: Component + } + }); + + ractive.on( '*.*', fired); + ractive.on( 'component.*', fired); + ractive.on( '*.foo', fired); + ractive.on( 'component.foo', fired); + + component = ractive.findComponent( 'component' ); + simulant.fire( component.nodes.test, 'click' ); + + // otherwise we get cross test failure due to "teardown" event + // becasue we're reusing fixture element + ractive.off(); + }); + + test( 'component "on-someEvent" implicitly cancels bubbling', t => { + var Component, component, ractive; + + expect( 1 ); + + Component = Ractive.extend({ + template: 'click me' + }); + + ractive = new Ractive({ + el: fixture, + template: '', + components: { + component: Component + } + }); + + ractive.on( 'foo', fired); + ractive.on( 'component.someEvent', shouldBeNoBubbling); + + component = ractive.findComponent( 'component' ); + simulant.fire( component.nodes.test, 'click' ); + }); + + test( 'component "on-" wildcards match', t => { + var Component, component, ractive; + + expect( 3 ); + + Component = Ractive.extend({ + template: 'click me' + }); + + ractive = new Ractive({ + el: fixture, + template: '', + components: { + component: Component + } + }); + + ractive.on( 'foo', fired); + ractive.on( 'bar', fired); + ractive.on( 'both', fired); + + component = ractive.findComponent( 'component' ); + simulant.fire( component.nodes.test, 'click' ); + }); + + test( 'component "on-" do not get auto-namespaced events', t => { + var Component, component, ractive; + + expect( 1 ); + + Component = Ractive.extend({ + template: 'click me' + }); + + ractive = new Ractive({ + el: fixture, + template: '', + components: { + component: Component + } + }); + + ractive.on( 'foo', shouldNotFire); + + component = ractive.findComponent( 'component' ); + simulant.fire( component.nodes.test, 'click' ); + t.ok( true ); + }); + + + + }; });