Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Daniel Heighton
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Daniel Heighton

Providing Default Values For The Safe Navigation Operator In Angular 2 Beta 8

By Ben Nadel on

Prologue: Angular 2 calls the "?." operator the "Elvis Operator". However, this is inconsistent with other programming languages. Historically, "?." is known as the "safe navigation operator". The "Elvis operator" is normally a binary operator that has more to do with providing default values.

In Angular 2, the "Elvis operator" (really, the safe navigation operator) allows an object path to be safely navigated in situations in which you are not entirely sure if every object in the path exists. This prevents null-reference exceptions by returning the result of the path if is valid; or, null otherwise. I don't have a great use-case for this as I would generally hide such ambiguity behind an *ngIf directive. But, I wanted to take this as an opportunity to experiment with Pipes in Angular 2. Specifically, I wanted to see if I could create a coalescing pipe that would return a default value if and only if the given value was null.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

In Angular 2, a pipe is just a JavaScript class that exposes a transform() method. This method takes the input value and an array of optional arguments and returns the transformed output. By default, pipes are "pure," which means that your transform() method won't be re-run unless the inputs to the transform method have changed identity.

To be honest, I haven't really played with pipes very much at all. It was really never something that I even used in Angular 1.x. So, instead of trying to explain things any further, I'll just show you the code that I came up with. In this demo, I have an incomplete object to render (imagine it came back from the data-access layer this way). For key-paths that may not exist, I am using the safe-navigation operator and piping the result into the DefaultPipe transformer, which will coalesce null values.

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Providing Default Values For The Safe Navigation Operator In Angular 2 Beta 8
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Providing Default Values For The Safe Navigation Operator In Angular 2 Beta 8
  • </h1>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // Defer bootstrapping until all of the components have been declared.
  • // --
  • // NOTE: Not all components have to be required here since they will be
  • // implicitly required by other components.
  • requirejs(
  • [ /* Using require() for better readability. */ ],
  • function run() {
  •  
  • var App = require( "App" );
  •  
  • ng.platform.browser.bootstrap( App );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root App component.
  • define(
  • "App",
  • function registerApp() {
  •  
  • var DefaultPipe = require( "DefaultPipe" );
  •  
  • // Configure the App component definition.
  • ng.core
  • .Component({
  • selector: "my-app",
  • pipes: [ DefaultPipe ],
  •  
  • // In this template, notice that we are using the "safe navigation"
  • // operator when outputting the values. If the safe navigation
  • // operator cannot return a value, it returns "null", which we
  • // are then piping into our Default Pipe, which will return the
  • // given default value if it receives "null".
  • template:
  • `
  • <h3>
  • {{ friend.name }}
  • </h3>
  •  
  • <ul>
  • <li>
  • ID: {{ friend.id }}
  • </li>
  • <li>
  • Name: {{ friend.name }}
  • </li>
  • <li>
  • Best Friend: {{ friend.traits?.isBFF | default: "not provided" }}
  • </li>
  • <li>
  • Likes Dogs: {{ friend.likes?.dogs | default: "not provided" }}
  • </li>
  • <li>
  • Likes Cats: {{ friend.likes?.cats | default: false }}
  • </li>
  • </ul>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppController );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // Setup the friend. Notice that we are purposefully leaving out
  • // some keys in the data structure so that we can fill in some
  • // default values in the view.
  • vm.friend = {
  • id: 1,
  • name: "Kim",
  • traits: {
  • isBFF: true
  • }
  • // likes: {
  • // cats: false,
  • // dogs: true
  • // }
  • };
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a pipe operator that will return a given default value if, and
  • // only if, it receives a null value.
  • define(
  • "DefaultPipe",
  • function registerDefaultPipe() {
  •  
  • // Configure the DefaultPipe pipe definition.
  • ng.core
  • .Pipe({
  • name: "default",
  •  
  • // By default, pipes are "pure." However, I am explicitly
  • // defining the "pure" properly since I am still learning. A pure
  • // pipe is only re-evaluated when either the input or any of the
  • // arguments change.
  • pure: true
  • })
  • .Class({
  • constructor: DefaultPipe
  • })
  • ;
  •  
  • return( DefaultPipe );
  •  
  •  
  • // I control the DefaultPipe transformer.
  • function DefaultPipe() {
  •  
  • // Return the public API.
  • return({
  • transform: transform
  • });
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I transform the given value. If the given value is null, and a
  • // default value was provided, the default value is returned.
  • // Otherwise, the given value is returned.
  • function transform( value, args ) {
  •  
  • if ( ( value === null ) && args.length ) {
  •  
  • return( args[ 0 ] );
  •  
  • } else {
  •  
  • return( value );
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, for each potentially problematic object path, I'm using the "default" pipe to provide a default value if the safe navigation operator returns null. And, when we run this page, we get the following output:


 
 
 

 
 Providing default values to the safe navigation operator using a pipe transformation. 
 
 
 

Neither the safe navigation operator nor the pipe transformer are constructs of the Angular 2 framework that I see myself using all that often. There's nothing wrong with them - it's just a personal preference. But, it's nice to know that they can work together quite well.




Reader Comments

Thanks, didn't know that Angular 2 supports this syntax.

But for this example you could just write this:
{{ friend.traits?.isBFF || "not provided" }}
and it will have the almost the same behavoir (except empty strings will also output "not provided").

Reply to this Comment

@John,

No problem - there's a ton of stuff in NG2! But be careful with the isBFF example. It's a Boolean - so if it's `false`, you'll end up with the "not provided" if you just use the OR operator. The trick to the `default:` pipe is that it specifically checks for "undefined" values. Cheers!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.