From c07c4b8a2f5034ba12f3a0ad07703fbb047c93e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Ram=C3=B3n=20L=C3=B3pez?= Date: Sat, 26 Jan 2013 20:15:06 +0100 Subject: [PATCH] feat(compile): '=' binding can now be optional If you bind using '=' to a non-existant parent property, the compiler will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception, which is right because the model doesn't exist. This enhancement allow to specify that a binding is optional so it won't complain if the parent property is not defined. In order to mantain backward compability, the new behaviour must be specified using '=?' instead of '='. The local property will be undefined is these cases. Closes #909 Closes #1435 --- docs/content/guide/directive.ngdoc | 4 +- src/ng/compile.js | 8 ++- test/ng/compileSpec.js | 104 +++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/docs/content/guide/directive.ngdoc b/docs/content/guide/directive.ngdoc index 8ef5e08fdf07..bc4fb6783eb7 100644 --- a/docs/content/guide/directive.ngdoc +++ b/docs/content/guide/directive.ngdoc @@ -336,7 +336,9 @@ compiler}. The attributes are: Given `` and widget definition of `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected - in `localModel` and any changes in `localModel` will reflect in `parentModel`. + in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent + scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You + can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. If no `attr` name is specified then the attribute name is assumed to be the same as the diff --git a/src/ng/compile.js b/src/ng/compile.js index 84d53e6d4154..dfdf7dfcd77a 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -714,13 +714,14 @@ function $CompileProvider($provide) { $element = attrs.$$element; if (newIsolateScopeDirective) { - var LOCAL_REGEXP = /^\s*([@=&])\s*(\w*)\s*$/; + var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; var parentScope = scope.$parent || scope; forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) { var match = definiton.match(LOCAL_REGEXP) || [], - attrName = match[2]|| scopeName, + attrName = match[3] || scopeName, + optional = (match[2] == '?'), mode = match[1], // @, =, or & lastValue, parentGet, parentSet; @@ -736,6 +737,9 @@ function $CompileProvider($provide) { } case '=': { + if (optional && (attrs[attrName] == null)) { + return; + } parentGet = $parse(attrs[attrName]); parentSet = parentGet.assign || function() { // reset the change, or we will throw this exception on every $digest diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 363b4329af83..dddc89633d5a 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -1775,6 +1775,9 @@ describe('$compile', function() { ref: '=', refAlias: '= ref', reference: '=', + optref: '=?', + optrefAlias: '=? optref', + optreference: '=?', expr: '&', exprAlias: '&expr' }, @@ -1917,6 +1920,107 @@ describe('$compile', function() { }); + describe('optional object reference', function() { + it('should update local when origin changes', inject(function() { + compile('
'); + expect(componentScope.optRef).toBe(undefined); + expect(componentScope.optRefAlias).toBe(componentScope.optRef); + + $rootScope.name = 'misko'; + $rootScope.$apply(); + expect(componentScope.optref).toBe($rootScope.name); + expect(componentScope.optrefAlias).toBe($rootScope.name); + + $rootScope.name = {}; + $rootScope.$apply(); + expect(componentScope.optref).toBe($rootScope.name); + expect(componentScope.optrefAlias).toBe($rootScope.name); + })); + + + it('should update local when origin changes', inject(function() { + compile('
'); + expect(componentScope.optref).toBe(undefined); + expect(componentScope.optrefAlias).toBe(componentScope.optref); + + componentScope.optref = 'misko'; + $rootScope.$apply(); + expect($rootScope.name).toBe('misko'); + expect(componentScope.optref).toBe('misko'); + expect($rootScope.name).toBe(componentScope.optref); + expect(componentScope.optrefAlias).toBe(componentScope.optref); + + componentScope.name = {}; + $rootScope.$apply(); + expect($rootScope.name).toBe(componentScope.optref); + expect(componentScope.optrefAlias).toBe(componentScope.optref); + })); + + + it('should update local when both change', inject(function() { + compile('
'); + $rootScope.name = {mark:123}; + componentScope.optref = 'misko'; + + $rootScope.$apply(); + expect($rootScope.name).toEqual({mark:123}) + expect(componentScope.optref).toBe($rootScope.name); + expect(componentScope.optrefAlias).toBe($rootScope.name); + + $rootScope.name = 'igor'; + componentScope.optref = {}; + $rootScope.$apply(); + expect($rootScope.name).toEqual('igor') + expect(componentScope.optref).toBe($rootScope.name); + expect(componentScope.optrefAlias).toBe($rootScope.name); + })); + + it('should complain on non assignable changes', inject(function() { + compile('
'); + $rootScope.name = 'world'; + $rootScope.$apply(); + expect(componentScope.optref).toBe('hello world'); + + componentScope.optref = 'ignore me'; + expect($rootScope.$apply). + toThrow("Non-assignable model expression: 'hello ' + name (directive: myComponent)"); + expect(componentScope.optref).toBe('hello world'); + // reset since the exception was rethrown which prevented phase clearing + $rootScope.$$phase = null; + + $rootScope.name = 'misko'; + $rootScope.$apply(); + expect(componentScope.optref).toBe('hello misko'); + })); + + // regression + it('should stabilize model', inject(function() { + compile('
'); + + var lastRefValueInParent; + $rootScope.$watch('name', function(ref) { + lastRefValueInParent = ref; + }); + + $rootScope.name = 'aaa'; + $rootScope.$apply(); + + componentScope.optreference = 'new'; + $rootScope.$apply(); + + expect(lastRefValueInParent).toBe('new'); + })); + + it('should not throw exception when reference does not exist', inject(function() { + compile('
'); + + expect(componentScope.optref).toBe(undefined); + expect(componentScope.optrefAlias).toBe(undefined); + expect(componentScope.optreference).toBe(undefined); + })); + }); + + describe('executable expression', function() { it('should allow expression execution with locals', inject(function() { compile('
');