Freitag, 2. Oktober 2009

Nice trees with ExtJS (Erstellung eines ExtJS-Baums…)

image Bekannterweise ist die in APEX standardmäßig verfügbare Baumstruktur suboptimal und nicht wirklich sexy. Abhilfe kann hier die ExtJS-Bibliothek schaffen, die eine einfach zu definierende Baumstruktur anbietet. Links ist ein Beispiel einer solchen sexy Baumstruktur…
In diesem Beitrag möcht ich kurz zeigen, wie sich ein solcher Baum einfach in eine APEX-Anwendung einbinden und mit Daten aus der Datenbank füttern lässt.
Meistens wird beim Austausch von Standardelementen der Weg bevorzugt, das Element erst von APEX laden zu lassen und es dann entsprechend zu verändern. Ich denke bei einem Baum ist das nicht unbedingt notwendig, so das ich diesen von Grund auf durch ExtJS generieren werde.

Zunächst mal ein paar Worte zu ExtJS. ExtJS ist ein clientseitiges Javascript bzw. AJAX-Framework, das browserunabhängige Funktionalitäten für die DOM-Manipulation, Event-Handling oder auch asynchrone Serverkommunikation zur Verfügung stellt. Die API von ExtJS ist leicht erlernbar und wirklich intuitiv nutzbar, da sie fast vollständig JSON gestützt ist.

Für das folgende Beispiel wurde eine APEX-Anwendung mit einer Seite erstellt. Innerhalb dieser Seite gibt es eine einfache Region, die als Inhalt eine DIV-Region mit der Id “tree” definiert.

Folgende Stylesheets und Javascript-Dateien aus dem ExtJS-Package sollten im Headerbereich eingebunden sein.

<link rel="stylesheet" type="text/css" href="#IMAGE_PREFIX#javascript/ext-2.2.1/resources/css/ext-all.css" />

<script type="text/javascript" src="#IMAGE_PREFIX#javascript/ext-2.2.1/adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="#IMAGE_PREFIX#javascript/ext-2.2.1/ext-all.js"></script>


Zusätzlich wurde folgende JS-Funktion im Footer der Seite hinterlegt.


    Ext.onReady(function(){
new Ext.Panel({
title: 'My first tree',
renderTo: 'tree',
layout: 'border',
width: 500,
height: 500,
items: [{
xtype: 'treepanel',
id: 'tree-panel',
region: 'center',
margins: '2 2 0 2',
autoScroll: true,
rootVisible: false,
root: { text: "rootnode",
expanded: true,
children: [{ text:"node 1",
id:1,
leaf:true
},
{ text: "node 2",
id: 2,
leaf:false,
expanded: true,
children:[{ text:"node 1",
id:3,
leaf:true
}
]
}
]
},

}]
});
});

Was passiert hier? Ganz einfach…

Wenn die Seite vollständig geladen ist, wird die onReady-Funktion ausgeführt. Innerhalb dieser Funktion wird ein neues ExtJS-Panel mit dem Titel “My first tree” generiert. Die Option renderTo gibt an, in welchem Bereich der Seite das neu erstellte Panel eingefügt wird. In diesem Beispiel die in unserer Seite definierte DIV-Region.

Innerhalb dieses Panels wird ein TreePanel generiert, was einen nicht sichtbaren (rootVisible:false) Root-Knoten bekommt, der zwei Unterknoten (children) “node 1” und “node 2” enthält. “node 2” wiederum hat einen weiteren Unterknoten.


Die entscheidenden Werte zur Definition einen Knoten sind:

text: Der Text der für den betreffenden Knoten im Baum angezeigt werden soll.

id: Eine eindeutige Bezeichnung für den Knoten, z.B. die tatsächliche numerische ID einer Kategorie oder ähnliches.

leaf: Diese Option gibt an, ob der betreffende Knoten ein Blatt ist oder ob er weiter Unterknoten enthält.

children: Ist ein Array der Unterknoten, sofern vorhanden.

href: Muss gesetzt werden, falls der Knoten als Link funktionieren soll.

Nun hat man natürlich eher selten eine feste Baumstruktur abzubilden. Normalerweise wollen wir eine dynamische Struktur aus der Datenbank laden.

Hierzu erstellen wir eine Datenbank-Funktion, die das JSON-Objekt als Text zurückgibt. Da es relativ mühselig wäre die dafür benötigte Struktur zusammenzubauen, bin ich mal auf die Suche nach einer fertigen Lösung gegangen. Und siehe da, es gibt bereits ein JSON-Datentyp für Oracle. Vielen Dank an Lewis Cunningham dafür.

Ok, bereiten wir also erstmal ein paar Testdaten und die benötigte Funktion auf der Datenbank vor.


create table tbl_mon_category (cat_id number, cat_par_id number, cat_label varchar2(100));

insert into tbl_mon_category (cat_id,cat_par_id,cat_label) values (1,0,'Node 1');
insert into tbl_mon_category (cat_id,cat_par_id,cat_label) values (2,0,'Node 2');
insert into tbl_mon_category (cat_id,cat_par_id,cat_label) values (3,1,'Childnode A for Node 1');
insert into tbl_mon_category (cat_id,cat_par_id,cat_label) values (4,2,'Childnode A for Node 2');
insert into tbl_mon_category (cat_id,cat_par_id,cat_label) values (5,2,'Childnode B for Node 2');
insert into tbl_mon_category (cat_id,cat_par_id,cat_label) values (6,2,'Childnode C for Node 2');
commit;

create or replace package PCK_EXTJS_JSON_EXAMPLE is

-- Author : AHILDEBRANDT
-- Created : 10.08.2009

-- Purpose : Prüfen, ob eine Kategorie und Unterkategorien hat
function hasChildren(i_cat_id in number) return boolean;

-- Purpose : anhand einer übergebenen Kategorie-ID das
-- vollständige JSON-Objekt generieren (nutzt
-- Datentyp PL/JSON; arbeitet rekursiv!)
function getJsonObject(i_cat_id in number default 0) return json;

-- Purpose : Aufruf der Funktion zum Generieren des
-- JSON-Objekts und Rückgabe des Ergebnisses
-- als String
function getTreeDataDynamic return varchar2;

end PCK_EXTJS_JSON_EXAMPLE;
/
create or replace package body PCK_EXTJS_JSON_EXAMPLE is
/*
-- Author : AHILDEBRANDT
-- Created : 10.08.2009
-- Purpose : Prüfen, ob eine Kategorie und Unterkategorien hat
*/
function hasChildren(i_cat_id in number) return boolean is
v_count number:=0;
begin
select nvl(count(*),0) into v_count from tbl_mon_category where cat_par_id=i_cat_id;
if v_count>0 then
return true;
else
return false;
end if;
end;

/*
-- Author : AHILDEBRANDT
-- Created : 10.08.2009
-- Purpose : anhand einer übergebenen Kategorie-ID das vollständige JSON-Objekt generieren
-- (nutzt Datentyp PL/JSON)
-- ACHTUNG! REKURSIV!
*/
function getJsonObject(i_cat_id in number default 0) return json is
v_json json:=json();
v_arr_id NUMBER;
v_ele_id NUMBER;
begin
if i_cat_id = 0 then -- wenn Kategorie 0 dann wird das der Root-Knoten mit festen Werten
v_json.add_member(p_name => 'id', p_value => i_cat_id);
v_json.add_member(p_name => 'text', p_value => 'root');
else -- sonst regulärer Knoten; Werte für die Kategorie benutzen
for cat in (select * from tbl_mon_category where cat_id=i_cat_id) loop
v_json.add_member(p_name => 'id', p_value => i_cat_id);
v_json.add_member(p_name => 'text', p_value => cat.cat_label);
end loop;
end if;

if not hasChildren(i_cat_id) then -- wenn Knoten keine Child-Knoten hat
v_json.add_member(p_name => 'leaf', p_value => true); -- als Blatt markieren und fertig
v_json.add_member(p_name => 'href', p_value => 'f?p=108:2:'||v('APP_SESSION')); -- einen Link setzen
else -- sonst
v_json.add_member(p_name => 'leaf', p_value => false); -- als Knoten mit Unterknoten kennzeichnen
v_json.add_array(p_name => 'children', p_array_id => v_arr_id ); -- Array für Unterknoten hinzufügen
for child in (select * from tbl_mon_category where cat_par_id=i_cat_id) loop -- Schleife über alle Unterkategorien
-- Unterknoten über rekursiven Aufruf generieren und ins Array hängen
v_json.add_array_element(p_array_id => v_arr_id, p_value => getJsonObject(i_cat_id => child.cat_id), p_element_id => v_ele_id);
end loop;
end if;

return v_json;
end;

/*
-- Author : AHILDEBRANDT
-- Created : 10.08.2009
-- Purpose : Aufruf der Funktion zum Generieren des JSON-Objekts und Rückgabe des Ergebnisses als String
*/
function getTreeDataDynamic return varchar2 is
v_json json:=json();
begin
v_json:=getJsonObject(i_cat_id => 0);
return(v_json.getString);
end;

end PCK_EXTJS_JSON_EXAMPLE;


Als nächstes benötigen wir einen bedarfsgesteuerten Anwendungsprozess "getTreeData", der die Package-Funktion aufruft.
declare

v_json varchar2(32767);

begin

v_json:=PCK_EXTJS_JSON_EXAMPLE.getTreeDataDynamic;

htp.prn(v_json);

end;


Als letzten Schritt muss nun noch die vorhandene Javascript-Funktion angepasst werden.
Ext.onReady(
function(){

var TreeRequest = new htmldb_Get(null,$v('pFlowId'),'APPLICATION_PROCESS=getTreeData',0);
var TreeResponse = TreeRequest.get();
if (TreeResponse) {
var v_extjs_tree_data= Ext.util.JSON.decode(TreeResponse);
}

new Ext.Panel({
title: 'My first tree',
renderTo: 'tree',
layout: 'border',
width: 500,
height: 500,
items: [{
xtype: 'treepanel',
id: 'tree-panel',
region: 'center',
margins: '2 2 0 2',
autoScroll: true,
rootVisible: false,
root: v_extjs_tree_data,
}]
});
}
);


Das Ergebnis sollte in etwa wie folgt aussehen.

image

Und das war’s auch schon….


Title: How to create an ExtJS-Tree...
image
You probably all now, that the apex trees aren't really sexy. Therefore i decided to give ExtJS a shot. On the left you can see a tree build with ExtJS. Nice, isn't it?
This post shows how you can integrate an ExtJS tree like this in your apex application.


Let's start with a few words about ExtJS. ExtJS is a clientside javascript and ajax framework, that provides browser independent functionality for DOM-manipulation, event-handling or asynchronous server communication. The api is really easy to learn and to use, because it is completely based on JSON.

For the example in this post i created an application with one page. This page contains a region in which a div-region with id "tree" is defined.

The following stylesheets and javascript-files have to be included in the header of the page.
<link rel="stylesheet" type="text/css" href="#IMAGE_PREFIX#javascript/ext-2.2.1/resources/css/ext-all.css" />

<script type="text/javascript" src="#IMAGE_PREFIX#javascript/ext-2.2.1/adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="#IMAGE_PREFIX#javascript/ext-2.2.1/ext-all.js"></script>


Now we put the following function in the page footer.
    Ext.onReady(function(){
new Ext.Panel({
title: 'My first tree',
renderTo: 'tree',
layout: 'border',
width: 500,
height: 500,
items: [{
xtype: 'treepanel',
id: 'tree-panel',
region: 'center',
margins: '2 2 0 2',
autoScroll: true,
rootVisible: false,
root: { text: "rootnode",
expanded: true,
children: [{ text:"node 1",
id:1,
leaf:true
},
{ text: "node 2",
id: 2,
leaf:false,
expanded: true,
children:[{ text:"node 1",
id:3,
leaf:true
}
]
}
]
},

}]
});
});


After the page is completely loaded the onReady-Function creates an ExtJS-Panel with the title "My first tree", which is rendered to our defined div-region.

Inside this panel the treepanel is created with an invisible root-node and two childnodes "node 1" and "node 2". "node 2" has another childnode.

The most important options to define a node are:

text: text to be displayed in the tree

id: id for the node, which should be unique in the tree

leaf: this option shows wether the node is a leaf or not

children: an array of childnodes if there are any

href: has to be set for the node to work as a link (line 68 next scriptblock)



Usually we need to load dynamic data from the database.

To do that, we will create a database function, that gives us a generated JSON-Object as a string.

After a bit of research i found that Lewis Cunningham already build a JSON-Datatype for Oracle. Thanks for that, because that makes our task really easy.

The following script creates a table, some test data and a package with needed functions.
create table tbl_category (cat_id number, cat_par_id number, cat_label varchar2(100));

insert into tbl_category (cat_id,cat_par_id,cat_label) values (1,0,'Node 1');
insert into tbl_category (cat_id,cat_par_id,cat_label) values (2,0,'Node 2');
insert into tbl_category (cat_id,cat_par_id,cat_label) values (3,1,'Childnode A for Node 1');
insert into tbl_category (cat_id,cat_par_id,cat_label) values (4,2,'Childnode A for Node 2');
insert into tbl_category (cat_id,cat_par_id,cat_label) values (5,2,'Childnode B for Node 2');
insert into tbl_category (cat_id,cat_par_id,cat_label) values (6,2,'Childnode C for Node 2');
commit;

create or replace package PCK_EXTJS_JSON_EXAMPLE is

-- Author : AHILDEBRANDT
-- Created : 10.08.2009

-- Purpose : check if category has sub-categories
function hasChildren(i_cat_id in number) return boolean;

-- Purpose : create JSON-Object for category;
-- recursive<br/> function getJsonObject(i_cat_id in number default 0) return json;

-- Purpose : function that calls getJsonObject and returns the result as string
function getTreeDataDynamic return varchar2;

end PCK_EXTJS_JSON_EXAMPLE;
/
create or replace package body PCK_EXTJS_JSON_EXAMPLE is
/*
-- Author : AHILDEBRANDT
-- Created : 10.08.2009
-- Purpose : check if category has sub-categories
*/
function hasChildren(i_cat_id in number) return boolean is
v_count number:=0;
begin
select nvl(count(*),0) into v_count from tbl_category where cat_par_id=i_cat_id;
if v_count>0 then
return true;
else
return false;
end if;
end;

/*
-- Author : AHILDEBRANDT
-- Created : 10.08.2009
-- Purpose : create JSON-Object for category;
-- recursive
*/
function getJsonObject(i_cat_id in number default 0) return json is<br/> v_json json:=json();
v_arr_id NUMBER;
v_ele_id NUMBER;
begin
if i_cat_id = 0 then -- if category is root then set root-node
v_json.add_member(p_name => 'id', p_value => i_cat_id);
v_json.add_member(p_name => 'text', p_value => 'root');
else -- else --> usual node; use category infos
for cat in (select * from tbl_category where cat_id=i_cat_id) loop
v_json.add_member(p_name => 'id', p_value => i_cat_id);
v_json.add_member(p_name => 'text', p_value => cat.cat_label);
end loop;
end if;

if not hasChildren(i_cat_id) then -- if node has no children
v_json.add_member(p_name => 'leaf', p_value => true); -- mark as leaf
v_json.add_member(p_name => 'href', p_value => 'f?p=108:2:'||v('APP_SESSION')); -- set a link if needed
else -- sonst
v_json.add_member(p_name => 'leaf', p_value => false); -- mark as node with children
v_json.add_array(p_name => 'children', p_array_id => v_arr_id ); -- add array for subcategories
for child in (select * from tbl_category where cat_par_id=i_cat_id) loop -- loop through all sub-categories
-- create JSON-object for sub-category using recursive call and the append to array
v_json.add_array_element(p_array_id => v_arr_id, p_value => getJsonObject(i_cat_id => child.cat_id), p_element_id => v_ele_id);
end loop;
end if;

return v_json;
end;

/*
-- Author : AHILDEBRANDT
-- Created : 10.08.2009
-- Purpose : function that calls getJsonObject and returns the result as string
*/
function getTreeDataDynamic return varchar2 is
v_json json:=json();
begin
v_json:=getJsonObject(i_cat_id => 0);
return(v_json.getString);
end;

end PCK_EXTJS_JSON_EXAMPLE;
/


Next thing we need is an on-demand application process "getTreeData", that will call our package-function.
declare

v_json varchar2(32767);

begin

v_json:=PCK_EXTJS_JSON_EXAMPLE.getTreeDataDynamic;

htp.prn(v_json);

end;


Only thing left to do is changing our javascript-function...
Ext.onReady(
function(){

var TreeRequest = new htmldb_Get(null,$v('pFlowId'),'APPLICATION_PROCESS=getTreeData',0);
var TreeResponse = TreeRequest.get();
if (TreeResponse) {
var v_extjs_tree_data= Ext.util.JSON.decode(TreeResponse);
}

new Ext.Panel({
title: 'My first tree',
renderTo: 'tree',
layout: 'border',
width: 500,
height: 500,
items: [{
xtype: 'treepanel',
id: 'tree-panel',
region: 'center',
margins: '2 2 0 2',
autoScroll: true,
rootVisible: false,
root: v_extjs_tree_data,
}]
});
}
);


The result should look like this...

image

And that's it....




4 Kommentare:

  1. btw is used version 6.2.... there were major changes lately so this example will probably not work with the newest pl/json release

    AntwortenLöschen
  2. Tobias Arnhold did a nice job and updated the example... You can find it here

    AntwortenLöschen
  3. Hi Anja,

    I am still new to APEX and PL/JSON. I tried following your examples using the latest downloads from source forge and it works like a CHARM

    Great work thanks and Keep it up, I am sure the community appreciates your hard work and contributions

    cheers

    FroM
    Themba Tjnas Ngobeni

    AntwortenLöschen