-
Notifications
You must be signed in to change notification settings - Fork 27.4k
feat(parse): ability to bind once an expressions #7486
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 |
---|---|---|
|
@@ -197,3 +197,98 @@ expose a `$event` object within the scope of that expression. | |
|
||
Note in the example above how we can pass in `$event` to `clickMe`, but how it does not show up | ||
in `{{$event}}`. This is because `$event` is outside the scope of that binding. | ||
|
||
|
||
## One-time binding | ||
|
||
An expression that starts with `::` is considered a one-time expression. One-time expressions | ||
will take the first non-undefined value assigned to them within a $digest | ||
|
||
<example module="oneTimeBidingExampleApp"> | ||
<file name="index.html"> | ||
<div ng-controller="EventController"> | ||
<button ng-click="clickMe($event)">Click Me</button> | ||
<p>One time binding {{::name}}</p> | ||
<p>Normal binding {{name}}</p> | ||
</div> | ||
</file> | ||
<file name="script.js"> | ||
angular.module('oneTimeBidingExampleApp', []). | ||
controller('EventController', ['$scope', function($scope) { | ||
var counter = 0; | ||
var names = ['Igor', 'Misko', 'Chirayu', 'Lucas']; | ||
/* | ||
* expose the event object to the scope | ||
*/ | ||
$scope.clickMe = function(clickEvent) { | ||
$scope.name = names[counter % names.length]; | ||
counter++; | ||
}; | ||
}]); | ||
</file> | ||
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. can you also write an e2e test for this please? 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 have a test for this already. |
||
</example> | ||
|
||
|
||
### Why this feature | ||
|
||
The main purpose of one-time binding expression is to provide a way to create a binding | ||
that gets deregistered and frees up resources once the binding is initialized. | ||
Reducing the number of expressions being watched makes the digest | ||
loop faster and allows more information to be displayed at the same time. | ||
|
||
|
||
### Value stabilization algorithm | ||
|
||
One-time binding expressions will retain the value of the expression at the end of the | ||
digest cycle as long as that value is not undefined. If the value of the expression is set | ||
within the digest loop and later, within the same digest loop, it is set to undefined, | ||
then the expression is not fulfilled and will remain watched. | ||
|
||
1. Given an expression that starts with `::` when a digest loop is entered and expression | ||
is dirty-checked store the value as V | ||
2. If V is not undefined mark the result of the expression as stable and schedule a task | ||
to deregister the watch for this expression when we exit the digest loop | ||
3. Process the digest loop as normal | ||
4. When digest loop is done and all the values have settled process the queue of watch | ||
deregistration tasks. For each watch to be deregistered check if it still evaluates | ||
to value that is not `undefined`. If that's the case, deregister the watch. Otherwise | ||
keep dirty-checking the watch in the future digest loops by following the same | ||
algorithm starting from step 1 | ||
|
||
|
||
### How to benefit from one-time binding | ||
|
||
When interpolating text or attributes. If the expression, once set, will not change | ||
then it is a candidate for one-time expression. | ||
|
||
```html | ||
<div name="attr: {{::color}}">text: {{::name}}</div> | ||
``` | ||
|
||
When using a directive with bidirectional binding and the parameters will not change | ||
|
||
```js | ||
someModule.directive('someDirective', function() { | ||
return { | ||
scope: { | ||
name: '=', | ||
color: '@' | ||
}, | ||
template: '{{name}}: {{color}}' | ||
}; | ||
}); | ||
``` | ||
|
||
```html | ||
<div some-directive name=“::myName” color=“My color is {{::myColor}}”></div> | ||
``` | ||
|
||
|
||
When using a directive that takes an expression | ||
|
||
```html | ||
<ul> | ||
<li ng-repeat="item in ::items">{{item.name}};</li> | ||
</ul> | ||
``` | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -252,16 +252,19 @@ function $InterpolateProvider() { | |
|
||
|
||
try { | ||
interpolationFn.$$unwatch = true; | ||
for (; i < ii; i++) { | ||
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. there is something wrong with interpolation. check out this plunker: http://plnkr.co/edit/rJOqa0oh35Y0xBhTYZ6w the case with interpolation gets messed up on the digest after the watches are finalized. 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. There were two issues, one was with text interpolation and transclusion and the other was with |
||
val = getValue(parseFns[i](context)); | ||
if (allOrNothing && isUndefined(val)) { | ||
interpolationFn.$$unwatch = undefined; | ||
return; | ||
} | ||
val = stringify(val); | ||
if (val !== lastValues[i]) { | ||
inputsChanged = true; | ||
} | ||
values[i] = val; | ||
interpolationFn.$$unwatch = interpolationFn.$$unwatch && parseFns[i].$$unwatch; | ||
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 like how you simplified this! |
||
} | ||
|
||
if (inputsChanged) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1246,13 +1246,19 @@ function $ParseProvider() { | |
}; | ||
|
||
return function(exp) { | ||
var parsedExpression; | ||
var parsedExpression, | ||
oneTime; | ||
|
||
switch (typeof exp) { | ||
case 'string': | ||
|
||
if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { | ||
oneTime = true; | ||
exp = exp.substring(2); | ||
} | ||
|
||
if (cache.hasOwnProperty(exp)) { | ||
return cache[exp]; | ||
return oneTime ? oneTimeWrapper(cache[exp]) : cache[exp]; | ||
} | ||
|
||
var lexer = new Lexer($parseOptions); | ||
|
@@ -1265,14 +1271,43 @@ function $ParseProvider() { | |
cache[exp] = parsedExpression; | ||
} | ||
|
||
return parsedExpression; | ||
if (parsedExpression.constant) { | ||
parsedExpression.$$unwatch = true; | ||
} | ||
|
||
return oneTime ? oneTimeWrapper(parsedExpression) : parsedExpression; | ||
|
||
case 'function': | ||
return exp; | ||
|
||
default: | ||
return noop; | ||
} | ||
|
||
function oneTimeWrapper(expression) { | ||
var stable = false, | ||
lastValue; | ||
oneTimeParseFn.literal = expression.literal; | ||
oneTimeParseFn.constant = expression.constant; | ||
oneTimeParseFn.assign = expression.assign; | ||
return oneTimeParseFn; | ||
|
||
function oneTimeParseFn(self, locals) { | ||
if (!stable) { | ||
lastValue = expression(self, locals); | ||
oneTimeParseFn.$$unwatch = isDefined(lastValue); | ||
if (oneTimeParseFn.$$unwatch && self && self.$$postDigestQueue) { | ||
self.$$postDigestQueue.push(function () { | ||
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. is it important that we freeze this function? our end goal is to unregister the watch and not make the parse function cachable. 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. Given that |
||
// create a copy if the value is defined and it is not a $sce value | ||
if ((stable = isDefined(lastValue)) && !lastValue.$$unwrapTrustedValue) { | ||
lastValue = copy(lastValue); | ||
} | ||
}); | ||
} | ||
} | ||
return lastValue; | ||
} | ||
} | ||
}; | ||
}]; | ||
} |
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.
we are going to need more docs than this. I suggest reusing portions of the design doc that describes: