Press "Enter" to skip to content

APEX Navigation Menu Experiments Part 2

Last updated on Wednesday, April 1, 2020

This is part 2 in a series of articles exploring navigation techniques in Oracle Application Express. Check out part 1 first if you missed it. Here I will show how to dynamically fetch new child nodes in the navigation tree on-demand.

The sample app for part 2 was created with the upcoming 20.1 APEX release but the techniques should work on previous releases. On apex.oracle.com there is currently a pre-release of 20.1 where you can try out the app (enter any username). You can also download the app and import it into your own workspace on apex.oracle.com or your own 20.1 instance once available.

Image of sample app ExpNav2

This second experiment was motivated by a customer that has many thousands of navigation links in their side navigation menu. This seems like an excessive number but who am I to judge. Generating all these navigation links for every page view was slowing their app down. The treeView widget, which is used by the side navigation menu, is capable of loading sub tree nodes on-demand when a parent node is expanded. However, I don’t think there is any example of this in the APEX builder or among any of the APEX sample or productivity apps. I have mentioned this dynamic fetching of nodes on the APEX forums before and there is a rough example in the API documentation.

Loading only the tree nodes that are needed when they are needed seemed like a good way to improve performance in this case. This requires customizing the code to initialize the treeView widget.

How do you get access to the tree as it is created? You can’t. Unlike the Tree region that has a JavaScript Initialization Code attribute that lets you customize how the treeView widget is initialized the side navigation tree is initialized in theme42.js with no chance to customize the initialization.

This means you can either create a new Side Navigation Menu list template to replace the Universal Theme (UT) tree and loose all the toggle behavior (remember from part 1 that the tree initialization and toggle behavior are deeply intertwined) or use the existing UT tree but update it after the page loads.

The second option is easier and it is the one I choose. Before continuing down this path it is worth taking a look at what functionality you get out-of-the-box with the UT side navigation menu and its implementation in theme42.js:

  • Toggle side column: template options to control collapse mode hidden or icon. Collapse by default option.
  • Responsive behavior for side column; when to show based on screen size.
  • Some template option styles: A/B/Classic.
  • Integration with apex.actions (new and not fully realized yet)
  • Is current class added to all parents; is-current, is-current–top
  • Default icon fa-file-o for (top level only) and badge display from label text. Both of these use DOM manipulation (this DOM manipulation is not a good idea and should be done with a custom node renderer).
  • Toggle on expand, expands the selection.
  • Layout change event triggered on the tree element.
  • Expand side bar if tree expanded.

Seeing all this functionality that I would be giving up, or having to re-implement, if I replaced the Side Navigation Menu template with my own reinforces my decision to update the tree after it is initially created and rendered. The down side is that it is a little inefficient because the tree gets rendered when the page loads and then you immediately tell it to refresh which causes it to render again.

My general plan was to have an application process that can return sub tree nodes in JSON format. The top level nodes would come from a normal static navigation menu list. The treeView’s treeNodeAdapter would be modified to fetch new sub trees from the server using ajax. A key aspect of this plan is a convention for the tree node ids so that the client knows which nodes need to have their children fetched on-demand.

I choose a heterogeneous tree with different sub trees coming from different tables. I think this is the more realistic case and it makes the solution more general. It also meant I could get a bigger tree using sample data sets (still well under even a thousand nodes). The node ids need to indicate what its “type” is to determine what table(s) to query for the sub trees. In some cases you may want to return one level at a time and in other cases you may want to get a whole sub tree no matter how deep. I choose to include examples of both cases.

As you may know by now the treeView widget uses model view separation. The widget is the view and it interacts with the tree model data via an adapter interface called treeNodeAdapter. A benefit of this design is that the model can hold all the data but it is not rendered in the DOM until it is needed; when the parent node expands. But it is also possible for the model to fetch data asynchronously when it is needed. The treeView uses the adapter’s fetchChildNodes method as needed to let the model load child nodes on-demand when a parent is expanded. However the default adapter does not implement this method and does not support on-demand loading of nodes. You can create your own adapter from scratch but there is no need because the default one does so much and is so close to what you want. You just have to replace 2 methods and add 1.

All the client side code for this example is in application file app2.js. I’m just showing snippets here. You can download the app to see the whole thing. The following code is what is needed to turn the default adapter into one that can handle on-demand loading.


adapter = $.apex.treeView.makeDefaultNodeAdapter( treeData, types, true );
...
// enhance the adapter to fetch nodes aysnc
adapter.childCount = function( n ) {
    if ( n.children === null ) {
        return n.children; // null means we don't yet know
    }
    return n.children ? n.children.length : 0;
};
adapter.hasChildren = function( n ) {
    if ( n.children === false || n.children === null ) {
        return n.children; // false means no, null means we don't yet know
    }
    return n.children ? n.children.length > 0 : false;
};
adapter.fetchChildNodes = function( n, cb ) {
    var p;
    n.children = [];
    p = apex.server.process(navProc, {
        x01: n.id // pass in the parent node id so the process knows what sub tree to return
    });
    p.done(function(data) {
        if (data.nodes) {
            n.children = data.nodes;
            traverse(n, n._parent);
        }
        cb( n.children.length > 0 ? true : 0 );
    }).fail(function() {
        cb( false );
    });
};

The above code is rather generic. It just needs to know what APEX process to call in apex.server.process and the initial data and optional types to pass into makeDefaultNodeAdapter. In this case the initial data comes from the existing nav tree. See the full code for details.

The protocol between the client and server used here is that the parent node id is in the X01 parameter and the returned JSON nodes are in a property called nodes. The nodes returned from the server need to have the structure defined by defaultNode used by the default adapter.

The traverse function visits all the nodes in the sub tree starting with node n and sets the _parent property and sets the children property to null for any nodes to be lazy loaded.

Once the default adapter is modified you need to tell the treeView to use it:


    navTree$.treeView("option", "getNodeAdapter",
            function() { return adapter; })
        .treeView("option", "autoCollapse", false);

There is no need to refresh because that happens automatically when the getNodeAdapter option is set. I also set the autoCollapse to false, which is the default but the side navigation tree sets it to true. You can change this however you like and set other options as well.

For the node ids I used the following format: type:primary-key. If the children need to be lazy loaded then the id ends in “:*”. In this way the node ids convey what the type of the node is, which determines what kind of children it has, the primary key and an indicator to determine if the children are fetched on-demand. You could come up with other formats and conventions to suit your needs.

In the static Navigation Menu the list template attribute ID Attribute (A01) is used to give the tree node an id. For entries like the home page the id is “H:0”. In this case the exact id value doesn’t mater as long as it doesn’t end with “:*”. For the Departments entry I wanted to dynamically load the children so its id is “DS:*”. This client code in the traverse function sets the children property to null when the id ends with “:*”.


    if (n.id && n.id.match(/:\*$/) && n.children === undefined) {
        n.children = null; // set up to async load more children
    }

It must be done this way because when the tree is created from list markup there is no option to indicate nodes to lazy load. Next you will see how the server uses the type.

Here is part of the Ajax Callback Application Process that gets the node data based on the type.

...
    l_parts := apex_string.split(apex_application.g_x01, ':');
    if l_parts.count >= 2 then
        l_ntype := l_parts(1);
        l_id := l_parts(2);
    end if;
...
    case l_ntype
    when 'DS' then
        -- departments category has departments
        open rc for 
          select 1 lvl, 'D:'||deptno||':*' id, null parent_id, dname label, 'D' n_type, '' icon, 
              apex_page.get_url(
                  p_application => :APP_ID,
                  p_page => 4,
                  p_debug => :DEBUG,
                  p_items => 'P4_DEPTNO',
                  p_values => deptno) link
          from dept order by label;
        fetch rc bulk collect into l_nodes;
        close rc;
    when 'D' then
        -- a department has employees
        open rc for
          select 1 lvl, 'E:'||empno id, null parent_id, ename label, 'E' n_type, '' icon, 
              ... link
          from emp 
          where deptno = l_id
          order by label;
        fetch rc bulk collect into l_nodes;
        close rc;
...

You can see that the ids for department nodes will look like “D:20:*” for example. The ending “:*” lets the client know to fetch the employees for the given department.

The above code gathers the data but it still needs to be turned into a JSON tree structure. The following code does that. It seems to be working well but I recommend testing it a bit more to double check me.


    apex_json.open_object;
    apex_json.open_array('nodes');

    if l_nodes.count() > 0 then
        l_cur_level := 1;
        for i in 1..l_nodes.count()
        loop
            l_node := l_nodes(i);
            if l_node.lvl > l_cur_level then
                apex_json.open_array('children');
            elsif l_node.lvl < l_cur_level then
                apex_json.close_object;
                loop
                    apex_json.close_array;
                    apex_json.close_object;
                    l_cur_level := l_cur_level - 1;
                    exit when l_node.lvl >= l_cur_level;
                end loop;
            elsif i > 1 then
                apex_json.close_object;
            end if;
            l_cur_level := l_node.lvl;

            apex_json.open_object;
            apex_json.write('id', l_node.id);
            apex_json.write('label', l_node.label);
            apex_json.write('icon', l_node.icon);
            apex_json.write('type', l_node.n_type);
            apex_json.write('link', l_node.link);
        end loop;
        apex_json.close_object;
        -- close open node arrays
        if l_cur_level > 1 then
            loop
                apex_json.close_array;
                apex_json.close_object;
                l_cur_level := l_cur_level - 1;
                exit when l_cur_level <= 1;
            end loop;
        end if;
    end if;
    apex_json.close_array;
    apex_json.close_object;

At this point the client side and server side are working together to fetch new nodes on demand. However one serious issue is that when you navigate to a page you expect the tree node representing that page to be selected/highlighted. This means that all the parent nodes need to be expanded. The trouble is that for dynamically loaded nodes they don’t exist yet so there is nothing to select.

Check out the code in app2.js that remembers the path of node ids (example: “/DS:*/D:10:*/E:7782”) in browser session storage and then on page load step by step expands each level waiting for each async expansion to complete until finally the saved node can be selected.

Note that although this example is specific to the navigation tree the same techinque could be used with the Tree region.

The form pages that make up the app are not very complete. The focus of the example is on the navigation.

Because the nav tree is somewhat deep I used Theme Roller to make the Navigation Tree wider (300px).

With the EMP/DEPT and Projects Data sample data sets when all nodes are expanded there are about 276 nodes. The lazy loading on apex.oracle.com seems quick. I did not do any performance comparisons but my guess is that with this small number of nodes there wouldn’t be much difference in page load times if all the nodes were include on the page. If anyone tries this technique out on a larger nav tree please let us know your findings.

The next and probably final part will be about switching between tree and menu nav.