Showing posts with label ojSelect. Show all posts
Showing posts with label ojSelect. Show all posts

Friday, May 27, 2016

Removing an Oracle JET Component from the Tab Order

A customer recently asked me if I could create a "quick entry" form that excluded certain fields from the regular tab order. Oracle JET input components have a tab index for a reason. They are designed for accessibility which means they work with accessible input devices and don't require a mouse. Removing data entry fields from the tab order means I have to use a pointing device, such as a mouse or tap, to select a skipped field. Although this goes against my better judgement, I understand the use case. If you are entering information in rapid, data entry mode, you don't want to tab over fields you rarely use. The issue I ran into is this:

How do you remove the tab index from certain Oracle JET components?

Input elements such as ojInputDate display a native HTML element and will automatically be part of the tab order. We can easily remove these elements from the tab order by setting the tabIndex to -1 like this:

<input tabindex="-1" data-bind="ojComponent: {
                    component: 'ojInputDate', value: selectedDate}" />

Other elements, such as ojSelect, use a clever collection of non-input HTML elements to capture input. For accessibility reasons, Oracle JET adds tabindex="0" to these input elements to ensure accessible devices can enter data. Since many of these Oracle JET elements begin life as a real HTML input element (input, select, etc), one might think that changing the tab index would be as easy as setting the tab index on the original input element. Unfortunately, it is not that easy. Another idea is to include tabIndex in the rootAttributes collection of an OJ component. Unfortunately, that doesn't work either. To remove these elements from the tab order, we must first identify the element with a tab index, and then change the value of the tab index. To do this effectively, we have to answer two questions:

  1. How do I identify the element with a tabIndex attribute?
  2. Timing wise, when will the DOM be ready for me to change the tabIndex?

Let's start with question #1. Study the following structure screenshot for a moment.

In the above screenshot, the JET generated oj-select is highlighted at the top. The very next element, the oj-select-choice element is the element with the tabIndex and is a child element of the oj-select. This is the element we want to modify and we need to find a way to programatically identify this element. Notice that the original select element is a sibling of the generated oj-select element. We could use a sibling selector. Sibling selectors are great for collections, where you want to style elements differently based on their position in a collection, but a sibling selector makes me a little nervous in this instance. I'm not sure we can depend on the sibling relationship identified in this screenshot. Rather than use a sibling selector, we can use Oracle JET's getNodeBySubId method. For Oracle JET components that are composed of several elements, we can identify individual pieces of the component by Sub ID. You can find the list of valid ojSelect Sub IDs here. We specifically want to select the oj-select-choice node. Unfortunately, oj-select-choice is not a node with a known Sub ID. If I expanded the oj-select-choice node in the structure screenshot, you would see that oj-select-chosen is a direct descendant of oj-select-choice. The oj-select-chosen node is selectable by Sub ID. With a little jQuery, we can easily traverse from oj-select-chosen up to oj-select-choice. We can test this by selecting our select element in the structure browser and then entering the following into the console:

$($0)
  .ojSelect( "getNodeBySubId", {'subId': 'oj-select-chosen'} )
  .closest( ".oj-select-choice" )

The following screenshot shows the results of that command (notice I also expanded the oj-select-choice to reveal the child oj-select-chosen).

A quick word about jQuery traversal methods... Using jQuery, there are often multiple ways to solve the same problem. In this situation we could either use the parents() or closest() methods to work our way up the hierarchy. Parents and Closest are similar, but, as the docs say, "The differences between the two, though subtle, are significant." We want to find the most immediate ancestor matching a selector. Closest accomplishes this. It identifies the match and then stops. The parents method, on the other hand, finds all matches and returns them as a collection. We have no need to walk the entire document hierarchy, so closest is the appropriate choice for this scenario. We have now answered question #1: How do I identify the element with a tabIndex attribute?

Next we need to handle the Life Cycle Management issue: How do we know when the ojSelect is available in the DOM (question #2)? Even though ojModule has Life Cycle Management events that tell us when the DOM is available, I don't suggest using them for this specific scenario. ojModule LCM events will work just fine with DOM elements that are hard coded into the view, but I'm not sure we should count on them for dynamically generated elements, such as those bound to an ko.observableArray. A better approach is to use a component-specific life cycle management approach such as custom bindings (ko.bindingHandlers). This allows us to manipulate an element as soon as it appears in the DOM. Here is an example ko.bindingHandler:

ko.bindingHandlers.inaccessibleOjSelect = {
  init: function(element, valueAccessor, allBindingsAccessor, ctx) {
    var options = allBindingsAccessor().ojSelectOptions || {};
    var multiple = !!options.multiple;
    var tabEl = $(element)
      // initialize ojSelect
      .ojSelect(options)
      // bind value change handler
      .on({
          'ojoptionchange': function (event, data) {
            if (data.option === "value") {
              var observable = valueAccessor();
              if (ko.isObservable(observable)) {
                if (multiple) {
                  observable(data.value);
                } else {
                  // unwrap from array if single select
                  observable(data.value[0]);
                }
              } // if not observable, just throw away the value
            }
          }
        })
      // get reference to item with tabIndex
      .ojSelect( "getNodeBySubId", {'subId': 'oj-select-chosen'})
      .closest(".oj-select-choice");

    $(tabEl).attr("tabIndex", -1);

  },
  update: function(element, valueAccessor) {
    var value = ko.utils.unwrapObservable(valueAccessor());
    $(element).ojSelect("option", "value", [value]);
  }
};

Note: The ojSelect value expects an array. In this JavaScript, notice that I distinguish between multi and single selection modes. For single-select ojSelect elements, I am unwrapping the value array and just returning the single value. This is for convenience and is in lieu of my other method for unwrapping ojSelect values.

You would use this with HTML similar to the following:

<select id="basicSelect" data-bind="inaccessibleOjSelect: browser,
                      ojSelectOptions: {optionChange: browserChangedHandler,
                           rootAttributes: {style:'max-width:20em'}}">
  <option value="IE">Internet Explorer
  <option value="FF">Firefox
  <option value="CH">Chrome
  <option value="OP">Opera
  <option value="SA">Safari
</select>

And finally, here is the jsFiddle so you can test it out:

As you review the code, you will notice that it is very ojSelect specific. As I mentioned before, not every Oracle JET input component requires a custom handler to change the tabIndex. For those that do, however, it wouldn't take much to rework this custom handler into something generic that could be used with a variety of ojComponent elements.

Wednesday, March 16, 2016

Synchronizing ojSelect Dependent Value List Observables

Dependent value lists are lists of values that change based on some other field. Country and state are common examples. If Country has no value, then the list of states (or provinces, territories, etc) should be empty. After selecting a country, the options within state should change to match relevant options for that country. Knockout computeds offer a great mechanism for maintaining dependent lists. Here are a couple of good examples of using computeds to maintain dependencies:

But what happens when the dependent list's selected value becomes invalid after changing the underlying list of options? For example, what value should the "state" field contain when the country changes, rendering the current "state" selection invalid? What about the data model? Should the underlying observable share the same value as what is shown on the screen? When the options list of a select changes and the model's value is not in the list of options (an invalid value), the default Knockout behavior is to update the model to contain the first (selected) option in the list (see valueAllowUnset). This may or may NOT be the right approach, which is why knockout allows us to change its behavior through the valueAllowUnset parameter. Oracle JET's ojSelect takes the opposite approach. When the options list changes, invalidating the selected option, ojSelect does NOT write back to the model. While this may be desirable (as shown in the valueAllowUnset parameter), it may lead to a situation where the display on the screen does not match the underlying data model. In the following recording, notice that the country starts as Canada and the State is Newfoundland. After switching to Country: United States, the state switches to Alabama. This seems reasonable because Canada does not contain the state Alabama and the United States does not contain a state named Newfoundland. What isn't obvious by this recording, however, is that the change to state doesn't affect the bound observable.

One method to keep the screen and the data model in sync is to subscribe to the optionChange event. When the options list changes, and the selected option is not in the list, ojSelect will trigger the optionChange event (because the selected option changed), but not write back to the data model. Your subscription handler can choose to update the data model observable with the newly selected option. Here is some sample HTML showing the optionChange attribute:

<select id="state" data-bind="ojComponent: {component: 'ojSelect',
    options: stateList, value: stateSelected,
    placeholder: '', optionChange: stateOptionChangedHandler}" required></select>

... and the stateOptionChangeHandler JavaScript:

self.stateOptionChangedHandler = function(event, data) {
  if (data.option === "value") {
    var value = data.value[0];
    var observable = self.selectedState;

    // only set if the option value change didn't update the observable
    // we want the underlying data to match the screen
    if (value !== observable()) {
      console.log("setting value from options handler", value, observable());
      observable(value);
    }

  }
};

Now replay the recording above. Notice the output in the console window? The recording above uses the optionChange handler presented here to write back to the observable when the option changes by some mechanism other than the user actually selecting a new value. What you see printed in the console window is the new value (Alabama) followed by the old observable value (Newfoundland and Labrador).

Chances are you will have multiple dependent value lists. Who wants to repeat that code for every list? Here is my generic library function:

var valueOptionChangeHandler = function(observable, event, data) {
  if (data.option === "value") {
    var value = data.value[0];

    // only set if the option value change didn't update the observable
    // we want the underlying data to match the screen
    if (value !== observable()) {
      console.log("setting value from options handler", value, observable());
      observable(value);
    }

  }
};

I can then "curry" an observable into a new function that I use as my optionChange handler like this:

self.stateOptionChangedHandler = ojsHelper.valueOptionChangeHandler
  .bind(undefined, self.selectedState);

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);
//...