Exploring One-Time Bindings In AngularJS 1.3
When I first starting reading about one-time data bindings in AngularJS 1.3, it wasn't immediately clear to me how they worked or how to use them. When I finally took a look at the source code last night, it finally clicked: one-time bindings aren't a "single thing." Rather, it's the interplay between the $parse() service and the various $watch-bindings. Once I understood this, the one-time data bindings made sense.
Run this demo in my JavaScript Demos project on GitHub.
As of AngularJS 1.3, you can use the "::" token to create one-time data bindings. These are bindings that deregister their own $watch() functions once the value has stabilized (which basically means the value is defined). But, to me, this doesn't clarify the usage of the "::" token. The most confusing part of the AngularJS documentation, regarding one-time data bindings, is the diversity of examples. Both of these attributes apparently use one-time data bindings:
- name="::myName"
- ng-repeat="item in ::items"
When I look at this, it becomes unclear as to where I need to put the "::". Is it at the beginning of the attribute? Or before any variable?
It turns out, the answer is "neither". The placement of the "::" in the attribute is completely irrelevant. The only thing that matters is how the attribute value is transformed into $watch() bindings by the consuming directive. This isn't clear, however, until you look at the source code and see what the one-time data binding is actually doing.
The one-time data bindings require two parts to function. It needs a parsed expression and a $watch() binding. The $parse() service is the first key ingredient. In order to get a one-time data binding, you have call $parse() - implicitly or explicitly - with a string that starts with "::":
$parse( ":: then anything else you want after" )
When $parse() detects ":" in characters 0 and 1, it will strip them out and flag the expression as a one-time expression. This does not affect the functionality of the computed accessor; however, it will assign a "watch delegate" to the accessor which, subsequently, will be consumed by any $watch() bindings that monitor that accessor.
This "watch delegate" acts as a proxy to your $watch() handler and calls its own deregistration function once the value of the expression is computed (and is not undefined).
So, going back to the attribute example above:
ng-repeat="item in ::items"
... this works because the "::items" string is passed into Scope.$watchCollection(), which will, in turn, pass it to $parse(), which will detect the leading "::" token, flag it as a one-time binding, parse it and provide a "one-time watch delegate" which will be consumed by $watchCollection().
To see this in action, I tried to create a demo that explicitly showcases the interplay between the $parse() service and the $watch() method:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Exploring One-Time Bindings In AngularJS 1.3
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">
<h1>
Exploring One-Time Bindings In AngularJS 1.3
</h1>
<p>
<em>View the console.</em>
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.8.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope, $parse, $timeout ) {
// The one-time binding feature isn't one thing, exactly. It's the
// interplay between parsed expressions and $watch() bindings. An
// expression will be flagged as a "one-time" expression if it starts
// with "::". The "::" is not part of the expression - it gets stripped
// out before AngularJS actually parses the expression.
var accessor = $parse( "::( 'My Friend, ' + friend )" );
// Now that we have our parsed expression (as an accessor object), we can
// test its functionality both when accessed directly (as a function) and
// when consumed in a $watch() binding.
$scope.friend = "Kim";
console.log( "From parse:", accessor( $scope ) );
$scope.$watch(
accessor,
function handleModelChange( newValue ) {
console.log( "From watch:", newValue );
}
);
// For the first watch binding, we're passing in the already-parsed
// expression; but, you can use the one-time binding syntax directly in
// the $watch() binding, as this value is simply passed to $parse()
// service behind the scenes.
$scope.$watch(
"::friend",
function handleModelChange( newValue ) {
console.log( "From watch (2):", newValue );
}
);
// Change the value at a later point in time (to make sure we're not in
// the same digest as the Controller body).
$timeout(
function asyncProcessing() {
// NOTE: This change in $scope.friend would normally trigger the
// the $watch() binding above; but when you look at the console
// output, you will see that the $watch() callback is only
// invoked once, for the initial configuration (so long as it
// doesn't result in an undefined value).
$scope.friend = "Sarah";
console.log( "From parse:", accessor( $scope ) );
},
10
);
}
);
</script>
</body>
</html>
Once we have a our parsed "one time" expression, I go about changing the relevant Scope value. Then, we see how this affects both the computed accessor and the $watch() bindings. I've create two kinds of $watch() bindings to demonstrate that $watch() passes its expression off to the $parse() service behind the scenes (and that the delivery mechanism doesn't matter).
When we run this code, we get the following console output:
From parse: My Friend, Kim
From watch: My Friend, Kim
From watch (2): Kim
From parse: My Friend, Sarah
As you can see, both $watch() bindings (using either the computed accessor or the raw string expression) were invoked for the first value of "friend". However, when I changed the value to "Sarah", and triggered another digest, the computed accessor returned the updated value while neither $watch() callbacks were invoked. This is because both $watch() bindings deregistered themselves after the first $digest phase completed with a defined value.
In AngularJS 1.3, you can now use the "::" token to define one-time data bindings. However, this only works if the given expression is passed to the $parse() service and then consumed by one of the $watch methods (ie, $watch(), $watchCollection(), etc.). This is particularly important to understand, especially when you start creating your own directives which can accept complex statements (like the ngRepeat statement, "item in items").
Reader Comments
@All,
I did a quick follow-up on one-time data bindings to see how they work with object-literal expressions:
www.bennadel.com/blog/2760-one-time-data-bindings-for-object-literal-expressions-in-angularjs-1-3.htm
Even if you've never set up an object-literal $watch() binding explicitly, this is exactly what ngClass and ngStyle do; so, if you use those directives, this exploration is meaningful to you.
Hi Ben,
Thank you for the excellent article. Here is a problem that I am facing with one time bindings. I want to create a large table to entries. I also want to be able to add entries to that table as well as inline-edit individual rows on that table (without having to reload the table entries). However, since the table is large, I want to use one-time binding to render it.
Is there some way to achieve this while still using the one-time bindings?
We are using an api [bindonce][1] for one time binding in `ng-repat` along with its other syntax e.g. `bo-if` for `ng-if` or `bo-bind` for `ng-bind`
As, We see here. AngularJS 1.3 on wards has built in syntax `ng-repeat="item in ::items"`.
So, With newer code we are using this new syntax. But, Out of curiosity,
Can this bindonce API cause memory leaks.?
[1]: https://github.com/Pasvaz/bindonce
@Mopy,
Would also love to know the answer to your question