-
Notifications
You must be signed in to change notification settings - Fork 27.4k
fix($parse): respect the interceptor.$stateful flag #16015
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1798,15 +1798,9 @@ function $ParseProvider() { | |
var lexer = new Lexer($parseOptions); | ||
var parser = new Parser(lexer, $filter, $parseOptions); | ||
parsedExpression = parser.parse(exp); | ||
if (parsedExpression.constant) { | ||
parsedExpression.$$watchDelegate = constantWatchDelegate; | ||
} else if (oneTime) { | ||
parsedExpression.oneTime = true; | ||
parsedExpression.$$watchDelegate = oneTimeWatchDelegate; | ||
} else if (parsedExpression.inputs) { | ||
parsedExpression.$$watchDelegate = inputsWatchDelegate; | ||
} | ||
cache[cacheKey] = parsedExpression; | ||
parsedExpression.oneTime = !!oneTime; | ||
|
||
cache[cacheKey] = addWatchDelegate(parsedExpression); | ||
} | ||
return addInterceptor(parsedExpression, interceptorFn); | ||
|
||
|
@@ -1890,28 +1884,37 @@ 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); | ||
} else { | ||
unwatch = scope.$watch(oneTimeWatch, oneTimeListener, objectEquality); | ||
} | ||
|
||
var exp = parsedExpression.$$intercepted || parsedExpression; | ||
var post = parsedExpression.$$interceptor || identity; | ||
|
||
var useInputs = parsedExpression.inputs && !exp.inputs; | ||
|
||
// Propogate the literal/inputs/constant attributes | ||
// ... but not oneTime since we are handling it | ||
oneTimeWatch.literal = parsedExpression.literal; | ||
oneTimeWatch.constant = parsedExpression.constant; | ||
oneTimeWatch.inputs = parsedExpression.inputs; | ||
|
||
// Allow other delegates to run on this wrapped expression | ||
addWatchDelegate(oneTimeWatch); | ||
|
||
unwatch = scope.$watch(oneTimeWatch, listener, objectEquality, prettyPrintExpression); | ||
|
||
return unwatch; | ||
|
||
function oneTimeWatch(scope) { | ||
return parsedExpression(scope); | ||
} | ||
function oneTimeListener(value, old, scope) { | ||
lastValue = value; | ||
if (isFunction(listener)) { | ||
listener(value, old, scope); | ||
function unwatchIfDone() { | ||
if (isDone(lastValue)) { | ||
unwatch(); | ||
} | ||
if (isDone(value)) { | ||
scope.$$postDigest(function() { | ||
if (isDone(lastValue)) { | ||
unwatch(); | ||
} | ||
}); | ||
} | ||
|
||
function oneTimeWatch(scope, locals, assign, inputs) { | ||
lastValue = useInputs && inputs ? inputs[0] : exp(scope, locals, assign, inputs); | ||
if (isDone(lastValue)) { | ||
scope.$$postDigest(unwatchIfDone); | ||
} | ||
return post(lastValue, scope, locals); | ||
} | ||
} | ||
|
||
|
@@ -1931,40 +1934,58 @@ function $ParseProvider() { | |
return unwatch; | ||
} | ||
|
||
function addWatchDelegate(parsedExpression) { | ||
if (parsedExpression.constant) { | ||
parsedExpression.$$watchDelegate = constantWatchDelegate; | ||
} else if (parsedExpression.oneTime) { | ||
parsedExpression.$$watchDelegate = oneTimeWatchDelegate; | ||
} else if (parsedExpression.inputs) { | ||
parsedExpression.$$watchDelegate = inputsWatchDelegate; | ||
} | ||
|
||
return parsedExpression; | ||
} | ||
|
||
function chainInterceptors(first, second) { | ||
function chainedInterceptor(value) { | ||
return second(first(value)); | ||
} | ||
chainedInterceptor.$stateful = first.$stateful || second.$stateful; | ||
|
||
return chainedInterceptor; | ||
} | ||
|
||
function addInterceptor(parsedExpression, interceptorFn) { | ||
if (!interceptorFn) return parsedExpression; | ||
var watchDelegate = parsedExpression.$$watchDelegate; | ||
var useInputs = false; | ||
|
||
var isDone = parsedExpression.literal ? isAllDefined : isDefined; | ||
|
||
function regularInterceptedExpression(scope, locals, assign, inputs) { | ||
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs); | ||
return interceptorFn(value); | ||
// Extract any existing interceptors out of the parsedExpression | ||
// to ensure the original parsedExpression is always the $$intercepted | ||
if (parsedExpression.$$interceptor) { | ||
interceptorFn = chainInterceptors(parsedExpression.$$interceptor, interceptorFn); | ||
parsedExpression = parsedExpression.$$intercepted; | ||
} | ||
|
||
function oneTimeInterceptedExpression(scope, locals, assign, inputs) { | ||
var useInputs = false; | ||
|
||
var fn = function interceptedExpression(scope, locals, assign, inputs) { | ||
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs); | ||
var result = interceptorFn(value); | ||
// we only return the interceptor's result if the | ||
// initial value is defined (for bind-once) | ||
return isDone(value) ? result : value; | ||
} | ||
return interceptorFn(value); | ||
}; | ||
|
||
var fn = parsedExpression.oneTime ? oneTimeInterceptedExpression : regularInterceptedExpression; | ||
// Maintain references to the interceptor/intercepted | ||
fn.$$intercepted = parsedExpression; | ||
fn.$$interceptor = interceptorFn; | ||
|
||
// Propogate the literal/oneTime attributes | ||
// Propogate the literal/oneTime/constant attributes | ||
fn.literal = parsedExpression.literal; | ||
fn.oneTime = parsedExpression.oneTime; | ||
fn.constant = parsedExpression.constant; | ||
|
||
// Propagate or create inputs / $$watchDelegates | ||
useInputs = !parsedExpression.inputs; | ||
if (watchDelegate && watchDelegate !== inputsWatchDelegate) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we remove this, what happens with "custom" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This actually fixes a bug because that lost For example watching There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where is the test for this fix then? 😛 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, I am still confused about what happens to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah they are just ignored, because the interceptor might have made that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's an example, I'll try to add a test for this...
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added a test for this: "should not propogate $$watchDelegate to the interceptor wrapped expression" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. And we are fine with that because we never use |
||
fn.$$watchDelegate = watchDelegate; | ||
fn.inputs = parsedExpression.inputs; | ||
} else if (!interceptorFn.$stateful) { | ||
// Treat interceptor like filters - assume non-stateful by default and use the inputsWatchDelegate | ||
fn.$$watchDelegate = inputsWatchDelegate; | ||
// Treat the interceptor like filters. | ||
// If it is not $stateful then only watch its inputs. | ||
// If the expression itself has no inputs then use the full expression as an input. | ||
if (!interceptorFn.$stateful) { | ||
useInputs = !parsedExpression.inputs; | ||
fn.inputs = (parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]).map(function(e) { | ||
// Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a | ||
// potentially non-pure interceptor function. | ||
|
@@ -1975,7 +1996,7 @@ function $ParseProvider() { | |
}); | ||
} | ||
|
||
return fn; | ||
return addWatchDelegate(fn); | ||
} | ||
}]; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How could
parsedExpresion.inputs
be truthy andparsedException.$$intercepted.inputs
be falsy?Can you give an example scenario?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's basically detecting this case. This is when the intercepted expression had no inputs, so the expression itself becomes the input.
For example
$parse("x", stringify)
:x
has no inputs, but we don't want to executestringify(x)
per-digest so we setx
itself as the input anduseInputs = true
to handle this$parse("x | number", stringify)
:x | number
already hasx
as an input so we just continue using that input (anduseInputs = false
)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that this line basically "detects" the
useInputs = true
fromaddInterceptor
is something I don't like about this PR atm. It already existed before, but I guess now it is spread across two methods which seems worse to me. But it's all internal and could always be changed...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't say I can fully wrap my head around this, but the tests pass, so... 😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which part? Anything I can explain more/better? There's a couple weird things being changed in this PR, and I also get confused after not looking at it for a while ^_^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
useInputs
bit still confuses me (but in tests we trust).(At some point I need to grab my debugger and go through some usecases, but this doesn't have to hold this PR back.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
useInputs
is also the part I don't like.Note that I think it should really be renamed to something such as
theInputIsTheInterceptedExpression
(where normally the input is the input to the intercepted expression).It is also just a performance thing, dropping all
useInputs == true
logic would still function correctly.