TutorialLoadData - mar10/fancytree GitHub Wiki
About initializing Fancytree and implementing lazy loading.
There are several ways to define the actual tree data:
- Format: plain JavaScript object, JSON formatted string, or HTML DOM elements
(
<ul><li>
) - Mode: synchronous or asynchronous
- Trigger: immediate or lazy (on demand)
This information is passed using the source
and lazyLoad
options:
$("#tree").fancytree({
// This option defines the initial tree node structure:
source: ...,
// This callback is triggered when a node with 'lazy' attribute expanded for
// the first time:
lazyLoad: ...,
...
};
The data format for source
and lazyLoad
is similar: a - possibly nested -
list of node objects.
i.e. an array of nested objects
$("#tree").fancytree({
source: [
{title: "Node 1", key: "1"},
{title: "Folder 2", key: "2", folder: true, children: [
{title: "Node 2.1", key: "3", myOwnAttr: "abc"},
{title: "Node 2.2", key: "4"}
]}
],
...
};
The following attributes are available as 'node.PROPERTY':
checkbox
, expanded
, extraClasses
, folder
, icon
, key
, lazy
,
partsel
, refKey
, selected
, statusNodeType
, title
, tooltip
,
unselectable
, unselectableIgnore
, unselectableStatus
.
All other fields are considered custom and will be added to the nodes data
object as 'node.data.PROPERTY' (e.g. 'node.data.myOwnAttr').
Additional information:
Passing Tree Meta Data
It is possible to pass additional tree meta-data along with the list of children:
{
foo: "bar",
baz: 17,
children: [
{title: "Node 1", key: "1"},
{title: "Folder 2", key: "2", folder: true, children: [
{title: "Node 2.1", key: "3"},
{title: "Node 2.2", key: "4"}
]
}
]
}
The additional properties will be added to the trees data
object:
alert(tree.data.foo); // -> 'bar'
Note: A top-level property named error
is reserved to signal error
conditions.
source
may be set to an jQuery.ajax()
settings object:
source: {
url: "/getTreeData",
cache: false
},
...
The Ajax service is expected to return valid JSON data:
[{"title": "Node 1", "key": "1"},
{"title": "Folder 2", "key": "2", "folder": true, "children": [
{"title": "Node 2.1", "key": "3"},
{"title": "Node 2.2", "key": "4"}
]}
]
See also '[Howto] Handle custom data formats' below.
Note: When using folders with Lazy Loading option (lazy: true), returned json for empty folders should contain "children": []
.
Not defined children keyword or with value = false || undefined || null renders empty folders into tree as expandable.
This case also applies to data which arrives to tree by lazy loading.
Note that the markup will be parsed and converted to the internal data format, so this is a rather inefficient way to pass data.
$("#tree").fancytree();
<div id="tree">
<ul id="treeData" style="display: none;">
<li id="1">Node 1
<li id="2" class="expanded folder">Folder 2
<ul>
<li id="3">Node 2.1
<li id="4">Node 2.2
</ul>
<li id="k234" class="lazy folder">This is a lazy loading folder with key k234.</li>
</ul>
</div>
...
The id
attribute becomes the node.key
(a unique key is generated if omitted).
The title
attribute becomes node.tooltip
and is displayed on hover.
The following boolean node properties may be set using class attributes of the
<li>
tag:
active
, expanded
, focus
, folder
, lazy
, selected
, unselectable
.
All other classes will be added to node.extraClasses
, and thus become classes
of the generated tree nodes.
Additional data can be added to node.data. ...
using data attributes:
<ul>
<li class="folder">jQuery links
<ul>
<li class="active" data-foo="bar" data-selected="true">jQuery home</li>
<li data-json='{expanded: true, "like": 42}'>jQuery docs</li>
will create node.data.foo = "bar"
, node.data.like = 42
.
Note that special attributes will change the node status instead:
one node will be selected, the other expanded.
A special syntax allows to set node.data.href
and node.data.target
using a HTML markup that is functional even when JavaScript is not available:
<div id="tree">
<ul>
<li id="1"><a href="http://example.com" target="_blank">Go home</a></li>
will be parsed as node.data.href = "http://example.com"
,
node.data.target = "_blank"
.
It is possible to pass additional tree meta-data with the container or outer
<ul>
element:
<div id="tree" data-foo="bar">
<ul data-json='{"baz": "x", "like": 42}'>
<li id="1">node 1</li>
The additional properties will be added to the trees data
object:
tree.data.foo = "bar"
and tree.data.like = 42
.
source
may be a jQuery deferred promise as returned by $.ajax()
or $.getJSON()
:
$("#tree").fancytree({
source: $.ajax({
url: "/myWebService",
dataType: "json"
}),
lazyLoad: function(event, data){
// Pass on a deferred promise from another method:
// data.result = $.getJSON("ajax-sub2.json");
// Immediately return a deferred and resolve it as soon as data is available:
var dfd = new $.Deferred();
data.result = dfd.promise();
window.setTimeout(function(){ // Simulate a slow Ajax request
dfd.resolve([
{ title: "node 1", lazy: true },
{ title: "node 2", select: true }
]);
}, 1500);
},
[...]
});
Promises are also a general way to pass results that are asynchronously created. Have a look at the documentation.
Note: Also ECMAScript 6 Promises are accepted.
source
may be callback that returns one of the above data formats.
source: function(event, data){
return [{title: "node1", ...}, ...];
}
The postProcess
callback allows to check or modify node data before it is added to the
tree.
This is especially helpful to convert an incoming Ajax response to the Fancytree format.
data.response
contains a reference to the original data as returned by the Ajax request.
It may be modified in-place:
postProcess: function(event, data) {
// assuming the Ajax response contains a list of child nodes:
data.response[0].title += " - hello from postProcess";
}
It is also possible to create a new response object that will be used instead:
postProcess: function(event, data) {
data.result = [{title: "Node created by postProcess"}];
}
Error conditions can be signaled by setting a special .error
property:
postProcess: function(event, data) {
if( data.response.status !== "ok" ) {
data.result = { error: "Didn't work :-/" };
} else {
data.result = data.response.d;
}
}
See also '[Howto] Handle Custom Data Formats' below.
Note that postProcess
is also triggered for lazy loaded nodes.
Single nodes may be marked 'lazy'. These nodes will generate Ajax request when expanded for the first time. Lazy loading allows to present hierarchical structures of infinite size in an efficient way.
For example:
$("#tree").fancytree({
// Initial node data that sets 'lazy' flag on some leaf nodes
source: [
{title: "Child 1", key: "1", lazy: true},
{title: "Folder 2", key: "2", folder: true, lazy: true}
],
// Called when a lazy node is expanded for the first time:
lazyLoad: function(event, data){
var node = data.node;
// Load child nodes via Ajax GET /getTreeData?mode=children&parent=1234
data.result = {
url: "/getTreeData",
data: {mode: "children", parent: node.key},
cache: false
};
},
[...]
});
The data format for lazy loading is similar to that of the source
option.
If node.lazy
is true and node.children
is null
or undefined, the lazyLoad
event is triggered, when this node is expanded.
Note that a handler can return an empty array ([]
) to mark the node
as 'no children available'. It then becomes a standard end-node which is no
longer expandable.
Lazy Loading Sequence Diagram
A typical use case would be to handle Ajax return formats that wrap the node data in order to pass additional success/fault information:
{ "status": "ok",
"result": [ (... list of child nodes...) ]
}
Example response in case of an error:
{ "status": "error",
"faultMsg": "Bad luck :-/",
"faultCode": 17,
"faultDetails": "Something went wrong."
}
The postProcess
callback (described earlier) allows to modify node data before it is added to the
tree:
postProcess: function(event, data) {
var orgResponse = data.response;
if( orgResponse.status === "ok" ) {
data.result = orgResponse.result;
} else {
// Signal error condition to tree loader
data.result = {
error: "ERROR #" + orgResponse.faultCode + ": " + orgResponse.faultMsg
}
}
}
This is just a variant of a 'Custom Data Format' and could be handled by
postProcess
as described above:
postProcess: function(event, data) {
data.result = JSON.parse(data.response.d);
}
However this is a common one, since ASPX WebMethods use this format.
Therefore Fancytree handles this transparently if the enableAspx
option
is set.
This example converts a flat list of tasks into the nested structure that
Fancytree expects.
Every task has an id
property and an optional parent
property to define
hierarchical relationship. Child order is defined by a position
property and
may differ from appearance in the flat list.
This example is taken from the Google Tasks API, see also issue 431.
Assume this data format:
{
"kind": "tasks#tasks",
"items": [{
"kind": "tasks#task",
"id": "MTYwNzEzNjc2OTEyMDI1MzcwNzM6ODUwNjk4NTgzOjExMTkyODk2MjA",
"title": "Task 01",
"position": "00000000002147483647",
"status": "needsAction"
}, {
"kind": "tasks#task",
"id": "MTYwNzEzNjc2OTEyMDI1MzcwNzM6ODUwNjk4NTgzOjg4ODk2MDI0MQ",
"title": "Children of Task 01",
"parent": "MTYwNzEzNjc2OTEyMDI1MzcwNzM6ODUwNjk4NTgzOjExMTkyODk2MjA",
"position": "00000000002147483647",
"status": "needsAction"
}, {
"kind": "tasks#task",
"id": "MTYwNzEzNjc2OTEyMDI1MzcwNzM6ODUwNjk4NTgzOjEwNzM4NjYzMw",
"title": "Task 02",
"position": "00000000003500526173",
"status": "completed"
} ]
}
We can now convert this data in the postProcess
event:
function convertData(childList) {
var parent,
nodeMap = {};
if( childList.kind === "tasks#tasks" ) {
childList = childList.items;
}
// Pass 1: store all tasks in reference map
$.each(childList, function(i, c){
nodeMap[c.id] = c;
});
// Pass 2: adjust fields and fix child structure
childList = $.map(childList, function(c){
// Rename 'key' to 'id'
c.key = c.id;
delete c.id;
// Set checkbox for completed tasks
c.selected = (c.status === "completed");
// Check if c is a child node
if( c.parent ) {
// add c to `children` array of parent node
parent = nodeMap[c.parent];
if( parent.children ) {
parent.children.push(c);
} else {
parent.children = [c];
}
return null; // Remove c from childList
}
return c; // Keep top-level nodes
});
// Pass 3: sort children by 'position'
$.each(childList, function(i, c){
if( c.children && c.children.length > 1 ) {
c.children.sort(function(a, b){
return ((a.position < b.position) ? -1 : ((a.position > b.position) ? 1 : 0));
});
}
});
return childList;
}
// Initialize Fancytree
$("#tree").fancytree({
checkbox: true,
source: {url: "get_tasks.json"},
postProcess: function(event, data){
data.result = convertData(data.response);
}
});
Also XML responses can be parsed and converted in the postProcess
event.
See this Stackoverflow answer
and this demo
for an example.
// Reload the tree from previous `source` option
tree.reload().done(function(){
alert("reloaded");
});
// Optionally pass new `source`:
tree.reload({
url: ...
}).done(function(){
alert("reloaded");
});
'Paging' nodes are status nodes of type 'paging' and can serve as a placeholder for missing data. Typically we add some additional information that we can use later to load the missing nodes.
[
{title: "Item 1", key: "node1"},
{title: "Folder 2", folder: true, expanded: true, key: "node2"},
{title: "Lazy error", key: "node3", lazy: true},
{title: "Lazy empty", key: "node4", lazy: true},
{title: "Lazy paging", key: "node5", lazy: true},
{title: "More...", statusNodeType: "paging", icon: false, url: "get_children?parent=4321&start=5&count=10"}
]
It is also possible to create paging nodes using the API, for example in the
loadChildren
event:
data.node.addPagingNode({
title: "More...",
url: "get_children?parent=4321&start=5&count=10"
});
Paging nodes generate special events instead of plain activate
.
A common implementation would issue a load request and replace the
'More...' entry with the result:
$("#tree").fancytree({
...
clickPaging: function(event, data) {
data.node.replaceWith({url: data.node.data.url}).done(function(){
// The paging node was replaced with the next bunch of entries.
});
}
This can be achieved using standard functionality and this simple pattern:
loadChildren: function(event, data) {
data.node.visit(function(subNode){
// Load all lazy/unloaded child nodes
// (which will trigger `loadChildren` recursively)
if( subNode.isUndefined() && subNode.isExpanded() ) {
subNode.load();
}
});
}
In selectMode 3 (multi-hier) we can fix the the selection state of child nodes after a lazy parent was loaded:
$("#tree").fancytree({
checkbox: true,
selectMode: 3,
source: { url: "/getTreeData", cache: false },
lazyLoad: function(event, data) {
data.result = {
url: "/getTreeData",
data: {mode: "children", parent: node.key},
cache: false
};
},
loadChildren: function(event, data) {
// Apply parent's state to new child nodes:
data.node.fixSelection3AfterClick();
},