Skip to main content
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Lola Lee Beno
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Lola Lee Beno ( @lolajl )

Exploring One-Time Bindings In AngularJS 1.3

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

Want to use code from this post? Check out the license.

Reader Comments

1 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?

1 Comments

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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel