Jeff Holoman’s Blog

Mostly Oracle, APEX and other stuff

jsTree and APEX

Posted by jholoman on January 9, 2010

I’ve had this post sitting here as a draft since August 4th, but with the arrival of our first son back in June, Ive been a little slow to blog. I was checking out the new early Adopters version of Apex 4.0 and saw the cool tree interface for the app builder. It reminded me of this post. Another reason for not posting was that a new version had been released for jstree with some changes, and I hadn’t had time to make the necessary revisions.

I’ve never really used the built in APEX trees, but I had a need to display some hierarchical data and wanted some added functionality over what the built in apex trees provide. I went out looking for interesting JQuery plugins. The one I settled upon was JSTree . I like that it has a customizable context menu among other things. Well this is just an example and can be made more generic…I’ve hard-coded a couple of things for the purposes of this demo but this should get you started. Here’s the demo: http://tryapexnow.com/apex/f?p=9155:2

For this example I am organizing different types of samples in a case. Here are the tables.

SQL> desc sample_types;
  ID                                        NOT NULL NUMBER(38)
 DESCRIPTION                                        VARCHAR2(50)
 ...
 TREE_IMAGE                                         VARCHAR2(250)

SQL> desc samples;

ID                                        NOT NULL NUMBER(38)
CASE_ID                                   NOT NULL NUMBER(38)
SAMPLE_TYPE_ID                            NOT NULL NUMBER(38)
...
PARENT_ID                                          NUMBER(38)

SQL> SELECT s.id, s.parent_id, level
      FROM samples s
      WHERE case_id = :CASE_ID
      START WITH parent_id = 0
      CONNECT BY PRIOR s.id = s.parent_id;

        ID  PARENT_ID      LEVEL
---------- ---------- ----------
      1067          0          1
      1068       1067          2
      1076       1068          3
      1071       1067          2
      1077       1071          3
      1078       1077          4
      1073       1067          2
      1072          0          1

8 rows selected.

Note that I made all parent samples have a parent_id of zero.

The author of JSTree Ivan Bozhanov has done a great job with providing both documentation and examples of how to setup the tree component.

Among the choices of data source, I chose to implement the source as a JSON object, and I wrote a function to output my connect by query in the format that jsTree expects.

create or replace function get_samples_tree(p_case_id_in cases.id%TYPE) RETURN VARCHAR2 AS
      l_json         VARCHAR2(32767);
      l_loop_counter NUMBER(5);
   BEGIN
      l_json := '[';
      FOR i IN (SELECT rownum,
                       s.id,
                       s.parent_id,
                       LEVEL,
                       lead(LEVEL, 1, 0) over(ORDER BY rownum) lead,
                       st.tree_image,
                       st.id type_id
                  FROM samples s, sample_types st
                 WHERE case_id = p_case_id_in
                   AND s.sample_type_id = st.id
                 START WITH parent_id = 0
                CONNECT BY PRIOR s.id = s.parent_id)
      LOOP
         l_json := l_json || '{ "attributes": ';
         l_json := l_json || '{ "id" : ';
         l_json := l_json || '"stree_' || i.id || '"';
         l_json := l_json || ', "rel" : ';
         l_json := l_json || '"' || i.type_id || '"';
         l_json := l_json || '}, ';
         l_json := l_json || '"data":{ "title" : "';
         l_json := l_json || i.id || '", "icon" : "/i/' || i.tree_image ||
                   '", "attributes" : { "href" : "f?p=' || v('APP_ID') ||
                   ':3:' || v('APP_SESSION') ||
                   '::NO::P3_SAMPLE_ID:'|| i.id ||
                   '" }}';
         --  l_loop_counter := i.LEVEL - i.lead;

         IF i.lead > i.LEVEL
         THEN
            l_json := l_json || ', "children": [ ';
         ELSE
            l_json := l_json || '},';
         END IF;
         IF i.lead < i.LEVEL
         THEN
            l_loop_counter := i.LEVEL - i.lead;
            FOR j IN 1 .. l_loop_counter
            LOOP
               l_json := TRIM(trailing ',' FROM l_json);
               l_json := l_json || ' ] ';
               IF (i.lead <> 0 OR j <> l_loop_counter)
               THEN
                  l_json := l_json || '},';
               END IF;
            END LOOP;
         END IF;
      END LOOP;

      RETURN l_json;
   EXCEPTION
      WHEN OTHERS THEN
         RAISE;
   END get_samples_tree;

The on-demand process is straight-forward enough:

declare
l_tree varchar2(32767);
begin
l_tree := get_samples_tree(wwv_flow.g_x01);
htp.prn(l_tree);
end;

jsTree is quite flexible. In the javascript below, I’m using asynchronous json to get the data. In order to pass the values to the URL in the data.opts section, you use the callback “beforedata”

function populateSamplesTree(pCaseId, pRegionId) {

jQuery("#"+pRegionId).tree({
    data  : {
    type  : "json",
    async : true,
    opts  : {
       method: "POST" ,
url:"wwv_flow.show"
	    }
	 },
  callback : {
  onchange : function (NODE,TREE_OBJ) {
  document.location.href = $(NODE).children("a:eq(0)").attr("href"); },
  beforedata : function(NODE, TREE_OBJ) {
       return {
        p_flow_id:jQuery('#pFlowId').val(),
        p_flow_step_id:jQuery('#pFlowStepId').val(),
        p_instance:jQuery('#pInstance').val(),
        x01:$v(pCaseId),
        p_request:"APPLICATION_PROCESS=GET_SAMPLE_TREE" }
     }
  }
});
//jQuery.tree.reference( pRegionId).open_all();
}

Another way to accomplish the same thing would be to use a “standard” on demand call for APEX, and parse the result as a json object. In order to do that you need the json javascript library from http://www.json.org/

function populateSamplesTree2(pCaseId, pRegionId) {
var get = new htmldb_Get(null,$v('pFlowId'), 'APPLICATION_PROCESS=GET_SAMPLE_TREE', 0);
 get.addParam('x01', $v(pCaseId));
gReturn = get.get();
var jsonobj = JSON.parse(gReturn);
apex.jQuery("#"+pRegionId).tree({
  data  : {
    type  : "json",
    opts  : {
      static :   jsonobj }
  },
  callback : {
  onchange : function (NODE,TREE_OBJ) {
  document.location.href = $(NODE).children("a:eq(0)").attr("href");
                               }
                          }

});
}

I could store the context menu config in the database as well and generate the JSON for it, but as this is just for example, I’ve left that out.  You can easily do dynamic context menus. I’ve included the type in the tree and you can see I’ve hardcoded in Sample Type 1 in the menu option for “split”.

function populateSamplesTree3(pCaseId, pRegionId) {
var get = new htmldb_Get(null,$v('pFlowId'), 'APPLICATION_PROCESS=GET_SAMPLE_TREE', 0);
 get.addParam('x01', $v(pCaseId));
gReturn = get.get();
var jsonobj = JSON.parse(gReturn);
apex.jQuery("#"+pRegionId).tree({
  data : {
    type : "json",
    opts : {
        static : jsonobj
    }
},
callback : {
    onchange : function (NODE,
    TREE_OBJ) {
        document.location.href = $(NODE).children("a:eq(0)").attr("href");
    }
},
plugins : {
    contextmenu : {
        items : {
            create : false,
            rename : false,
            remove : false,
            split : {
                label : "Split",
                icon : "",
                visible : function (NODE,
                TREE_OBJ) {
                    if (TREE_OBJ.get_type(NODE) != "1") {
                        return -1
                    } ;
                },
                action : function (NODE,
                TREE_OBJ) {
                    // Just a demo of the arguments
alert('"' + NODE.children("a").text() + '" from "' + TREE_OBJ.container.attr("id") + '"');
                }
            },
            type_2 : {
                label : "Another Type",
                icon : "",
                visible : true,
                action : function (NODE,
                TREE_OBJ) {
                    // Just a demo of the arguments
alert('"' + NODE.children("a").text() + '" from "' + TREE_OBJ.container.attr("id") + '"');
                }
            }
        }
    }
}
});
}

Now I can just create the regions on the page with source of

<div id="samples_tree"></div>

<div id="samples_tree2"></div>

<div id="samples_tree3"><div>

Now go time:

<script type="text/javascript">
apex.jQuery(document).ready(function(){
   populateSamplesTree('P2_CASE_ID', 'samples_tree');
   populateSamplesTree2('P2_CASE_ID', 'samples_tree2');
populateSamplesTree3('P2_CASE_ID', 'samples_tree3');
});
</script>

I’ve left out icons, which are easy to add, and a host of other things. If I ever get some more time I’d like to make this more generic and reusable.

Check out a demo here:
http://tryapexnow.com/apex/f?p=9155:2

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

 
Follow

Get every new post delivered to your Inbox.