From 798905d51c6b45dd8ede33fe599ca0486b1ecec7 Mon Sep 17 00:00:00 2001 From: James Davies Date: Mon, 3 Jun 2013 13:04:12 +1000 Subject: [PATCH] fix($parse) - Allow setterFn to bind correctly to promise results When binding the result of a promise directly to an ng:model directive, getterFn successfully extracts the data from the promise by traversing the '$$v' attribute. However the setterFn was not successfully traversing to the $$v attribute, and instead writing model updates directly on the promise itself. The subsequent apply/digest phase of Angular was then pulling the original value from the promise and applying it back into the viewState, thus simulating a read-only field. In summary, setterFn was writing to the root of the promise, while getterFn was reading from the '$$v' component. Fixed by modifying setterFn to unwrap promises if found and read $$v. If promises have not been resolved yet, bindings will function as they previously did (act as a read-only field). This appears to be more of a side effect than intentional design in the existing codebase, however I kept this behaviour in this patch to minimize the chance of breaking existing projects. Closes #1827 Also see: http://stackoverflow.com/questions/16883239/angularjs-ngmodel- field-is-readonly-when-bound-to-q-promise/16883387 --- src/ng/parse.js | 16 +++++++++++++++- test/ng/parseSpec.js | 45 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 5af52f600d6f..681460f0fa56 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -705,6 +705,20 @@ function parser(text, json, $filter, csp){ // Parser helper functions ////////////////////////////////////////////////// +// Unwrap values from a promise +function unwrapPromise(obj){ + + // If we'v been passed a promise + if( obj.then ){ + + // Then unwrap the value. If the value has not been returned yet, bind to a throwaway object. This will simulate a read-only field. + return obj.$$v || {} + } + + return obj; +} + + function setter(obj, path, setValue) { var element = path.split('.'); for (var i = 0; element.length > 1; i++) { @@ -714,7 +728,7 @@ function setter(obj, path, setValue) { propertyObj = {}; obj[key] = propertyObj; } - obj = propertyObj; + obj = unwrapPromise( propertyObj ); } obj[element.shift()] = setValue; return setValue; diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index c3cb0ce11f4a..ec133289d2b1 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -679,6 +679,51 @@ describe('parser', function() { expect(scope.$eval('greeting')).toBe('hello!'); }); + it('should evaluate a resolved object promise and set its value', inject(function($parse) { + var person = {'name': 'Bill Gates'}; + + deferred.resolve(person); + scope.person = promise; + + var getter = $parse( 'person.name' ); + + expect(getter(scope)).toBe(undefined); + scope.$digest(); + expect(getter(scope)).toBe('Bill Gates'); + + getter.assign(scope, 'Warren Buffet'); + expect(getter(scope)).toBe('Warren Buffet'); + })); + + it('should evaluate a resolved primitive type promise and set its value', inject(function($parse) { + deferred.resolve("Salut!"); + scope.greeting = promise; + + var getter = $parse( 'greeting' ); + + expect(getter(scope)).toBe(undefined); + scope.$digest(); + expect(getter(scope)).toBe('Salut!'); + + getter.assign(scope, 'Bonjour'); + expect(getter(scope)).toBe('Bonjour'); + })); + + it('should evaluate an unresolved promise and *not* set its value', inject(function($parse) { + + + scope.person = promise; + + var getter = $parse( 'person.name' ); + + expect(getter(scope)).toBe(undefined); + scope.$digest(); + expect(getter(scope)).toBe(undefined); + + getter.assign(scope, 'Warren Buffet'); + expect(getter(scope)).toBe(undefined); + })); + it('should evaluated rejected promise and ignore the rejection reason', function() { deferred.reject('sorry');