Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Revert "fix($parse): standardize one-time literal vs non-literal and interceptors" #15958

Merged
merged 1 commit into from
Jun 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 45 additions & 8 deletions src/ng/directive/ngClass.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ function classDirective(name, selector) {
return {
restrict: 'AC',
link: function(scope, element, attr) {
var expression = attr[name].trim();
var isOneTime = (expression.charAt(0) === ':') && (expression.charAt(1) === ':');

var watchInterceptor = isOneTime ? toFlatValue : toClassString;
var watchExpression = $parse(expression, watchInterceptor);
var watchAction = isOneTime ? ngClassOneTimeWatchAction : ngClassWatchAction;

var classCounts = element.data('$classCounts');
var oldModulo = true;
var oldClassString;
Expand All @@ -36,7 +43,7 @@ function classDirective(name, selector) {
scope.$watch(indexWatchExpression, ngClassIndexWatchAction);
}

scope.$watch($parse(attr[name], toClassString), ngClassWatchAction);
scope.$watch(watchExpression, watchAction, isOneTime);

function addClasses(classString) {
classString = digestClassCounts(split(classString), 1);
Expand Down Expand Up @@ -78,9 +85,9 @@ function classDirective(name, selector) {
}

function ngClassIndexWatchAction(newModulo) {
// This watch-action should run before the `ngClassWatchAction()`, thus it
// This watch-action should run before the `ngClass[OneTime]WatchAction()`, thus it
// adds/removes `oldClassString`. If the `ngClass` expression has changed as well, the
// `ngClassWatchAction()` will update the classes.
// `ngClass[OneTime]WatchAction()` will update the classes.
if (newModulo === selector) {
addClasses(oldClassString);
} else {
Expand All @@ -90,13 +97,15 @@ function classDirective(name, selector) {
oldModulo = newModulo;
}

function ngClassWatchAction(newClassString) {
// When using a one-time binding the newClassString will return
// the pre-interceptor value until the one-time is complete
if (!isString(newClassString)) {
newClassString = toClassString(newClassString);
function ngClassOneTimeWatchAction(newClassValue) {
var newClassString = toClassString(newClassValue);

if (newClassString !== oldClassString) {
ngClassWatchAction(newClassString);
}
}

function ngClassWatchAction(newClassString) {
if (oldModulo === selector) {
updateClasses(oldClassString, newClassString);
}
Expand Down Expand Up @@ -143,6 +152,34 @@ function classDirective(name, selector) {

return classString;
}

function toFlatValue(classValue) {
var flatValue = classValue;

if (isArray(classValue)) {
flatValue = classValue.map(toFlatValue);
} else if (isObject(classValue)) {
var hasUndefined = false;

flatValue = Object.keys(classValue).filter(function(key) {
var value = classValue[key];

if (!hasUndefined && isUndefined(value)) {
hasUndefined = true;
}

return value;
});

if (hasUndefined) {
// Prevent the `oneTimeLiteralWatchInterceptor` from unregistering
// the watcher, by including at least one `undefined` value.
flatValue.push(undefined);
}
}

return flatValue;
}
}

/**
Expand Down
64 changes: 38 additions & 26 deletions src/ng/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -1801,8 +1801,8 @@ function $ParseProvider() {
if (parsedExpression.constant) {
parsedExpression.$$watchDelegate = constantWatchDelegate;
} else if (oneTime) {
parsedExpression.oneTime = true;
parsedExpression.$$watchDelegate = oneTimeWatchDelegate;
parsedExpression.$$watchDelegate = parsedExpression.literal ?
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
} else if (parsedExpression.inputs) {
parsedExpression.$$watchDelegate = inputsWatchDelegate;
}
Expand Down Expand Up @@ -1888,7 +1888,6 @@ function $ParseProvider() {
}

function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) {
var isDone = parsedExpression.literal ? isAllDefined : isDefined;
var unwatch, lastValue;
if (parsedExpression.inputs) {
unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression);
Expand All @@ -1905,22 +1904,41 @@ function $ParseProvider() {
if (isFunction(listener)) {
listener(value, old, scope);
}
if (isDone(value)) {
if (isDefined(value)) {
scope.$$postDigest(function() {
if (isDone(lastValue)) {
if (isDefined(lastValue)) {
unwatch();
}
});
}
}
}

function isAllDefined(value) {
var allDefined = true;
forEach(value, function(val) {
if (!isDefined(val)) allDefined = false;
});
return allDefined;
function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) {
var unwatch, lastValue;
unwatch = scope.$watch(function oneTimeWatch(scope) {
return parsedExpression(scope);
}, function oneTimeListener(value, old, scope) {
lastValue = value;
if (isFunction(listener)) {
listener(value, old, scope);
}
if (isAllDefined(value)) {
scope.$$postDigest(function() {
if (isAllDefined(lastValue)) unwatch();
});
}
}, objectEquality);

return unwatch;

function isAllDefined(value) {
var allDefined = true;
forEach(value, function(val) {
if (!isDefined(val)) allDefined = false;
});
return allDefined;
}
}

function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) {
Expand All @@ -1936,28 +1954,22 @@ function $ParseProvider() {
var watchDelegate = parsedExpression.$$watchDelegate;
var useInputs = false;

var isDone = parsedExpression.literal ? isAllDefined : isDefined;
var regularWatch =
watchDelegate !== oneTimeLiteralWatchDelegate &&
watchDelegate !== oneTimeWatchDelegate;

function regularInterceptedExpression(scope, locals, assign, inputs) {
var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) {
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
return interceptorFn(value, scope, locals);
}

function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
} : function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
var value = parsedExpression(scope, locals, assign, inputs);
var result = interceptorFn(value, scope, locals);
// we only return the interceptor's result if the
// initial value is defined (for bind-once)
return isDone(value) ? result : value;
}

var fn = parsedExpression.oneTime ? oneTimeInterceptedExpression : regularInterceptedExpression;

// Propogate the literal/oneTime attributes
fn.literal = parsedExpression.literal;
fn.oneTime = parsedExpression.oneTime;
return isDefined(value) ? result : value;
};

// Propagate or create inputs / $$watchDelegates
// Propagate $$watchDelegates other then inputsWatchDelegate
useInputs = !parsedExpression.inputs;
if (watchDelegate && watchDelegate !== inputsWatchDelegate) {
fn.$$watchDelegate = watchDelegate;
Expand Down
142 changes: 69 additions & 73 deletions test/ng/parseSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2688,86 +2688,82 @@ describe('parser', function() {
expect($parse(':: ').literal).toBe(true);
}));

[true, false].forEach(function(isDeep) {
describe(isDeep ? 'deepWatch' : 'watch', function() {
it('should only become stable when all the properties of an object have defined values', inject(function($parse, $rootScope, log) {
var fn = $parse('::{foo: foo, bar: bar}');
$rootScope.$watch(fn, function(value) { log(value); }, isDeep);

expect(log.empty()).toEqual([]);
expect($rootScope.$$watchers.length).toBe(1);

$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([{foo: undefined, bar: undefined}]);

$rootScope.foo = 'foo';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([{foo: 'foo', bar: undefined}]);

$rootScope.foo = 'foobar';
$rootScope.bar = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log.empty()).toEqual([{foo: 'foobar', bar: 'bar'}]);

$rootScope.foo = 'baz';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log.empty()).toEqual([]);
}));
it('should only become stable when all the properties of an object have defined values', inject(function($parse, $rootScope, log) {
var fn = $parse('::{foo: foo, bar: bar}');
$rootScope.$watch(fn, function(value) { log(value); }, true);

expect(log.empty()).toEqual([]);
expect($rootScope.$$watchers.length).toBe(1);

$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([{foo: undefined, bar: undefined}]);

$rootScope.foo = 'foo';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([{foo: 'foo', bar: undefined}]);

$rootScope.foo = 'foobar';
$rootScope.bar = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log.empty()).toEqual([{foo: 'foobar', bar: 'bar'}]);

$rootScope.foo = 'baz';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log.empty()).toEqual([]);
}));

it('should only become stable when all the elements of an array have defined values', inject(function($parse, $rootScope, log) {
var fn = $parse('::[foo,bar]');
$rootScope.$watch(fn, function(value) { log(value); }, isDeep);
it('should only become stable when all the elements of an array have defined values', inject(function($parse, $rootScope, log) {
var fn = $parse('::[foo,bar]');
$rootScope.$watch(fn, function(value) { log(value); }, true);

expect(log.empty()).toEqual([]);
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([]);
expect($rootScope.$$watchers.length).toBe(1);

$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([[undefined, undefined]]);
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([[undefined, undefined]]);

$rootScope.foo = 'foo';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([['foo', undefined]]);
$rootScope.foo = 'foo';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([['foo', undefined]]);

$rootScope.foo = 'foobar';
$rootScope.bar = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log.empty()).toEqual([['foobar', 'bar']]);
$rootScope.foo = 'foobar';
$rootScope.bar = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log.empty()).toEqual([['foobar', 'bar']]);

$rootScope.foo = 'baz';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log.empty()).toEqual([]);
}));
$rootScope.foo = 'baz';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(0);
expect(log.empty()).toEqual([]);
}));

it('should only become stable when all the elements of an array have defined values at the end of a $digest', inject(function($parse, $rootScope, log) {
var fn = $parse('::[foo]');
$rootScope.$watch(fn, function(value) { log(value); }, isDeep);
$rootScope.$watch('foo', function() { if ($rootScope.foo === 'bar') {$rootScope.foo = undefined; } });

$rootScope.foo = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(2);
expect(log.empty()).toEqual([['bar'], [undefined]]);

$rootScope.foo = 'baz';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([['baz']]);

$rootScope.bar = 'qux';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log).toEqual([]);
}));
});
});
it('should only become stable when all the elements of an array have defined values at the end of a $digest', inject(function($parse, $rootScope, log) {
var fn = $parse('::[foo]');
$rootScope.$watch(fn, function(value) { log(value); }, true);
$rootScope.$watch('foo', function() { if ($rootScope.foo === 'bar') {$rootScope.foo = undefined; } });

$rootScope.foo = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(2);
expect(log.empty()).toEqual([['bar'], [undefined]]);

$rootScope.foo = 'baz';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log.empty()).toEqual([['baz']]);

$rootScope.bar = 'qux';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toBe(1);
expect(log).toEqual([]);
}));
});
});

Expand Down