diff --git a/spec/bindingAttributeBehaviors.js b/spec/bindingAttributeBehaviors.js index b622f0c63..6ebce418c 100644 --- a/spec/bindingAttributeBehaviors.js +++ b/spec/bindingAttributeBehaviors.js @@ -165,6 +165,30 @@ describe('Binding attribute syntax', { value_of(passedValues[1]).should_be("goodbye"); }, + 'Should trigger koInit and koUpdate event (with bubbling)': function () { + var observable = new ko.observable("hello"); + var initLog = []; + var updateLog = []; + testNode.innerHTML = "
"; + ko.applyBindings({ + myObservable: observable, + myLog: function(array, elem) { array.push('innerText' in elem ? elem.innerText : elem.textContent); }, + myInit: function(data, event) { this.myLog(initLog, event.target); }, + myUpdate: function(data, event) { this.myLog(updateLog, event.target); } + }, testNode); + value_of(initLog.length).should_be(2); + value_of(updateLog.length).should_be(2); + value_of(initLog[0]).should_be(""); + value_of(updateLog[0]).should_be(""); + value_of(initLog[1]).should_be(""); + value_of(updateLog[1]).should_be("hello"); + + observable("goodbye"); + value_of(initLog.length).should_be(2); // init event count should be unchanged + value_of(updateLog.length).should_be(3); + value_of(updateLog[2]).should_be("goodbye"); + }, + 'Should be able to refer to the bound object itself (at the root scope, the viewmodel) via $data': function() { testNode.innerHTML = "
"; ko.applyBindings({ someProp: 'My prop value' }, testNode); diff --git a/spec/defaultBindingsBehaviors.js b/spec/defaultBindingsBehaviors.js index cf618e8f5..7e2d81e1f 100755 --- a/spec/defaultBindingsBehaviors.js +++ b/spec/defaultBindingsBehaviors.js @@ -599,6 +599,18 @@ describe('Binding: Event', { ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "custom"); value_of(model.innerWasCalled).should_be(true); value_of(model.outerWasCalled).should_be(true); + }, + + 'Should allow custom events (without bubbling)': function () { + var model = { + innerWasCalled: false, innerDoCall: function () { this.innerWasCalled = true; }, + outerWasCalled: false, outerDoCall: function () { this.outerWasCalled = true; } + }; + testNode.innerHTML = "
"; + ko.applyBindings(model, testNode); + ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "custom", {bubbles: false}); + value_of(model.innerWasCalled).should_be(true); + value_of(model.outerWasCalled).should_be(false); } }); @@ -1360,20 +1372,14 @@ describe('Binding: Foreach', { value_of(testNode.childNodes[0]).should_contain_html('first childadded child'); }, - 'Should be able to handle afterAdd and beforeRemove events': function() { - testNode.innerHTML = "
"; + 'Should be able to handle afterAdd and beforeRemove events (as bindings on child element)': function() { + testNode.innerHTML = "
"; var someItems = ko.observableArray([{ childprop: 'first child' }]); var afterAddCallbackData = [], beforeRemoveCallbackData = []; ko.applyBindings({ someItems: someItems, - myAfterAdd: function(data, event) { - afterAddCallbackData.push({ elem: event.target, index: event.ko_index, - value: event.ko_data, currentParentClone: event.target.parentNode.cloneNode(true) }); - }, - myBeforeRemove: function(data, event) { - beforeRemoveCallbackData.push({ elem: event.target, index: event.ko_index, - value: event.ko_data, currentParentClone: event.target.parentNode.cloneNode(true) }); - } + myAfterAdd: function(elem, index, value) { afterAddCallbackData.push({ elem: elem, index: index, value: value, currentParentClone: elem.parentNode.cloneNode(true) }) }, + myBeforeRemove: function(elem, index, value) { beforeRemoveCallbackData.push({ elem: elem, index: index, value: value, currentParentClone: elem.parentNode.cloneNode(true) }) } }, testNode); value_of(testNode.childNodes[0]).should_contain_text('first child'); diff --git a/src/binding/bindingAttributeSyntax.js b/src/binding/bindingAttributeSyntax.js index f2c8e5661..247660ad1 100755 --- a/src/binding/bindingAttributeSyntax.js +++ b/src/binding/bindingAttributeSyntax.js @@ -112,6 +112,7 @@ } } } + ko.utils.triggerEvent(node, "koInit"); initPhase = 2; } @@ -124,6 +125,7 @@ handlerUpdateFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContextInstance); } } + ko.utils.triggerEvent(node, "koUpdate"); } } }, diff --git a/src/binding/defaultBindings.js b/src/binding/defaultBindings.js index 9e9337ad0..392e90f4e 100755 --- a/src/binding/defaultBindings.js +++ b/src/binding/defaultBindings.js @@ -480,7 +480,26 @@ ko.bindingHandlers['afterRender'] = { return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindingsAccessor, viewModel); } }; - + +var templateForeachEvents = [{event:'koAfterAdd', binding:'afterAdd'}, {event:'koBeforeRemove', binding:'beforeRemove'}]; +ko.utils.arrayForEach(templateForeachEvents, function(e) { + ko.bindingHandlers[e.binding] = { + 'init': function(element, valueAccessor, allBindingsAccessor, viewModel) { + var newValueAccessor = function () { + var result = {}; + result[e.event] = function(data, event) { + var handlerFunction = valueAccessor(); + if (!handlerFunction) + return; + return handlerFunction.call(this, event.target, event.ko_index, event.ko_data); + }; + return result; + }; + return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindingsAccessor, viewModel); + } + }; +}); + // "with: someExpression" is equivalent to "template: { if: someExpression, data: someExpression }" ko.bindingHandlers['with'] = { makeTemplateValueAccessor: function(valueAccessor) { diff --git a/src/binding/editDetection/arrayToDomNodeChildren.js b/src/binding/editDetection/arrayToDomNodeChildren.js index 21f258d37..9a4be252a 100644 --- a/src/binding/editDetection/arrayToDomNodeChildren.js +++ b/src/binding/editDetection/arrayToDomNodeChildren.js @@ -142,11 +142,11 @@ // call afterAdd and beforeRemove custom events for (var i = 0; i < nodesAdded.length; i++) { if (nodesAdded[i].element.nodeType == 1) - ko.utils.triggerEvent(nodesAdded[i].element, "koAfterAdd", {'ko_index': nodesAdded[i].index, 'ko_data': nodesAdded[i].value}); + ko.utils.triggerEvent(nodesAdded[i].element, "koAfterAdd", {bubbles: false, 'ko_index': nodesAdded[i].index, 'ko_data': nodesAdded[i].value}); } for (var i = 0; i < nodesToDelete.length; i++) { if (nodesToDelete[i].element.nodeType == 1) { - if (ko.utils.triggerEvent(nodesToDelete[i].element, "koBeforeRemove", {'ko_index': nodesToDelete[i].index, 'ko_data': nodesToDelete[i].value}) === false) { + if (ko.utils.triggerEvent(nodesToDelete[i].element, "koBeforeRemove", {bubbles: false, 'ko_index': nodesToDelete[i].index, 'ko_data': nodesToDelete[i].value}) === false) { nodesToDelete[i].removedByCallback = true; } } diff --git a/src/templating/templating.js b/src/templating/templating.js index 4e2921f82..470728599 100644 --- a/src/templating/templating.js +++ b/src/templating/templating.js @@ -74,7 +74,7 @@ options['afterRender'](renderedNodesArray, bindingContext['$data']); for (var i = 0; i < renderedNodesArray.length; i++) { if (renderedNodesArray[i].nodeType == 1) - ko.utils.triggerEvent(renderedNodesArray[i], "koAfterRender", {'ko_data': bindingContext['$data']}); + ko.utils.triggerEvent(renderedNodesArray[i], "koAfterRender", {bubbles: false, 'ko_data': bindingContext['$data']}); } } @@ -133,7 +133,7 @@ options['afterRender'](addedNodesArray, bindingContext['$data']); for (var i = 0; i < addedNodesArray.length; i++) { if (addedNodesArray[i].nodeType == 1) - ko.utils.triggerEvent(addedNodesArray[i], "koAfterRender", {'ko_data': bindingContext['$data']}); + ko.utils.triggerEvent(addedNodesArray[i], "koAfterRender", {bubbles: false, 'ko_data': bindingContext['$data']}); } }; diff --git a/src/utils.js b/src/utils.js index 17197b5da..0aedc862b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -245,6 +245,7 @@ ko.utils = new (function () { triggerEvent: function (element, eventType, data) { if (!(element && element.nodeType)) throw new Error("element must be a DOM node when calling triggerEvent"); + var bubbles = data && 'bubbles' in data ? data.bubbles : true; if (typeof jQuery != "undefined") { var eventData = []; @@ -252,14 +253,22 @@ ko.utils = new (function () { // Work around the jQuery "click events on checkboxes" issue described above by storing the original checked state before triggering the handler eventData.push({ checkedStateBeforeEvent: element.checked }); } + var e = jQuery(element); + // Set a one-time handler that stop the bubbling (since we can't tell jQuery not to bubble the event) + if (!bubbles) { + e['bind'](eventType, function(event) { + event.stopPropagation(); + e['unbind'](eventType, arguments.callee); + }); + } var event = new jQuery.Event(eventType, data); - jQuery(element)['trigger'](event, eventData); + e['trigger'](event, eventData); return !event.isDefaultPrevented(); } else if (typeof document.createEvent == "function") { if (typeof element.dispatchEvent == "function") { var eventCategory = knownEventTypesByEventName[eventType] || "HTMLEvents"; var event = document.createEvent(eventCategory); - event.initEvent(eventType, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element); + event.initEvent(eventType, bubbles, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element); ko.utils.extend(event, data); return element.dispatchEvent(event); } @@ -276,6 +285,15 @@ ko.utils = new (function () { return element.fireEvent("on" + eventType); } else { // IE doesn't support custom events, so we'll highjack a standard event (keypress) + // Set a one-time handler that stop the bubbling (since we can't tell IE not to bubble the event) + if (!bubbles) { + element.attachEvent("onkeypress", function(event) { + if (event.data == eventType) { + event.cancelBubble = true; + element.detachEvent("onkeypress", arguments.callee); + } + }); + } var event = document.createEventObject(); event.data = eventType; event.recordset = data ? data : {};