Thursday, March 3, 2016

Unwrapping Oracle JET's ojSelect value binding

The ojSelect component is a very powerful alternative to the HTML <select> element. It has a long list of impressive features including a type-ahead search box (for long lists), pill-like multi-select, and the ability to include images in the options list. If you are building web applications connected to Oracle applications (like me), then you can't help but appreciate the prepackaged Alta skin as well.

One of the issues I struggled with when switching from the traditional HTML <select> element to ojSelect was the value binding. The single <select> element returns a single value (or whatever the selected row in the option binding represents) whereas ojSelect single select returns an array. Even though the ojSelect value array has just one element, it is still an array. If my data model doesn't expect an array, then this can cause problems when binding the data model to an ojSelect in the view layer. Here are a couple of options I have used to work around the ojSelect array value:

  1. Bind to a temporary observable within the ViewModel and then marshal content from that temporary observable into the data model on save.
  2. Use a read/write computed observable to maintain state between the ViewModel and the Model.

One reason for using a 2-way data binding architecture, such as knockout, is so I don't have to copy values between the view and the model, so option #1 is not a favorite of mine. Option #2 is similar in that it uses a temporary observable in the ViewModel, but it is a little different in that I don't have to specifically transfer data between the Model and the ViewModel. Rather, it is more like connecting some plumbing and letting knockout stream data between the two. Here is what that might look like:

require(['ojs/ojcore', 'knockout', 'jquery', 'ojs/ojknockout',
  'ojs/ojselectcombobox'
], function(oj, ko, $) {
  // make ko accessible to the console for ko.dataFor($0) inspection
  window.ko = ko;

  $(document).ready(
    function() {
      var data = {
        browser: ko.observable()
      };

      function ValueModel() {
        var self = this;

        // expose data to the view so we can bind other hypothetical values
        self.data = data;
        self.val = ko.pureComputed({
          read: function() {
            var val = self.data.browser();

            // 'required' validation doesn't work if the value is [undefined].
            // it only identifies empty as undefined (no array), so this
            // function doesn't wrap in array syntax if the value is undefined
            if (val === undefined) {
              return undefined;
            } else {
              return [val];
            }
          },
          write: function(value) {
            if (!!value) {
              self.data.browser(value[0]);
            }
          }
        });
      }
      ko.applyBindings(new ValueModel(), document.getElementById('form1'));
    }
  );
});

Note: This code fragment was specifically written for testing in the Oracle JET Cookbook. You can test it by pasting the fragment into the JavaScript block of the Oracle JET Cookbook ojSelect recipe page. After pasting, click the "Apply Changes" button. Select a value from the ojSelect list and notice the cookbook example still displays the observable with array notation. This is because the ViewModel is bound to the pureComputed observable, which returns an array. The underlying data model, however, contains the raw, unwrapped value. You can see the value stored in the data model by:

  • Right-clicking the ojSelect or "Current selected value..." paragraph and choosing "Inspect" from the context menu.
  • Switch to the console window and type ko.dataFor($0).data.browser()

This should display the unwrapped observable value without array notation.

I use this pureComputed wrapper for each of my ojSelect single-value select lists. Rather than replicate that code for every single ojSelect, I have a RequireJS module that exposes a method I can then use to create these computedObservables. Here is what that module contains:

define(["knockout"], function(ko) {
  'use strict';

  var wrapObservable = function(observable) {
    return ko.pureComputed({
      read: function() {
        var val = observable();

        // 'required' validation doesn't work if the value is [undefined].
        // it only identifies empty as undefined (no array), so this function
        // doesn't wrap in array syntax if the value is undefined
        if (val === undefined) {
          return undefined;
        } else {
          return [val];
        }
      },
      write: function(value) {
        if (!!value) {
          observable(value[0]);
        }
      }
    });
  };

  return {
    // ojSelect expects array values, so this method wraps single values in
    // array syntax
    wrapObservableForOJSelect: wrapObservable,
  };
});

I can then create ViewModel computeds using the following:

self.browser = ojsHelper.wrapObservableForOJSelect(data.browser);
self.os = ojsHelper.wrapObservableForOJSelect(data.os);
//...

No comments:

Post a Comment