Wednesday, April 13, 2016

Help! I'm using Asynchronous JavaScript and my View isn't Updating

One of the most common knockout binding questions/misunderstandings I see involves updating Knockout Observables with Asynchronous data. Whether it is a select/options list, a table, a chart, or any other data-driven component, we often load data asynchronously (Ajax). The benefit of this asynchronous architecture is that our View renders before the ViewModel has all of its data. Users see most of the application render and have a feeling of better performance. It is at this render point that bindings are bound—data pointers are locked in. Now the data arrives... how you push those data values into the bound observables can be the difference between success and failure. In this blog post I want to share with you a few ways to update observables while ensuring the View stays bound to the ViewModel. First things first: we need a mock Ajax service that returns data asynchronously. In this case, mock means we aren't actually going to fetch data via Ajax. We are just going to yield JavaScript execution for a short period of time. The following listing uses setTimeout to mimic the delay of an asynchronous response:

var fakeJAX = function() {
  return new Promise(function(resolve, reject) {
    window.setTimeout(function() {
      resolve([
        {id: 101, name: "Beauregard Duke"},
        {id: 102, name: "Lucas Duke"},
        {id: 103, name: "Daisy Duke"},
        {id: 201, name: "Cooter Davenport"},
        {id: 301, name: "Jefferson Davis Hogg"},
        {id: 302, name: "Lulu Coltrane Hogg"},
        {id: 401, name: "Rosco P Coltrane"}
      ]);
    }, 2000);
  });
};

As you can see, the method returns an HTML5 Promise that resolves when the timer expires (2 seconds). The result/interface is designed to mimic the fetch API.

When it comes to out-right errors, here is the common one I see:

var ViewModel = function() {
  var self = this;

  self.employees = ko.observableArray([]);

  self.datasource = new oj.ArrayTableDataSource(
    self.employees, {
      idAttribute: 'id'
    });
      
  // Array Push All option
  fakeJAX()
    .then(function(data) {
        self.employees = ko.observableArray(data);
      });
  };

Do you see the problem? Inside the Asynchronous response handler, we are changing the self.employees pointer. This breaks the relationship between our ViewModel and the DOM. The view now points to the old memory space whereas self.employees points to a new memory space. Note: Later you will see how to work around this by using the if binding.

Now let's look at an example that is technically correct, but may not work with certain components because of the internal workings of the component (the way the component is implemented and how it binds to the observable). In Knockout it is common to swap out the value of the observable. This works great... most of the time. Here is an example:

fakeJAX()
  .then(function(data) {
    self.employees(data);
  });

Now don't get me wrong. This works most of the time. But if you find that your observable isn't updating, then try pushing values directly into the array wrapped by the Observable Array. Here is an example:

fakeJAX()
  .then(function(data) {
    ko.utils.arrayPushAll(self.employees(), data);
    self.employees.valueHasMutated();
  });

There are times where pushing into the array in-place won't work either. If you find yourself in a situation where the last two options won't work, then try this next approach. I have yet to find a scenario where this next approach does not work. It uses the knockout if binding to hide a data bound element from the DOM until we have data. Here is an HTML example of a table wrapped in an if binding:

<!-- ko if: haveEmployees --> 
  <table id="table" summary="Employee Table 2" aria-label="Employee Table"
         data-bind="ojComponent: {component: 'ojTable',
                    data: datasource,
                    columnsDefault: {sortable: 'none'},
                    columns: [{headerText: 'Employee Id',
                    field: 'id'},
                    {headerText: 'Employee Name',
                    field: 'name'}]}">
  </table>
<!-- /ko -->  

Notice the haveEmployees ViewModel observable? There are a couple of ways to implement that observable, but my favorite way is through a knockout pureComputed like this:

self.haveEmployees = ko.pureComputed(function() {
  return (self.employees().length > 0);
});

Now when the fakeJAX success handler populates the observableArray, the hasEmployees pureComputed will automatically flip from false to true and the table will suddenly appear in the DOM. It is at this moment that knockout will bind observables to attributes. Since binding happens after we have data, this may seem like the easiest approach. In my opinion, however, this should be the last option chosen because it hides a portion of the user interface until the Ajax response.

I put together a jsFiddle showing the last two approaches named Async Data Binding.

Interested in learning more? Here are a couple of Oracle JET OTN Discussion Forum threads discussing this very topic:

Special thank you to @JB, Oracle JET product manager for chiming in on those threads

No comments:

Post a Comment