Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Chris Laning
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Chris Laning@claning )

Exploring One-Time Bindings In AngularJS 1.3

By Ben Nadel on

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").



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

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