Skip to content

Commit c07c4b8

Browse files
committed
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 angular#909 Closes angular#1435
1 parent 69be39f commit c07c4b8

File tree

3 files changed

+113
-3
lines changed

3 files changed

+113
-3
lines changed

docs/content/guide/directive.ngdoc

+3-1
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,9 @@ compiler}. The attributes are:
336336
Given `<widget my-attr="parentModel">` and widget definition of
337337
`scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the
338338
value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected
339-
in `localModel` and any changes in `localModel` will reflect in `parentModel`.
339+
in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent
340+
scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You
341+
can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional.
340342

341343
* `&` or `&attr` - provides a way to execute an expression in the context of the parent scope.
342344
If no `attr` name is specified then the attribute name is assumed to be the same as the

src/ng/compile.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -714,13 +714,14 @@ function $CompileProvider($provide) {
714714
$element = attrs.$$element;
715715

716716
if (newIsolateScopeDirective) {
717-
var LOCAL_REGEXP = /^\s*([@=&])\s*(\w*)\s*$/;
717+
var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
718718

719719
var parentScope = scope.$parent || scope;
720720

721721
forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) {
722722
var match = definiton.match(LOCAL_REGEXP) || [],
723-
attrName = match[2]|| scopeName,
723+
attrName = match[3] || scopeName,
724+
optional = (match[2] == '?'),
724725
mode = match[1], // @, =, or &
725726
lastValue,
726727
parentGet, parentSet;
@@ -736,6 +737,9 @@ function $CompileProvider($provide) {
736737
}
737738

738739
case '=': {
740+
if (optional && (attrs[attrName] == null)) {
741+
return;
742+
}
739743
parentGet = $parse(attrs[attrName]);
740744
parentSet = parentGet.assign || function() {
741745
// reset the change, or we will throw this exception on every $digest

test/ng/compileSpec.js

+104
Original file line numberDiff line numberDiff line change
@@ -1775,6 +1775,9 @@ describe('$compile', function() {
17751775
ref: '=',
17761776
refAlias: '= ref',
17771777
reference: '=',
1778+
optref: '=?',
1779+
optrefAlias: '=? optref',
1780+
optreference: '=?',
17781781
expr: '&',
17791782
exprAlias: '&expr'
17801783
},
@@ -1917,6 +1920,107 @@ describe('$compile', function() {
19171920
});
19181921

19191922

1923+
describe('optional object reference', function() {
1924+
it('should update local when origin changes', inject(function() {
1925+
compile('<div><span my-component optref="name">');
1926+
expect(componentScope.optRef).toBe(undefined);
1927+
expect(componentScope.optRefAlias).toBe(componentScope.optRef);
1928+
1929+
$rootScope.name = 'misko';
1930+
$rootScope.$apply();
1931+
expect(componentScope.optref).toBe($rootScope.name);
1932+
expect(componentScope.optrefAlias).toBe($rootScope.name);
1933+
1934+
$rootScope.name = {};
1935+
$rootScope.$apply();
1936+
expect(componentScope.optref).toBe($rootScope.name);
1937+
expect(componentScope.optrefAlias).toBe($rootScope.name);
1938+
}));
1939+
1940+
1941+
it('should update local when origin changes', inject(function() {
1942+
compile('<div><span my-component optRef="name">');
1943+
expect(componentScope.optref).toBe(undefined);
1944+
expect(componentScope.optrefAlias).toBe(componentScope.optref);
1945+
1946+
componentScope.optref = 'misko';
1947+
$rootScope.$apply();
1948+
expect($rootScope.name).toBe('misko');
1949+
expect(componentScope.optref).toBe('misko');
1950+
expect($rootScope.name).toBe(componentScope.optref);
1951+
expect(componentScope.optrefAlias).toBe(componentScope.optref);
1952+
1953+
componentScope.name = {};
1954+
$rootScope.$apply();
1955+
expect($rootScope.name).toBe(componentScope.optref);
1956+
expect(componentScope.optrefAlias).toBe(componentScope.optref);
1957+
}));
1958+
1959+
1960+
it('should update local when both change', inject(function() {
1961+
compile('<div><span my-component optref="name">');
1962+
$rootScope.name = {mark:123};
1963+
componentScope.optref = 'misko';
1964+
1965+
$rootScope.$apply();
1966+
expect($rootScope.name).toEqual({mark:123})
1967+
expect(componentScope.optref).toBe($rootScope.name);
1968+
expect(componentScope.optrefAlias).toBe($rootScope.name);
1969+
1970+
$rootScope.name = 'igor';
1971+
componentScope.optref = {};
1972+
$rootScope.$apply();
1973+
expect($rootScope.name).toEqual('igor')
1974+
expect(componentScope.optref).toBe($rootScope.name);
1975+
expect(componentScope.optrefAlias).toBe($rootScope.name);
1976+
}));
1977+
1978+
it('should complain on non assignable changes', inject(function() {
1979+
compile('<div><span my-component optref="\'hello \' + name">');
1980+
$rootScope.name = 'world';
1981+
$rootScope.$apply();
1982+
expect(componentScope.optref).toBe('hello world');
1983+
1984+
componentScope.optref = 'ignore me';
1985+
expect($rootScope.$apply).
1986+
toThrow("Non-assignable model expression: 'hello ' + name (directive: myComponent)");
1987+
expect(componentScope.optref).toBe('hello world');
1988+
// reset since the exception was rethrown which prevented phase clearing
1989+
$rootScope.$$phase = null;
1990+
1991+
$rootScope.name = 'misko';
1992+
$rootScope.$apply();
1993+
expect(componentScope.optref).toBe('hello misko');
1994+
}));
1995+
1996+
// regression
1997+
it('should stabilize model', inject(function() {
1998+
compile('<div><span my-component optreference="name">');
1999+
2000+
var lastRefValueInParent;
2001+
$rootScope.$watch('name', function(ref) {
2002+
lastRefValueInParent = ref;
2003+
});
2004+
2005+
$rootScope.name = 'aaa';
2006+
$rootScope.$apply();
2007+
2008+
componentScope.optreference = 'new';
2009+
$rootScope.$apply();
2010+
2011+
expect(lastRefValueInParent).toBe('new');
2012+
}));
2013+
2014+
it('should not throw exception when reference does not exist', inject(function() {
2015+
compile('<div><span my-component>');
2016+
2017+
expect(componentScope.optref).toBe(undefined);
2018+
expect(componentScope.optrefAlias).toBe(undefined);
2019+
expect(componentScope.optreference).toBe(undefined);
2020+
}));
2021+
});
2022+
2023+
19202024
describe('executable expression', function() {
19212025
it('should allow expression execution with locals', inject(function() {
19222026
compile('<div><span my-component expr="count = count + offset">');

0 commit comments

Comments
 (0)