Be Careful With Compound Conditions In AngularJS (And JavaScript In General)
This post really has nothing to do with AngularJS specifically; but, I happen to trip over this feature of the JavaScript language while constructing an AngularJS view. As such, I thought I should present it in an AngularJS context in case other Angularistas find it interesting. What I'm referring to is the fact that in a compound condition JavaScript will return the last evaluated expression.
Run this demo in my JavaScript Demos project on GitHub.
Just to quickly put the kibosh on any JavaScript haters, what I'm talking about here is not a bug. In fact, just the opposite - it's a freaking amazing, insanely cool, feature of the JavaScript language that makes our lives better every day! When you have a compound condition, JavaScript will return the last evaluated expression within the condition.
As such, the following comparisons all evaluate to True as they are all compared to their last evaluated expression.
console.log( ( true && true && "" ) === "" );
console.log( ( 0 && true && "" ) === 0 );
console.log( ( null || 0 || 3 ) === 3 );
console.log( ( null || 0 || "" ) === "" );
console.log( ( false || "blam" || true ) === "blam" );
console.log( ( true && null && true ) === null );
console.log( ( true && true && 16 ) === 16 );
console.log( ( "foo" || true || 16 ) === "foo" );
Please note that the "last evaluated" expression is not necessarily the "last expression" in the condition - JavaScript will short-circuit the evaluation the moment it knowns when a condition will be True or False.
Ok, so how does this all tie back to AngularJS? In my case, this came up because I was using a compound ngSwitch condition in conjunction with a Boolean ngSwitchWhen case statement. My mistake, at the time, was assuming my switch condition would return a real Boolean value; but, given what I just talked about above, the switch condition does nothing more than return the last evaluated expression, which may or may not be a Boolean value.
To see this in action, I put together a small demo that shows an image thumbnail based on the compound condition in which the thumbnail URL is available and the image has been processed. One of those values is a String, the other is a Boolean. And, with the way that JavaScript works its magic, the order of evaluation makes a big difference:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Be Careful With Compound Conditions In AngularJS (And JavaScript In General)
</title>
<style type="text/css">
p {
border: 1px solid #CCCCCC ;
padding: 10px 10px 10px 10px ;
}
</style>
</head>
<body ng-controller="AppController">
<h1>
Be Careful With Compound Conditions In AngularJS (And JavaScript In General)
</h1>
<!--
In this context, the TRUE case will never be selected because the (&&) operator
will return the last evaluated expression within the condition. In this context,
/ that will be the thumbnailUrl. So, while thumbnailUrl is a TRUHTY value (when
not an empty string), it is certainly NOT "true". As such, it never matches the
"true" CASE.
-->
<p ng-switch="( image.isProcessed && image.thumbnailUrl )">
<img ng-switch-when="true" ng-src="{{ image.thumbnailUrl }}" />
<em ng-switch-default>Your thumbnail is still being generated.</em>
</p>
<!--
In this context, the evaluated expressions are all the same, only the order of
them within the compound condition is different. The (&&) operator will still
return the last evaluated expression; however, this time, the last-evaluated
condition happens to be a real Boolean value. As such, it will match the "true"
CASE.
-->
<p ng-switch="( image.thumbnailUrl && image.isProcessed )">
<img ng-switch-when="true" ng-src="{{ image.thumbnailUrl }}" />
<em ng-switch-default>Your thumbnail is still being generated.</em>
</p>
<!--
In this context, we're using the double-not / double-bang pseudo-operator to
coerce the compound condition into a true Boolean value. This way, even though
the thumbnailUrl is the last evaluated expression, the overall condition is
converted to a Boolean before it is compared to the CASE statements.
--
NOTE: The double-bang operator isn't really an operator - it's simply two
instances of the NOT operator.
-->
<p ng-switch="!!( image.isProcessed && image.thumbnailUrl )">
<img ng-switch-when="true" ng-src="{{ image.thumbnailUrl }}" />
<em ng-switch-default>Your thumbnail is still being generated.</em>
</p>
<!--
In this context, rather than using a compound condition in the View, we're
relying on a computed value that has been added to the View-Model. We still have
the same concerned; but, those concerns have been pushed into the Controller.
-->
<p ng-switch="image.showThumbnail">
<img ng-switch-when="true" ng-src="{{ image.thumbnailUrl }}" />
<em ng-switch-default>Your thumbnail is still being generated.</em>
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.13.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 ) {
// In this scenario, we have the thumbnail URL and we have a flag that
// determines if that URL is ready to be consumed by the client.
$scope.image = {
isProcessed: true,
thumbnailUrl: "./frances-mcdormand.jpg"
}
// Augment the view-model with a flag that more explicitly states
// whether or not the image thumbnail should be displayed. In this
// context, we still have to worry about the thumbnailUrl not evaluating
// to a true Boolean value; so, we still need to use the double-bang to
// coerce it into a true Boolean.
$scope.image.showThumbnail = !! ( $scope.image.isProcessed && $scope.image.thumbnailUrl );
}
);
</script>
</body>
</html>
Notice that in the first example the thumbnail URL is the last part of the compound condition. As such, while the non-empty string is a Truthy value, the switch never matches against the True case statement:
As you can see, the compound ngSwitch conditions worked when the last expression was a strict Boolean; but, that feels like it merely works by coincidence (at least in this case). As such, I would prefer either using the double-not operator to explicitly coerce the condition into a strict Boolean value; or, defer to a computed View-Model property that does the same.
Again, this really has nothing to do with AngularJS specifically. However, since the View portion of an AngularJS application creates a layer of abstraction, it can be easier, in an AngularJS context, to forget how JavaScript works. Nothing here is a bug - it's just a feature of the language and needs to be treated as such.
Want to use code from this post? Check out the license.
Reader Comments
Ben -
You always explain things so well! :~) such soothing voice! gracias!
I'm not familiar with Angular concepts and internals, but it seems to me that Angular would be "more compatible" with JavaScript's coercion mechanism if it itself performed coercion in the relevant places.
@Šime,
I think it does tend to do this where it can. For example, there is a directive - ng-if - that will implicitly coerce its input to a Boolean:
<div ng-if=" someTruthy "> ... </div>
The problem with the ngSwitch / ngSwitchWhen is that this is basically how JavaScript works under the hood. If we try to replicate the problem with vanilla JavaScript:
... we get "Matched some string".
Here, the core switch operator did the same thing - our compound "truthy" condition simply returned the last evaluated expression, "some string", not "true".
So, I don't call this out as a bug - just something to be aware of.