Skip to content

Commit

Permalink
#7 and #3 make template related events not bubble; add events for ini…
Browse files Browse the repository at this point in the history
…t and update (that bubble)
  • Loading branch information
mbest committed Dec 20, 2011
1 parent d0edd9d commit 0d4cda5
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 17 deletions.
24 changes: 24 additions & 0 deletions spec/bindingAttributeBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<div data-bind='event: {koInit: myInit, koUpdate: myUpdate}'><div data-bind='text: myObservable'></div></div>";
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 = "<div data-bind='text: $data.someProp'></div>";
ko.applyBindings({ someProp: 'My prop value' }, testNode);
Expand Down
26 changes: 16 additions & 10 deletions spec/defaultBindingsBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<div data-bind='event:{custom:outerDoCall}'><button data-bind='event:{custom:innerDoCall}'>hey</button></div>";
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);
}
});

Expand Down Expand Up @@ -1360,20 +1372,14 @@ describe('Binding: Foreach', {
value_of(testNode.childNodes[0]).should_contain_html('<span data-bind="text: childprop">first child</span><span data-bind="text: childprop">added child</span>');
},

'Should be able to handle afterAdd and beforeRemove events': function() {
testNode.innerHTML = "<div data-bind='foreach: someItems'><span data-bind='text: childprop, event: {koAfterAdd: $parent.myAfterAdd, koBeforeRemove: $parent.myBeforeRemove}'></span></div>";
'Should be able to handle afterAdd and beforeRemove events (as bindings on child element)': function() {
testNode.innerHTML = "<div data-bind='foreach: someItems'><span data-bind='text: childprop, afterAdd: $parent.myAfterAdd, beforeRemove: $parent.myBeforeRemove'></span></div>";
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');
Expand Down
2 changes: 2 additions & 0 deletions src/binding/bindingAttributeSyntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
}
}
}
ko.utils.triggerEvent(node, "koInit");
initPhase = 2;
}

Expand All @@ -124,6 +125,7 @@
handlerUpdateFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContextInstance);
}
}
ko.utils.triggerEvent(node, "koUpdate");
}
}
},
Expand Down
21 changes: 20 additions & 1 deletion src/binding/defaultBindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions src/binding/editDetection/arrayToDomNodeChildren.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/templating/templating.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']});
}
}

Expand Down Expand Up @@ -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']});
}
};

Expand Down
22 changes: 20 additions & 2 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,21 +245,30 @@ 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 = [];
if (isClickOnCheckableElement(element, eventType)) {
// 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);
}
Expand All @@ -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 : {};
Expand Down

0 comments on commit 0d4cda5

Please sign in to comment.