From 5c14a1e22ddfbca013efea408869933b7bf4c82d Mon Sep 17 00:00:00 2001 From: Brian Hunt Date: Sun, 20 Dec 2015 13:32:32 -0500 Subject: [PATCH] #1) Capture binding errors with `onBindingError` --- spec/bindingAttributeBehaviors.js | 116 +++++++++++++++++++++++++- spec/helpers/setup.js | 1 - src/binding/bindingAttributeSyntax.js | 78 +++++++++++++---- 3 files changed, 177 insertions(+), 18 deletions(-) diff --git a/spec/bindingAttributeBehaviors.js b/spec/bindingAttributeBehaviors.js index 767f7640..8dc5f5a1 100644 --- a/spec/bindingAttributeBehaviors.js +++ b/spec/bindingAttributeBehaviors.js @@ -1,3 +1,4 @@ +/* eslint semi: 0 */ describe('Binding attribute syntax', function() { beforeEach(jasmine.prepareTestNode); @@ -93,9 +94,122 @@ describe('Binding attribute syntax', function() { testNode.innerHTML = "
"; 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 = "
"; + 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 = "
"; + 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 = "
"; + 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 = "
"; + 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 = { diff --git a/spec/helpers/setup.js b/spec/helpers/setup.js index f678ac0b..62ced56c 100644 --- a/spec/helpers/setup.js +++ b/spec/helpers/setup.js @@ -11,7 +11,6 @@ window.jQueryInstance = window.jQuery; jasmine.updateInterval = 500; - /* Some helper functions for jasmine on the browser */ diff --git a/src/binding/bindingAttributeSyntax.js b/src/binding/bindingAttributeSyntax.js index 9748b7a9..3fa018cb 100755 --- a/src/binding/bindingAttributeSyntax.js +++ b/src/binding/bindingAttributeSyntax.js @@ -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. @@ -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) { @@ -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); } @@ -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") { @@ -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 + } + ); } }); } @@ -411,7 +435,7 @@ } else { return ko.utils.domData.get(node, storedBindingContextDomDataKey); } - } + }; function getBindingContext(viewModelOrBindingContext) { return viewModelOrBindingContext && (viewModelOrBindingContext instanceof ko.bindingContext) @@ -466,6 +490,27 @@ 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); @@ -473,4 +518,5 @@ ko.exportSymbol('applyBindingsToNode', ko.applyBindingsToNode); ko.exportSymbol('contextFor', ko.contextFor); ko.exportSymbol('dataFor', ko.dataFor); + ko.exportSymbol('onBindingError', ko.onBindingError); })();