FalcorJS with AngularJS v1.x

Ever since it was presented at ng-conf, I've been excited to get my hands on Netflix's new "One Model Everywhere" data-fetching framework: FalcorJS. Now it's been released on the world as a Developer Preview, I wanted to show how it can be used with AngularJS v1.x.

What is FalcorJS?

"A JavaScript library for efficient data fetching"

That sums up FalcorJS pretty well.
It's designed to retrieve only as much data as you need at a time by letting the view specify the values it needs, then the model can batch those requests up when it calls the server.

Falcor represents your data as a big JSON 'graph', which means you can request the same data from different paths and it will use the cache from the first request.

{
  todosById: {
    "44": {
      name: "get milk from corner store",
      done: false,
      prerequisites: [{ $type: "ref", value: ["todosById", 54] }]
    },
    "54": {
      name: "withdraw money from ATM",
      done: false,
      prerequisites: []
    }
  },
  todos: [
    { $type: "ref", value: ["todosById", 44] },
    { $type: "ref", value: ["todosById", 54] }
  ]
};

Sample from FalcorJS' JSON Graph Guide

One odd thing you will have to get used to with FalcorJS is that you only request 1 value at a time. And by "value" I mean: string, number, boolean, or null. This feels totally alien coming from... everything else, but it works because FalcorJS takes care of the efficiencies for you. Instead of having 1 request for an array of objects, which you then have to split up into 20 fields on your view - you instead make 20 requests from your view, which are automatically batched for you into 1 request.

Other mechanics that Falcor provides is functions on your JSON Graph than you can 'call()', which is good for things like creating whole new objects and transactions where you want to be sure everything is being handled in one go. Responses can also include multiple values, for situations where you're sure it's going to be required, and can specify what routes should have their cache invalidated.

The last thing to mention is that FalcorJS is built on Observables from RxJS for all it's asynchronous operations. For those unfamiliar with them, Observables and the whole ReactiveX API are the ultimate asynchronous toolkit. Observables fill in the gaps with Promises, things like how to handle multiple values over time, and whether new 'observers' see just new values or the preceding values too. I won't go into Observables too much, but there are lots and lots of excellent resources online.

Using with AngularJS v1.x

Disclaimer: Once again, FalcorJS is in Developer Preview. So what's written here today may not work tomorrow. Check the FalcorJS API Reference and Github if you have difficulties.

In this example we're going to build a UI with AngularJS v1.x that uses a FalcorJS Model with a HttpDataSource pointed at our falcor-express-demo and falcor-router-demo server.

import angular from 'angular';  
import falcor from 'falcor';  
import HttpDataSource from 'falcor-http-datasource';

class MyController {  
  constructor() {
    this.model = new falcor.Model({
        source: new HttpDataSource('/model.json')
      }).batch(); // Batches value requests together
  }
}

angular.module('falcorExample', [])  
  .controller('MyController', MyController);

With our Model setup we can pull values from the server using this.model.getValue(path). It will then build the necessary XHR call and return an Observable for it's result.

The $apply problem

Wait a sec... if it makes it's own XHR call, how will Angular know when to start a $digest loop?

Well the usual solution in these circumstances is to use $scope.$apply(), such as when binding to an event that's not triggered within Angular's digest loop. However we can't actually be 100% sure that we're not in the digest loop either. For example, if we're hitting cached values, all the callback functions could be called immediately. A better solution is to use $scope.$evalAsync() which will start a new digest loop if, and only if, one is not already in progress. So any time we subscribe to an Observable, we simply need to call $scope.$evalAsync() and we're good to go.

// snip ...

class MyController {  
  // snip...
  getValue() {
    this.model.getValue('titlesById[523].name')
      .subscribe(value => {
        this.value = value;
        this.$scope.$evalAsync();
      });
  }
}

angular.module('falcorExample', [])  
  .controller('MyController', MyController);

Calling from the View

The next thing we want is to start making calls from the View rather than making it the controller's responsibility. This helps to decouple the view and the controller further (all the controller supplies is a reference to the model), and means we're only requesting as much data as we're actually using. eg. If we're showing a cut-down mobile friendly view, we don't have to pull back as much data.
But how are we going to get our results onto the View? All our requests are going to return fresh Observables.

This is where the Model.getCache(path) method comes in. It synchronously returns the value from the Model's cache. If that returns undefined, we can call Model.getValue(path).subscribe(_ => $scope.$evalAsync()) - It'll request the value and call $scope.$evalAsync() once it returns, which means a new digest loop, which will trigger a new call, which will get a successful hit from the cache.

class MyController {  
  // ... snip
  viewValue(path) {
    // getCache() returns a JSON fragment, so we need to parse the path
    const value = this.$parse(path)(this.model.getCache(path));
    if (typeof value !== 'undefined') {
      return typeof value === 'object' ? value.value : value;
    }

    // Cache miss - request the real value
    this.model.getValue(path)
      .subscribe(_ => this.$scope.$evalAsync());
  }
}
<dl>  
  <dt>Title</dt>
  <dd>{{:: ctrl.viewValue('titlesById[0].title')}}</dd>
  <dt>Description</dt>
  <dd>{{:: ctrl.viewValue('titlesById[0].description')}}</dd>
</dl>  

Dealing with collections

Since FalcorJS doesn't support returning objects and arrays (well, sort of) we need a way to deal with collections in our data. The solution that I prefer is to generate an array of index numbers which represent a 'page range', and request the length of your collection ahead of time to set your max index.

function pageRangeFilterProvider() {  
  return function pageRangeFilter(itemCount, currentPage, pageSize) {
    itemCount = itemCount || 0; // The 'length' of the collection
    currentPage = currentPage || 1; // The current page number being viewed
    pageSize = pageSize || 5; // The size of each page
    const range = [];
    for (var x = (currentPage - 1) * pageSize; x < currentPage * pageSize && x < itemCount; x++) {
      range.push(x);
    }
    return range;
  };
}

angular.module('falcorExample')  
  .filter('pageRange', pageRangeFilterProvider);
<ul>  
  <li ng-repeat="index in ctrl.viewValue('titlesById.length') | pageRange:ctrl.page:ctrl.pageSize">
    <dl>
      <dt>Title</dt>
      <dd>{{:: ctrl.viewValue('titlesById[' + index + '].title')}}</dd>
      <dt>Description</dt>
      <dd>{{:: ctrl.viewValue('titlesById[' + index + '].description')}}</dd>
    </dl>
  </li>
</ul>  

Another thing you may be able to use is the Model.deref(path) method, passing the result to another directive that can handle resolving the Observable.

What about writing data?

Falcor comes with two primary methods for writing values: setValue(path, value) and call(path, args).

setValue() sets the value for a particular path, returning an Observable for the result. Integrating it with ng-model is simple using ng-model-options="{getterSetter: true}". We create a function which takes the model and the path, then returns a function which either calls getValue() or setValue(), depending on the number of arguments.

class MyController {  
  // ... snip
  getterSetter(path) {
    // Need to return an unbound function
    var ctrl = this;
    return function (newValue) {
      // If arguments.length === 1 then the function is used as a setter,
      // otherwise it's a getter
      return arguments.length
        // setValue() returns an Observable.
        // So we call $scope.$evalAsync() when we subscribe() to it,
        // which keeps us in sync with the digest loop
        ? ctrl.model.setValue(path, newValue).subscribe(_ => ctrl.$scope.$evalAsync())
        // Here we'll reuse viewValue()
        : ctrl.viewValue(path);
    };
  }
}
<dl>  
  <dt>Title</dt>
  <dd>{{:: ctrl.viewValue('titlesById[0].title')}}</dd>
  <dt>Description</dt>
  <dd><input type="text" ng-model="ctrl.getterSetter('titlesById[0].description')" ng-model-options="{getterSetter: true}"/></dd>
</dl>  

setValue() is great if you want to set one value at a time. But what if you want to save the entire form at once, after you've confirmed your data is valid? This is where call() comes in.

The Falcor Router supports 3 different types of operations on each route: get, set, and call. call turns the route into an RPC endpoint where you pass in arguments, which can include objects and arrays (anything that can be JSON encoded), and it returns a set of PathValues and cache invalidations for the Falcor Model to interpret.

class MyController {  
  // ... snip
  addTodoClicked() {
    // Calls 'todos.add()' with the formModel
    this.model.call(['todos', 'add'], [this.formModel])
      .subscribe(_ => this.$scope.$evalAsync());

    // Clear the form  
    this.formModel = {};
  }
}

Summary

Once again I have to repeat: Falcor is in "Developer Preview" mode. Which means that not only is it not ready for production, but it's also only recommended for those of us who like to cut ourselves on the bleeding edge.

With that in mind: Falcor is pretty solid, and the learning curve is quite smooth. It does require rethinking your data API to fit "the falcor way", but that's a common cost with opinionated frameworks which pays back with simpler and more consistent code.

Where it really excels most is with "mostly-read/rarely-write" type applications, but you can still do write operations with setValue() and calls(). I probably wouldn't try to write any games with it (though that might be a fun exercise all the same).

I've started a github repository with a working example of everything I've shown here, along with a "falcor" module that encapsulates the falcor specific code.

Cheers,
Jason Stone

(Originally posted 6 Sep 2015 at legacytotheedge.blogspot.com.au)

Jason Stone

Jason is an accidental front end developer with delusions of grandeur.

Sydney, Australia