feat($compile): add one-way collection bindings

Closes #14039
Closes #16553
Closes  #15874
This commit is contained in:
Jason Bedard
2018-05-17 06:30:39 -07:00
committed by Martin Staffa
parent f6e189dde0
commit f9d1ca20c3
2 changed files with 116 additions and 2 deletions
+7 -2
View File
@@ -339,6 +339,11 @@
* One-way binding is useful if you do not plan to propagate changes to your isolated scope bindings
* back to the parent. However, it does not make this completely impossible.
*
* By default, the {@link ng.$rootScope.Scope#$watch `$watch`}
* method is used for tracking changes, and the equality check is based on object identity.
* It's also possible to watch the evaluated value shallowly with
* {@link ng.$rootScope.Scope#$watchCollection `$watchCollection`}: use `<*` or `<*attr`
*
* * `&` 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 local name.
* Given `<my-component my-attr="count = count + value">` and the isolate scope definition `scope: {
@@ -1068,7 +1073,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var bindingCache = createMap();
function parseIsolateBindings(scope, directiveName, isController) {
var LOCAL_REGEXP = /^([@&<]|=(\*?))(\??)\s*([\w$]*)$/;
var LOCAL_REGEXP = /^([@&]|[=<](\*?))(\??)\s*([\w$]*)$/;
var bindings = createMap();
@@ -3615,7 +3620,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var initialValue = destination[scopeName] = parentGet(scope);
initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) {
removeWatch = scope[definition.collection ? '$watchCollection' : '$watch'](parentGet, function parentValueWatchAction(newValue, oldValue) {
if (oldValue === newValue) {
if (oldValue === initialValue || (isLiteral && equals(oldValue, initialValue))) {
return;
+109
View File
@@ -5148,6 +5148,9 @@ describe('$compile', function() {
owOptref: '<?',
owOptrefAlias: '<? owOptref',
$owOptrefAlias: '<? $owOptref$',
owColref: '<*',
owColrefAlias: '<* owColref',
$owColrefAlias: '<* $owColref$',
expr: '&',
optExpr: '&?',
exprAlias: '&expr',
@@ -6327,6 +6330,112 @@ describe('$compile', function() {
});
});
describe('one-way collection bindings', function() {
it('should update isolate scope when origin scope changes', inject(function() {
$rootScope.collection = [{
name: 'Gabriel',
value: 18
}, {
name: 'Tony',
value: 91
}];
$rootScope.query = '';
$rootScope.$apply();
compile('<div><span my-component ow-colref="collection | filter:query" $ow-colref$="collection | filter:query">');
expect(componentScope.owColref).toEqual($rootScope.collection);
expect(componentScope.owColrefAlias).toEqual(componentScope.owColref);
expect(componentScope.$owColrefAlias).toEqual(componentScope.owColref);
$rootScope.query = 'Gab';
$rootScope.$apply();
expect(componentScope.owColref).toEqual([$rootScope.collection[0]]);
expect(componentScope.owColrefAlias).toEqual([$rootScope.collection[0]]);
expect(componentScope.$owColrefAlias).toEqual([$rootScope.collection[0]]);
}));
it('should not update isolate scope when deep state within origin scope changes', inject(function() {
$rootScope.collection = [{
name: 'Gabriel',
value: 18
}, {
name: 'Tony',
value: 91
}];
$rootScope.$apply();
compile('<div><span my-component ow-colref="collection" $ow-colref$="collection">');
expect(componentScope.owColref).toEqual($rootScope.collection);
expect(componentScope.owColrefAlias).toEqual(componentScope.owColref);
expect(componentScope.$owColrefAlias).toEqual(componentScope.owColref);
componentScope.owColref = componentScope.owColrefAlias = componentScope.$owColrefAlias = undefined;
$rootScope.collection[0].name = 'Joe';
$rootScope.$apply();
expect(componentScope.owColref).toBeUndefined();
expect(componentScope.owColrefAlias).toBeUndefined();
expect(componentScope.$owColrefAlias).toBeUndefined();
}));
it('should update isolate scope when origin scope changes', inject(function() {
$rootScope.gab = {
name: 'Gabriel',
value: 18
};
$rootScope.tony = {
name: 'Tony',
value: 91
};
$rootScope.query = '';
$rootScope.$apply();
compile('<div><span my-component ow-colref="[gab, tony] | filter:query" $ow-colref$="[gab, tony] | filter:query">');
expect(componentScope.owColref).toEqual([$rootScope.gab, $rootScope.tony]);
expect(componentScope.owColrefAlias).toEqual([$rootScope.gab, $rootScope.tony]);
expect(componentScope.$owColrefAlias).toEqual([$rootScope.gab, $rootScope.tony]);
$rootScope.query = 'Gab';
$rootScope.$apply();
expect(componentScope.owColref).toEqual([$rootScope.gab]);
expect(componentScope.owColrefAlias).toEqual([$rootScope.gab]);
expect(componentScope.$owColrefAlias).toEqual([$rootScope.gab]);
}));
it('should update isolate scope when origin literal object content changes', inject(function() {
$rootScope.gab = {
name: 'Gabriel',
value: 18
};
$rootScope.tony = {
name: 'Tony',
value: 91
};
$rootScope.$apply();
compile('<div><span my-component ow-colref="[gab, tony]" $ow-colref$="[gab, tony]">');
expect(componentScope.owColref).toEqual([$rootScope.gab, $rootScope.tony]);
expect(componentScope.owColrefAlias).toEqual([$rootScope.gab, $rootScope.tony]);
expect(componentScope.$owColrefAlias).toEqual([$rootScope.gab, $rootScope.tony]);
$rootScope.tony = {
name: 'Bob',
value: 42
};
$rootScope.$apply();
expect(componentScope.owColref).toEqual([$rootScope.gab, $rootScope.tony]);
expect(componentScope.owColrefAlias).toEqual([$rootScope.gab, $rootScope.tony]);
expect(componentScope.$owColrefAlias).toEqual([$rootScope.gab, $rootScope.tony]);
}));
});
describe('executable expression', function() {
it('should allow expression execution with locals', inject(function() {
compile('<div><span my-component expr="count = count + offset" $expr$="count = count + offset">');