Tuesday, April 19, 2016

Extending Oracle JET Components through Custom Bindings (Input Text with Buttons)

The other day I saw an interesting question on an Oracle JET forum: "Can you add a clear button inside the end of an ojInputText?" Think of this like the magnifying glass at the end of a search field or an X that allows you to delete all of the text within an ojInputText. My first thought was to use a little CSS to add a FontAwesome icon to the end of the data entry field. If we were using a plain HTML input element, this would be no small task because it is impossible to use CSS alone to add an icon to the end of an input element (maybe someday HTML and CSS will support before and after selectors for input elements?). ojInputText, however, already wraps input elements in an outer div so we just need to add a little CSS to style that outer div. Here is an example that just uses CSS styling

You see a problem with this solution? I didn't at first. From a visual perspective, it meets all of the requirements—oh, except it isn't a button. If all you want is a visual indicator/icon within ojInputText, then this is a small, tidy solution will suffice. If you actually wanted a button, then keep reading.

My colleague Paul Thaden reworked my example for click events:

Notice this example replaces the :after pseudo selector with jQuery append. This allows us to handle the icon's click events. This is a great, simple solution if you need to handle click events AND know when elements will exist in the DOM (so you can wire up event handlers, enhance the markup, etc). But what about those times when elements are created and destroyed through iterators or View/Component changes? What we really need is a way to manage the component's lifecycle so we can enhance and wire up event handlers on creation. Knockout has a mechanism for this called Custom Binding Handlers.

Have you noticed the $(".selector").ojFoo syntax in a lot of the Oracle JET JSDocs? That looks a lot like the jQuery UI syntax (because it is—thank you JB for confirming, see this video). If Oracle JET components are a lot like jQuery UI widgets, then we are in luck. The internet is littered with examples of people creating custom binding handlers for jQuery UI components. Here is a great example that creates a custom binding handler for the jQuery UI datepicker. All we need to do is follow that example, replacing datepicker with ojInputText. In other words, we can extend any Oracle JET component binding by pretending it doesn't have bindings and treating it like a jQuery plugin. Here is a jsFiddle showing examples of two-way data binding and data model creation/destruction, etc:

Too much clutter? Just want to see the ojInputText extension? Here is the HTML

<input id="text-input" 
       type="text"
       data-bind="audioInputText: value,
                  ojInputTextOptions: {rootAttributes:
                                        {class: 'audio-ojInputText'}}"/>

And the JavaScript

ko.bindingHandlers.audioInputText = {
    // setup extension to ojInputText as well as register event handlers
    init: function(element, valueAccessor, allBindingsAccessor, ctx) {
      var options = allBindingsAccessor().ojInputTextOptions || {};
        
      $(element)
        .ojInputText(options)
        .on({
          'ojoptionchange': function (event, data) {
            // use option === "value" for final value
            // use option === "rawValue" for each character
            if(data.option === "value") {
              var observable = valueAccessor();
              observable($(element).ojInputText("option", "value"));
            }
          }
        })
        .closest(".audio-ojInputText")
          .append('')
          .find("i")
            .click(function() {
              var msg = "This could activate the microphone... but it doesn't. " +
                "Hey, I noticed you entered '" +
                ko.utils.unwrapObservable(valueAccessor()) + "'"
              alert(msg);
            });

      //handle disposal (if KO removes by the template binding)
      ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
        $(element).ojInputText("destroy");
        console.log("ojInputText destroyed");
      });
        
    },
    // This is how we update the UI based on observable changes
    update: function(element, valueAccessor) {
      var value = ko.utils.unwrapObservable(valueAccessor());
      $(element).ojInputText("option", "value", value);
    }
  };

Here is an example of an ojInputText with a delete button that deletes all of the text within the observable when clicked.

In each of the examples above, I hard coded the click handler and the icon. In prior examples, the click handler used model data, making it somewhat generic, but not generic enough to delegate clicks to the ViewModel. Let's create one final example that we'll call ojInputText Clear Buttons. This example is generic enough to use any icon library (glyphicons, fontawesome, oj icons, etc) and invokes a click handler within the ViewModel.

8 comments:

  1. Jim - Great blog! I only just stumbled onto this site. Wow! There is already a lot of material on JET for me to catch up on. Thanks for the posts!

    ReplyDelete
  2. An Excellent post. Can you let us know how to use textInput instead of value binding?

    ReplyDelete
    Replies
    1. I put together another jsFiddle showing rawValue, which is what ojInputText uses instead of textInput: jsFiddle: Realtime ojInputText w/Delete Button

      Delete
  3. Yes, rawValue can be used as an alternative to textInput, thanks for putting up a fiddle...

    ReplyDelete
  4. Based on your information on rawValue, I have created my first blog post on oracle jet... small but something I found I needed sometime back. I will certainly do whenever I find some useful info... Keep rocking.

    Here is my blog post on it.
    http://controlspace.info/2016/04/restricting-ojinputtext-for-numericals

    ReplyDelete
    Replies
    1. Welcome to the Oracle JET blogging community! Thank you for contributing.

      Delete
  5. In the first demo you are hardcoding border: 1px solid #dfe4e7; background-color: #fcfdfe;. If the theme changes the component's border will not be in sync with the others. Or am I missing something?

    ReplyDelete
    Replies
    1. Explain what you mean by "theme changes" and "hard coded." If you are referring to the use of color and width values instead of SCSS variables, that is true. My focus for the example was JavaScript, not CSS. My intent was to keep the example as simple as possible, but I can see your point. The CSS is supposed to move the Oracle JET border from the input element to the root element. For a "real" solution, you would want to import _oj.alta.variables.scss and use $border4Color so that the root element border matches everything else in Oracle JET. I will put together some examples using the Oracle JET SCSS as well. Thank you.

      Delete