Wednesday, July 20, 2016

Using jQuery Interactions with Oracle JET

Draggable, droppable, resizable, selectable, and sortable are jQuery UI interactions that you may want to use when building custom Oracle JET applications. The question is, "How?" Here are three steps:

  1. Find the jQuery UI interaction plugin file,
  2. Import the interaction file into a module (declare it as a dependency), and
  3. Attach the interaction to a component.

Caveat: even though jQuery UI is one of Oracle JET's dependencies, Oracle JET uses only a small subset of the jQuery UI library. With that in mind, this post will describe how to use draggable and droppable with Oracle JET. Just be careful when using jQuery UI interactions with Oracle JET components because JET components weren't designed to be manipulated in this manner. Nevertheless, jQuery UI's interactions are so polished, it is worth a try.

Identify the jQuery UI Interaction File

This sounds like the easy part. Since jQuery UI is one of Oracle JET's dependencies, bower will install it automatically. Depending on how you installed Oracle JET, however, finding jQuery UI files may be difficult (see this thread). If you add Oracle JET to your project through the Bower command (bower install oraclejet --save), Bower will download all of jQuery UI and you will find each jQuery UI component inside your bower components folder (usually bower_components). Since the Oracle JET Yeoman generator also uses bower, the generator will also download all of jQuery UI into your bower components folder. But here is where it gets a little tricky. After Bower finishes, the Yeoman generator will move Oracle JET's real dependencies into /js/libs. A quick review of the files in /js/libs show that certain files, such as droppable, are missing. They are still in the bower_components folder, just not in the jQuery UI location identified in the Oracle JET RequireJS configuration. For Yeoman template users, this can be a little problematic since the RequireJS configuration already contains a declaration for jQuery UI, but that declaration doesn't include every jQuery UI file. For others that are using Bower directly, this is a non-issue. Your Oracle JET RequireJS configuration already points to jQuery UI and that location contains ALL jQuery UI files including source, minified, and combined files. So for bower users, "carry on." Yeoman users will want to copy the appropriate interaction file into the jQuery UI folder identified by the RequireJS configuration.

Import the Interaction File into a Module

Whether your ViewModels are pure knockout components or ojModules, chances are high you are using AMD-style modules through RequireJS. We need to tell RequireJS how to find our interaction plugin. jQuery UI interaction plugins are jQuery plugins, so we will want to import jQuery as well. Here is a sample AMD define block that references draggable and droppable from the standard Oracle JET RequireJS configuration location:

define([
  'jquery',
  'jqueryui-amd/draggable',
  'jqueryui-amd/droppable'
], function($) {

Attach the Interaction to a Component

Let's say we have a list of DOM elements that are supposed to be draggable and a couple more DOM elements that are supposed to be drop targets (droppable). Both draggable and droppable elements need to be enhanced by jQuery UI. Through CSS class selectors, etc, identifying those DOM elements should be trivial. In the asynchronous world of Knockout and Single Page Applications (SPA), the hard part is knowing when those DOM elements will exist. Can't we use $(document).ready? Maybe, but don't count on it. In an SPA, $(document).ready may have fired several views ago. Furthermore, the draggable elements may come from an asynchronous service, which means they may not exist at page load. Instead, we need DOM elements with lifecycle management events. For each enhanced DOM node, we need to know when that DOM node is available. Knockout gives us lifecycle management capability through custom bindings. I have written about ko.bindingHandlers on many occasions. What makes this example different is that it is very simple. Whereas others required init and update handlers, these interactions require only an init handler. Here are example ko.bindingHandlers implementations for draggable and droppable::

ko.bindingHandlers.jmDraggable = {
  init: function(element) {
    $(element)
      .draggable({
        revert: 'invalid',
        helper: "clone"
      });
  }
};

ko.bindingHandlers.jmDroppable = {
  init: function(element) {
    $(element)
      .droppable({
        drop: function(event, ui) {
          $(ui.draggable)
            .detach()
            .css({
              top: 0,
              left: 0
            })
            .appendTo($(this));
        }
      });
  }
};

When implementing a custom binding handler, you are creating your own custom knockout binding. You want to make sure your binding names don't collide with other binding names. With that in mind, I used the jm prefix to distinguish my bindings from other people's bindings. I would then use these bindings in HTML that looked something like this:

<div id="panelPage">
  <div class="oj-flex drag-items draggables oj-margin oj-padding"
       data-bind="jmDroppable, foreach: {data: labels, as: 'label'}">
    <div class="oj-panel oj-margin" data-bind="jmDraggable, css: label">
        <span data-bind="text: label"></span>
    </div>
    
  </div>
  
  <div class="drop-target oj-margin oj-padding"
       data-bind="jmDroppable">
    <span>Drop items here</span>
  </div>
  
</div>

Here is a working example that makes oj-panel draggable:

No comments:

Post a Comment