From 78b6e8a446c0e38075c14b724f3cdf345c01fa06 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Wed, 23 Nov 2011 15:53:06 -0800 Subject: [PATCH] feat($parse): add support for transparent evaluation of Promises Parser now builds expressions that can detect promises and transparently evaluate them to undefined or the promise value. If promiseA is resolved with value 'A', then {{promiseA}} evals to 'A'; If promiseA is unresolved, then {{promiseA}} evals to undefined; Following invocations are supported: - {{promise}} - {{promise.futureProp}} - {{[promise][0]}} - {{object.promise}} - {{object[promise]}} - {{array[promise]}} - {{fn(promise)}} - combinations of the above --- src/service/parse.js | 29 +++++-- test/service/parseSpec.js | 165 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 5 deletions(-) diff --git a/src/service/parse.js b/src/service/parse.js index 432929b5499b..3a6c41065e37 100644 --- a/src/service/parse.js +++ b/src/service/parse.js @@ -522,9 +522,21 @@ function parser(text, json, $filter){ consume(']'); return extend( function(self){ - var o = obj(self); - var i = indexFn(self); - return (o) ? o[i] : undefined; + var o = obj(self), + i = indexFn(self), + v, p; + + if (!o) return undefined; + v = o[i]; + if (v && v.then) { + p = v; + if (!('$$v' in v)) { + p.$$v = undefined; + p.then(function(val) { p.$$v = val; }); + } + v = v.$$v; + } + return v; }, { assign:function(self, value){ return obj(self)[indexFn(self)] = value; @@ -673,7 +685,7 @@ function getterFn(path) { var fn = getterFnCache[path]; if (fn) return fn; - var code = 'var l, fn, t;\n'; + var code = 'var l, fn, p;\n'; forEach(path.split('.'), function(key) { key = (JS_KEYWORDS[key]) ? '["' + key + '"]' : '.' + key; code += 'if(!s) return s;\n' + @@ -683,11 +695,18 @@ function getterFn(path) { ' fn=function(){ return l' + key + '.apply(l, arguments); };\n' + ' fn.$unboundFn=s;\n' + ' s=fn;\n' + + '} else if (s && s.then) {\n' + + ' if (!("$$v" in s)) {\n' + + ' p=s;\n' + + ' p.$$v = undefined;\n' + + ' p.then(function(v) {p.$$v=v;});\n' + + '}\n' + + ' s=s.$$v\n' + '}\n'; }); code += 'return s;'; fn = Function('s', code); - fn["toString"] = function() { return code; }; + fn.toString = function() { return code; }; return getterFnCache[path] = fn; } diff --git a/test/service/parseSpec.js b/test/service/parseSpec.js index 2742f3810a22..85dab72ca0a6 100644 --- a/test/service/parseSpec.js +++ b/test/service/parseSpec.js @@ -407,6 +407,171 @@ describe('parser', function() { }); + describe('promises', function() { + var deferred, promise, q; + + beforeEach(inject(function($q) { + q = $q; + deferred = q.defer(); + promise = deferred.promise; + })); + + describe('{{promise}}', function() { + it('should evaluated resolved promise and get its value', function() { + deferred.resolve('hello!'); + scope.greeting = promise; + expect(scope.$eval('greeting')).toBe(undefined); + scope.$digest(); + expect(scope.$eval('greeting')).toBe('hello!'); + }); + + + it('should evaluated rejected promise and ignore the rejection reason', function() { + deferred.reject('sorry'); + scope.greeting = promise; + expect(scope.$eval('gretting')).toBe(undefined); + scope.$digest(); + expect(scope.$eval('greeting')).toBe(undefined); + }); + + + it('should evaluate a promise and eventualy get its value', function() { + scope.greeting = promise; + expect(scope.$eval('greeting')).toBe(undefined); + + scope.$digest(); + expect(scope.$eval('greeting')).toBe(undefined); + + deferred.resolve('hello!'); + expect(scope.$eval('greeting')).toBe(undefined); + scope.$digest(); + expect(scope.$eval('greeting')).toBe('hello!'); + }); + + + it('should evaluate a promise and eventualy ignore its rejection', function() { + scope.greeting = promise; + expect(scope.$eval('greeting')).toBe(undefined); + + scope.$digest(); + expect(scope.$eval('greeting')).toBe(undefined); + + deferred.reject('sorry'); + expect(scope.$eval('greeting')).toBe(undefined); + scope.$digest(); + expect(scope.$eval('greeting')).toBe(undefined); + }); + }); + + describe('dereferencing', function() { + it('should evaluate and dereference properties leading to and from a promise', function() { + scope.obj = {greeting: promise}; + expect(scope.$eval('obj.greeting')).toBe(undefined); + expect(scope.$eval('obj.greeting.polite')).toBe(undefined); + + scope.$digest(); + expect(scope.$eval('obj.greeting')).toBe(undefined); + expect(scope.$eval('obj.greeting.polite')).toBe(undefined); + + deferred.resolve({polite: 'Good morning!'}); + scope.$digest(); + expect(scope.$eval('obj.greeting')).toEqual({polite: 'Good morning!'}); + expect(scope.$eval('obj.greeting.polite')).toBe('Good morning!'); + }); + + it('should evaluate and dereference properties leading to and from a promise via bracket ' + + 'notation', function() { + scope.obj = {greeting: promise}; + expect(scope.$eval('obj["greeting"]')).toBe(undefined); + expect(scope.$eval('obj["greeting"]["polite"]')).toBe(undefined); + + scope.$digest(); + expect(scope.$eval('obj["greeting"]')).toBe(undefined); + expect(scope.$eval('obj["greeting"]["polite"]')).toBe(undefined); + + deferred.resolve({polite: 'Good morning!'}); + scope.$digest(); + expect(scope.$eval('obj["greeting"]')).toEqual({polite: 'Good morning!'}); + expect(scope.$eval('obj["greeting"]["polite"]')).toBe('Good morning!'); + }); + + + it('should evaluate and dereference array references leading to and from a promise', + function() { + scope.greetings = [promise]; + expect(scope.$eval('greetings[0]')).toBe(undefined); + expect(scope.$eval('greetings[0][0]')).toBe(undefined); + + scope.$digest(); + expect(scope.$eval('greetings[0]')).toBe(undefined); + expect(scope.$eval('greetings[0][0]')).toBe(undefined); + + deferred.resolve(['Hi!', 'Cau!']); + scope.$digest(); + expect(scope.$eval('greetings[0]')).toEqual(['Hi!', 'Cau!']); + expect(scope.$eval('greetings[0][0]')).toBe('Hi!'); + }); + + + it('should evaluate and dereference promises used as function arguments', function() { + scope.greet = function(name) { return 'Hi ' + name + '!'; }; + scope.name = promise; + expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); + + scope.$digest(); + expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); + + deferred.resolve('Veronica'); + expect(scope.$eval('greet(name)')).toBe('Hi undefined!'); + + scope.$digest(); + expect(scope.$eval('greet(name)')).toBe('Hi Veronica!'); + }); + + + it('should evaluate and dereference promises used as array indexes', function() { + scope.childIndex = promise; + scope.kids = ['Adam', 'Veronica', 'Elisa']; + expect(scope.$eval('kids[childIndex]')).toBe(undefined); + + scope.$digest(); + expect(scope.$eval('kids[childIndex]')).toBe(undefined); + + deferred.resolve(1); + expect(scope.$eval('kids[childIndex]')).toBe(undefined); + + scope.$digest(); + expect(scope.$eval('kids[childIndex]')).toBe('Veronica'); + }); + + + it('should evaluate and dereference promises used as keys in bracket notation', function() { + scope.childKey = promise; + scope.kids = {'a': 'Adam', 'v': 'Veronica', 'e': 'Elisa'}; + + expect(scope.$eval('kids[childKey]')).toBe(undefined); + + scope.$digest(); + expect(scope.$eval('kids[childKey]')).toBe(undefined); + + deferred.resolve('v'); + expect(scope.$eval('kids[childKey]')).toBe(undefined); + + scope.$digest(); + expect(scope.$eval('kids[childKey]')).toBe('Veronica'); + }); + + + it('should not mess with the promise if it was not directly evaluated', function() { + scope.obj = {greeting: promise, username: 'hi'}; + var obj = scope.$eval('obj'); + expect(obj.username).toEqual('hi'); + expect(typeof obj.greeting.then).toBe('function'); + }); + }); + }); + + describe('assignable', function() { it('should expose assignment function', inject(function($parse) { var fn = $parse('a');