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 : {};