Skip to content

Commit

Permalink
#1) Capture binding errors with onBindingError
Browse files Browse the repository at this point in the history
  • Loading branch information
brianmhunt committed Dec 20, 2015
1 parent aeb82a3 commit 5c14a1e
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 18 deletions.
116 changes: 115 additions & 1 deletion spec/bindingAttributeBehaviors.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint semi: 0 */
describe('Binding attribute syntax', function() {
beforeEach(jasmine.prepareTestNode);

Expand Down Expand Up @@ -93,9 +94,122 @@ describe('Binding attribute syntax', function() {
testNode.innerHTML = "<div data-bind='test: nonexistentValue'></div>";
expect(function () {
ko.applyBindings(null, testNode);
}).toThrowContaining("Unable to process binding \"test: function");
}).toThrowContaining("nonexistentValue");
});

it("Should call ko.onBindingError with relevant details of a bindingHandler init error", function () {
var saved_obe = ko.onBindingError,
obe_calls = 0;
this.after(function () {
ko.onBindingError = saved_obe;
})
ko.onBindingError = function (spec) {
obe_calls++;
expect(spec.during).toEqual('init');
expect(spec.errorCaptured.message).toEqual('A moth!');
expect(spec.bindingKey).toEqual('test')
expect(spec.valueAccessor()).toEqual(64728)
expect(spec.element).toEqual(testNode.children[0])
expect(spec.bindings.test()).toEqual(64728)
expect(spec.bindingContext.$data).toEqual('0xe')
expect(spec.allBindings().test).toEqual(64728)
}
ko.bindingHandlers.test = {
init: function () { throw new Error("A moth!") }
}
testNode.innerHTML = "<div data-bind='test: 64728'></div>";
ko.applyBindings('0xe', testNode);
expect(obe_calls).toEqual(1);
});

it("Should call ko.onBindingError with relevant details of a bindingHandler update error", function () {
var saved_obe = ko.onBindingError,
obe_calls = 0;
this.after(function () {
ko.onBindingError = saved_obe;
})
ko.onBindingError = function (spec) {
obe_calls++;
expect(spec.during).toEqual('update');
expect(spec.errorCaptured.message).toEqual('A beetle!');
expect(spec.bindingKey).toEqual('test')
expect(spec.valueAccessor()).toEqual(64729)
expect(spec.element).toEqual(testNode.children[0])
expect(spec.bindings.test()).toEqual(64729)
expect(spec.bindingContext.$data).toEqual('0xf')
expect(spec.allBindings().test).toEqual(64729)
}
ko.bindingHandlers.test = {
update: function () { throw new Error("A beetle!") }
}
testNode.innerHTML = "<div data-bind='test: 64729'></div>";
ko.applyBindings('0xf', testNode);
expect(obe_calls).toEqual(1);
});

it("Should call ko.onBindingError with relevant details when an update fails", function () {
var saved_obe = ko.onBindingError,
obe_calls = 0,
observable = ko.observable();
this.after(function () {
ko.onBindingError = saved_obe;
});

ko.onBindingError = function (spec) {
obe_calls++;
expect(spec.during).toEqual('update');
expect(spec.errorCaptured.message).toEqual('Observable: 42');
expect(spec.bindingKey).toEqual('test');
expect(spec.valueAccessor()).toEqual(64725);
expect(spec.element).toEqual(testNode.children[0]);
expect(spec.bindings.test()).toEqual(64725);
expect(spec.bindingContext.$data).toEqual('0xef');
expect(spec.allBindings().test).toEqual(64725);
};

ko.bindingHandlers.test = {
update: function () {
if (observable() === 42) {
throw new Error("Observable: " + observable());
}
}
};
testNode.innerHTML = "<div data-bind='test: 64725'></div>";
ko.applyBindings('0xef', testNode);
expect(obe_calls).toEqual(0);
try { observable(42); } catch (e) {}
expect(obe_calls).toEqual(1);
observable(24);
expect(obe_calls).toEqual(1);
try { observable(42); } catch (e) {}
expect(obe_calls).toEqual(2);
});

it("Calls ko.onError, if it is defined", function () {
var oe_calls = 0
var oxy = ko.observable()
this.after(function () { ko.onError = undefined })
ko.onError = function (err) {
expect(err.message.indexOf('turtle')).toNotEqual(-1)
// Check for the `spec` properties
expect(err.bindingKey).toEqual('test')
oe_calls++
}
ko.bindingHandlers.test = {
init: function () { throw new Error("A turtle!") },
update: function (e, oxy) {
ko.unwrap(oxy()); // Create dependency.
throw new Error("Two turtles!")
}
}
testNode.innerHTML = "<div data-bind='test: oxy'></div>";
ko.applyBindings({oxy: oxy}, testNode);
expect(oe_calls).toEqual(2)
oxy(1234)
expect(oe_calls).toEqual(3)
})


it('Should invoke registered handlers\'s init() then update() methods passing binding data', function () {
var methodsInvoked = [];
ko.bindingHandlers.test = {
Expand Down
1 change: 0 additions & 1 deletion spec/helpers/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ window.jQueryInstance = window.jQuery;

jasmine.updateInterval = 500;


/*
Some helper functions for jasmine on the browser
*/
Expand Down
78 changes: 62 additions & 16 deletions src/binding/bindingAttributeSyntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
};
}
}
}
};

// Extend the binding context hierarchy with a new view model object. If the parent context is watching
// any observables, the new child context will automatically get a dependency on the parent context.
Expand Down Expand Up @@ -191,7 +191,7 @@
function validateThatBindingIsAllowedForVirtualElements(bindingName) {
var validator = ko.virtualElements.allowedBindings[bindingName];
if (!validator)
throw new Error("The binding '" + bindingName + "' cannot be used with virtual elements")
throw new Error("The binding '" + bindingName + "' cannot be used with virtual elements");
}

function applyBindingsToDescendantsInternal (bindingContext, elementOrVirtualElement, bindingContextsMayDifferFromDomParentElement) {
Expand Down Expand Up @@ -288,7 +288,13 @@
var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
if (!sourceBindings) {
if (alreadyBound) {
throw Error("You cannot apply bindings multiple times to the same element.");
ko.onBindingError({
during: 'apply',
errorCaptured: new Error("You cannot apply bindings multiple times to the same element."),
element: node,
bindingContext: bindingContext
});
return false;
}
ko.utils.domData.set(node, boundElementDomDataKey, true);
}
Expand Down Expand Up @@ -365,6 +371,19 @@
validateThatBindingIsAllowedForVirtualElements(bindingKey);
}

function reportBindingError(during, errorCaptured) {
ko.onBindingError({
during: during,
errorCaptured: errorCaptured,
element: node,
bindingKey: bindingKey,
bindings: bindings,
allBindings: allBindings,
valueAccessor: getValueAccessor(bindingKey),
bindingContext: bindingContext
});
}

try {
// Run init, ignoring any dependencies
if (typeof handlerInitFn == "function") {
Expand All @@ -379,20 +398,25 @@
}
});
}
} catch (ex) {
reportBindingError('init', ex);
}

// Run update in its own computed wrapper
if (typeof handlerUpdateFn == "function") {
ko.dependentObservable(
function() {
// Run update in its own computed wrapper
if (typeof handlerUpdateFn == "function") {
ko.computed(
function updatedValueAccessor() {
try {
handlerUpdateFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);
},
null,
{ disposeWhenNodeIsRemoved: node }
);
}
} catch (ex) {
ex.message = "Unable to process binding \"" + bindingKey + ": " + bindings[bindingKey] + "\"\nMessage: " + ex.message;
throw ex;
} catch (ex) {
reportBindingError('update', ex);
}

},
null, {
disposeWhenNodeIsRemoved: node
}
);
}
});
}
Expand All @@ -411,7 +435,7 @@
} else {
return ko.utils.domData.get(node, storedBindingContextDomDataKey);
}
}
};

function getBindingContext(viewModelOrBindingContext) {
return viewModelOrBindingContext && (viewModelOrBindingContext instanceof ko.bindingContext)
Expand Down Expand Up @@ -466,11 +490,33 @@
return context ? context['$data'] : undefined;
};

ko.onBindingError = function onBindingError(spec) {
var error, bindingText;
if (spec.bindingKey) {
// During: 'init' or initial 'update'
error = spec.errorCaptured;
bindingText = ko.bindingProvider['instance']['getBindingsString'](spec.element);
error.message = "Unable to process binding \"" + spec.bindingKey
+ "\" in binding \"" + bindingText
+ "\"\nMessage: " + error.message;
} else {
// During: 'apply'
error = spec.errorCaptured;
}
ko.utils.extend(error, spec);
if (typeof ko.onError === 'function') {
ko.onError(error);
} else {
throw error;
}
};

ko.exportSymbol('bindingHandlers', ko.bindingHandlers);
ko.exportSymbol('applyBindings', ko.applyBindings);
ko.exportSymbol('applyBindingsToDescendants', ko.applyBindingsToDescendants);
ko.exportSymbol('applyBindingAccessorsToNode', ko.applyBindingAccessorsToNode);
ko.exportSymbol('applyBindingsToNode', ko.applyBindingsToNode);
ko.exportSymbol('contextFor', ko.contextFor);
ko.exportSymbol('dataFor', ko.dataFor);
ko.exportSymbol('onBindingError', ko.onBindingError);
})();

0 comments on commit 5c14a1e

Please sign in to comment.