Apologies, this post is quite long due to the inclusion of some javascript code for reference…
The other day I posted about the fact that we we’re looking to support the nesting of group level widgets, like rendering a TabPanel within an Accordion, and vice versa… and doing this recursively so we can go down x many levels. The plan was to do this simply using the REGION_STATIC_ID and a dot notation e.g. “TABPANEL1.ACCORDION1.TABPANEL1″ for. Looking at the syntax I thought it should be simple enough to implement.
Note: in the framework group level widgets are not defined as regions in APEX, they are simply defined in the REGION_STATIC_ID region fields, as they are just holders around normal widgets e.g. Grid, Tree, Form, Graph etc. this simplifies the defining of them. We add a numeric identifier on them to either group widgets together within the same tab if the identifier is the same, or seperate them on different tabs if they’e not.
The thing that I’ve found is that doing anything simply means you can make it really complex behind the scenes, especially if you try and do something recursively, my brain went into melt down after 5 coffee’s and staring at the same lines of code for over 6 hours.
A couple of things have significantly helped though:
Previously I read Simon Hunts blog on using a pipelined function to turn a delimited string into a query-able table in SQL, anyway what I use it for is to work out where I am in the recursion level and what widget I’m currently processing (However i’ve simplified it by purely using it as function call and return the row I’m interested in), i.e. in level 2 of a recursive call for the following “TABPANEL1.ACCORDION1.TABPANEL1″ I’m processing the Accordion widget. To show you how I use it and process the widgets, here’s the SQL (I don’t expect you to completely follow it but hopefully it gives you an idea of how it can work):
SELECT SUBSTR(MAX(sys_connect_by_path(region_id,':')),2,LENGTH(MAX(sys_connect_by_path(region_id,':')))) regions, max(decode(rn,1,region_name, null)) region_name
FROM
(SELECT pr.region_id
, splitRow(pr.static_id,'.',p_level) static_id
, pr.region_name
, row_number() OVER (PARTITION BY splitRow(pr.static_id,'.',p_level) ORDER BY pr.display_sequence) rn
FROM apex_application_page_regions pr
WHERE pr.application_id = v('APP_ID')
AND pr.page_id = v('APP_PAGE_ID')
AND splitRow(regexp_replace(pr.static_id,'\d',''),'.',p_level) = splitRow(p_object_type,'.',p_level)
AND decode
( pr.display_position_code
, 'AFTER_SHOW_ITEMS' , 'BOX_BODY'
, 'BEFORE_BOX_BODY' , 'BOX_BODY'
, 'BEFORE_SHOW_ITEMS', 'BOX_BODY'
, pr.display_position_code) = p_display_position
AND extjs_utils.auth_condition_check(pr.condition_type,pr.condition_expression1,pr.condition_expression2,pr.authorization_scheme) = 0
)
START WITH rn = 1
CONNECT BY PRIOR rn = rn-1 AND PRIOR static_id = static_id
GROUP BY static_id
ORDER BY static_id
The actual results are a colon delimited string of the REGION_ID’s that belong to the group level widget, which I then use apex_util.string_to_table to process them. Here’s the actual code for splitting a string, and getting a value using a supplied row number.
create or replace TYPE "SPLIT_TBL" as table of varchar2(32767);
/
create or replace FUNCTION split
( p_list VARCHAR2
, p_del VARCHAR2 DEFAULT ','
) RETURN split_tbl PIPELINED
is
l_idx pls_integer;
l_list varchar2(32767) := p_list;
l_value varchar2(32767);
BEGIN
LOOP
l_idx := instr(l_list,p_del);
IF l_idx > 0 THEN
PIPE ROW(substr(l_list,1,l_idx-1));
l_list := substr(l_list,l_idx+length(p_del));
ELSE
PIPE ROW(l_list);
EXIT;
END IF;
END LOOP;
RETURN;
END split;
/
create or replace FUNCTION splitRow
( p_list VARCHAR2
, p_del VARCHAR2 DEFAULT ','
, p_rownum NUMBER DEFAULT 1
) RETURN VARCHAR2
is
l_value VARCHAR2(32767);
BEGIN
SELECT column_value
INTO l_value
FROM ( SELECT rownum rn, t.*
FROM TABLE(split(p_list,p_del)) t
)
WHERE rn = p_rownum;
RETURN l_value;
EXCEPTION
WHEN OTHERS THEN
RETURN 'Error';
END splitRow;
/
Anyway the point of the post is that we can now recursively support the nesting of group type widgets using a simple dot notation in the REGION_STATIC_ID, I don’t think you can code widget functionality much faster than that? The other added bonus is that the layout and widget config is completely customizable and you can use any of the allowed Ext config, and this applies to everything we have integrated thus far, so it’s really flexible and customizable.
The config is managed in runtime development mode, as per the screenshot in the last post, which we use an Ext property grid. Reminds me of one of David Peake’s comments about the APEX development team “Eating their own dog food!” i.e. the APEX development team use the APEX IDE to build APEX, similarly we reuse integrated components to manage integrated components. Anyway here’s a few screenshots of some simple, two level nesting…
A nested accordion within a tabpanel:

A nested tabpanel within an accordion:

The thing to note is that traditional APEX development would mean that we wouldn’t have coded that many regions on a page as we’d use navigational elements that were shared across pages, e.g. tabbed navigation lists, wizards etc. now these regions can be defined on a single page and we can use Ext layouts and components to organize them in a clean and organized way, thus cutting down on number of pages and components (it does have some maintenance benefits).
The final thing is that when you’re dealing with complexity, dynamic code generation based on underlying metadata is the best way to go (thats what APEX is, we’re just building on top of it!). If you get the design right, it removes the complexity. Just debugging all the issues and getting the design right in the first place is the struggle, but the end result is well worth it!
Here’s an example of the dynamic code generated by our viewport call for the above screenshot, which is built by the recursive function:
var apExtCenterPanel;
Ext.onReady(function () {
Ext.state.Manager.setProvider(new Ext.state.CookieProvider());
apExtCenterPanel = new Ext.app.apExtiFrameTabPanel("center-tabs", {
"region": "center",
"closable": false,
"hideBorders": true,
"id": "center-pane",
"layout": "fit",
"tabTip": gcApExtWelcome,
"title": "About",
"items": [Ext.app.apExtAccordion({
"items": [{
"layout": "fit",
"bodyBorder": false,
"border": false,
"title": "Center Tab 3",
"items": [Ext.app.apExtTabPanel({
"items": [{
"layout": "fit",
"bodyBorder": false,
"border": false,
"title": "Center Tab 3",
"items": [Ext.app.apExtPanel(extR31742036883319874)]
},
{
"layout": "fit",
"bodyBorder": false,
"border": false,
"title": "Products",
"items": [Ext.app.apExtGrid(ext17788801571748784)]
}],
"config": {
"border": false,
"draggable": false,
"frame": false,
"iconCls": "icon-form",
"shadow": false,
"tabPosition": "bottom"
}
},
"center-pane")]
},
{
"layout": "fit",
"bodyBorder": false,
"border": false,
"title": "Center Tab 2",
"items": [Ext.app.apExtPanel(extR40263958257140884)]
}],
"config": {
"bodyBorder": false,
"border": false,
"defaults": {
iconCls: "icon-accordion"
},
"iconCls": "settings",
"layout": "accordion",
"layoutConfig": {
titleCollapse: false,
animate: true,
activeOnTop: true
}
}
},
"center-pane")]
});
viewport = new Ext.ux.apExtFormViewport({
"layout": "border",
"id": "apextjs-viewport-newc",
"items": [new Ext.BoxComponent({
"region": "north",
"el": "ext-north-pane",
"height": 65
}), new Ext.BoxComponent({
"region": "south",
"el": "ext-south-pane",
"height": 32
}), {
"region": "east",
"collapseMode": "mini",
"collapsible": true,
"id": "east-pane",
"layout": "fit",
"margins": "0 5 0 0",
"maxSize": 400,
"minSize": 175,
"split": true,
"title": "East Panel",
"useSplitTips": true,
"width": 225,
"items": [Ext.app.apExtAccordion({
"items": [{
"layout": "fit",
"bodyBorder": false,
"border": false,
"title": "East Tab 1",
"items": [Ext.app.apExtPanel(extR40263369208134543)]
},
{
"layout": "fit",
"bodyBorder": false,
"border": false,
"title": "East Tab 2",
"items": [Ext.app.apExtPanel(extR40263573364135812)]
},
{
"layout": "fit",
"bodyBorder": false,
"border": false,
"title": "Property Grid",
"items": [Ext.app.apExtPropertyGrid(extR33794511261603830)]
}],
"config": {
"bodyBorder": false,
"border": false,
"defaults": {
iconCls: "icon-accordion"
},
"iconCls": "settings",
"layout": "accordion",
"layoutConfig": {
titleCollapse: false,
animate: true,
activeOnTop: true
}
}
},
"east-pane")]
},
{
"region": "west",
"collapseMode": "mini",
"collapsible": true,
"id": "west-pane",
"layout": "fit",
"layoutConfig": {
animate: true
},
"maxSize": 400,
"minSize": 175,
"split": true,
"title": "West Panel",
"useSplitTips": true,
"width": 245,
"items": [Ext.app.apExtTabPanel({
"items": [{
"layout": "vbox",
"bodyBorder": false,
"border": false,
"title": "Navigation",
"items": [Ext.app.apExtTree("R40259858241590632")]
},
{
"layout": "fit",
"bodyBorder": false,
"border": false,
"title": "West Tab 2",
"items": [Ext.app.apExtPanel(extR40263061243132300)]
},
{
"layout": "fit",
"bodyBorder": false,
"border": false,
"title": "West Tab 3",
"items": [Ext.app.apExtPanel(extR33795216240614766)]
},
{
"layout": "fit",
"bodyBorder": false,
"border": false,
"title": "West Tab 4",
"items": [Ext.app.apExtPanel(extR33795421781616407)]
}],
"config": {
"border": false,
"draggable": false,
"frame": false,
"iconCls": "icon-form",
"shadow": false,
"tabPosition": "bottom"
}
},
"west-pane")]
},
apExtCenterPanel]
});
});