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.
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:
- 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:
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").
Want to use code from this post? Check out the license.
I did a quick follow-up on one-time data bindings to see how they work with object-literal expressions:
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.
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] 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.?
Would also love to know the answer to your question