Opened 9 years ago

Closed 4 years ago

#11065 closed enhancement (patchwelcome)

Tree: option to reload/refresh

Reported by: Florian Owned by:
Priority: high Milestone: 1.13
Component: Dijit Version: 1.4.2
Keywords: Cc:
Blocked By: Blocking:

Description

I already posted on dojo-interest (http://mail.dojotoolkit.org/pipermail/dojo-interest/2010-February/043151.html and http://mail.dojotoolkit.org/pipermail/dojo-interest/2010-April/044861.html), but I think the issue may be a bug, so this goes here too:

In my application I have a Tree with a ForestStoreModel? and a JsonRestStore? with a PHP-backend. As the data on the PHP-backend can be altered by other users of the application I want to give the users a possibility to manually "reload" the tree. Technically spoken I like to inform the tree of changes which were made to the store's backend. Kris Zyp told me that the only thing I have to do is to call a fetch() on the JsonRestStore?. If I do that, I can watch the call to the backend in Firebug, can see the altered data the backend delivers and the tree does ... nothing.

To demonstrate this behaviour I created a little example here: http://digicult-museen.de/test/tree-refresh-example.html . Code:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/dojo/1.4/dijit/themes/soria/soria.css" />
<script src="http://ajax.googleapis.com/ajax/libs/dojo/1.4/dojo/dojo.xd.js" type="text/javascript"></script>
<script type="text/javascript">
    dojo.require('dijit.Tree');
    dojo.require('dojox.data.JsonRestStore');
</script>
<script type="text/javascript">
var reloadStore = function() {
    store.fetch({
        query: { 
            parent: 0
        }
    });
};

var loadTreeItem = function(item) {
	console.debug(item);
};

dojo.addOnLoad(function() {
	dojo.parser.parse();
});
</script>
</head>
<body class="soria">
<span dojoType="dojox.data.JsonRestStore" 
        jsId="store" 
        target="/test/treeRootData.php" 
        labelAttribute="name"
        idAttribute="id">
</span>
<span dojoType="dijit.tree.ForestStoreModel" 
        jsId="model" 
        store="store"  
        childrenAttr="children" 
        query="{parent: 0}" 
        rootId="0" 
        rootLabel="" 
        labelAttr="name" 
        idAttribute="id" 
        deferItemLoadingUntilExpand="true">
</span>  

<div dojoType="dijit.Tree" 
        model="model"
        jsId="tree"  
        showRoot="false"
        persist="false"
        copyOnly="true">
    <script type="dojo/method" event="onClick" args="item">
        loadTreeItem(item);
    </script>
</div> 
<a href="javascript:reloadStore()">reload root by store.fetch()</a>
<br>
<a href="javascript:model._requeryTop()">reload root by model._requeryTop()</a>
</body>
</html>

Short summary: If the reload-link is clicked, the store fetches the two rootnodes again, which alternately deliver the following data:

[ 
    { $ref: 'node1', name:'node1', someProperty:'somePropertyA', children:true},
    { $ref: 'node2', name:'node2', someProperty:'somePropertyB'}
]
[ 
    { $ref: 'node1', name:'node1', someProperty:'somePropertyA'},
    { $ref: 'node2', name:'node2', someProperty:'somePropertyB', children:true}
]

Bug? Am I doing something wrong?

Regards,

Florian

Change History (16)

comment:1 Changed 9 years ago by bill

Milestone: tbdfuture
Summary: reloading treestore with fetch()Tree: option to reload/refresh
Type: defectenhancement

It's not a bug, calling fetch() on the store isn't supposed to have any effect on the Tree. Tree responds to notification (onNew, onChange, etc, see the dojo.data Notification API), but that requires the store to be aware of what elements changed and then notify the Tree.

Various people have asked for an option to reload the Tree (refetching all data), but I don't see a ticket for it so I'll use this one.

However, the simple thing to do is just to recreate your tree. get('path') on the old tree followed by set('path', ...) on the new tree will restore it to it's old position.

comment:2 Changed 9 years ago by Florian

Thanks for the reply and for changing the summary!

For the simple thing in your reply: .get('path') only gives me the possibility to remember exactly one openend subnode (-> the last expanded), but a user usually has more than one openend path in a tree... How do I handle this?

comment:3 Changed 9 years ago by bill

Oh, the opened/closed state of each tree node is stored in a cookie, just use the persist=true flag.

comment:4 Changed 9 years ago by Florian

I think I found a solution I can live with.

Code:

/**
 * 
 * Extension to dijit.Tree to provide a reload-method 
 * and prevent caching with JsonRestStore 
 * 
 * @author floriank
 * @date 2010/05/06
 * @link http://www.digicult-sh.de/  
 * 
 */
dojo.provide("digicult.dijit.Tree");

dojo.require("dijit.Tree");

dojo.declare("digicult.dijit.Tree", dijit.Tree, {
    
    /**
     * initialize the tree-persist-cookie to be aware of 
     * tree-expands (necessary for refreshing of tree) 
     */
    onLoad: function(){
        // clear and init the tree's persist-cookie
        dojo.cookie(this.cookieName, '');
        this.attr('persist', true);
        this._initState();
    },  
    
    /** 
     * remember the original _loadObject-method in TreeNode to be 
     * used in onOpen-event 
     */
    _expandNode: function(/*_TreeNode*/ node, /*Boolean?*/ recursive){
        if(node._expandNodeDeferred && !recursive){
            // there's already an expand in progress (or completed), so just return
            return node._expandNodeDeferred;    // dojo.Deferred
        }
        
        item = node.item;
        if (item._loadObject && !node._loadObjectFunction) {
            node._loadObjectFunction = item._loadObject;
        }
        return this.inherited(arguments);
    }, 
    
    /**
     * reset the _loadObject-method of the item to its default 
     * to force a "fresh reload" at the next _expandNode() 
     */
    onOpen: function(/* dojo.data */ item, /*TreeNode*/ node){
        node.state = "UNCHECKED";
        if (node._loadObjectFunction && !item._loadObject) {
            item._loadObject = node._loadObjectFunction;
            delete node._loadObjectFunction;
        }
    }, 
    
    /**
     * reload the whole tree
     *  
     * (many thanks to the tips from Rob Weiss found here: 
     * http://mail.dojotoolkit.org/pipermail/dojo-interest/2010-April/045180.html)
     */
    reload: function () {
        // delete the tree's itemNodes
        this._itemNodesMap = {};
        
        // reset the state of the rootNode
        this.rootNode.state = "UNCHECKED";
        
        // unset the tree.model's root-children
        this.model.root.children = null;
        
        // unset the JsonRestStore's cache 
        storeTarget = this.model.store.target;
        for (var idx in dojox.rpc.Rest._index) {
            if (idx.match("^" + storeTarget)) {
                delete dojox.rpc.Rest._index[idx];
            }
        }
        
        // remove the rootNode 
        if (this.rootNode) {
            this.rootNode.destroyRecursive();
        }
        
        // rebuild the tree
        this._load();
    } 
});

I also updated the example on http://digicult-museen.de/test/tree-refresh-example.html .

comment:5 Changed 9 years ago by Florian

Update: Additionally to the function to reload the whole tree it would be fine to have an option to reload all opened childnodes of one treenode. In my opinion the function reload() should have one paramter of type "TreeNode?" where you pass in either the tree's rootNode or one other node of the tree.

comment:6 Changed 9 years ago by r.reckert

Hi all.

Just found this ticket. Is there really no way to refresh a branch in a tree (using ForestStoreModel? and JsonRestStore?)? I thought, JsonRestStore? was the recommended store for a tree? How else am I supposed to show updated data from the server in a tree? Is it not even planned to implement the reload/refresh for a branch of a tree?

Best regards, Ralph

comment:7 Changed 9 years ago by timtoo

There appears to be a possible solution, at least to some cases, here in a new mail list thread which referred back to this bug report:

http://mail.dojotoolkit.org/pipermail/dojo-interest/2010-August/048292.html

comment:8 in reply to:  6 Changed 9 years ago by bill

Replying to r.reckert:

Hi all.

Just found this ticket. Is there really no way to refresh a branch in a tree (using ForestStoreModel? and JsonRestStore?)? I thought, JsonRestStore? was the recommended store for a tree? How else am I supposed to show updated data from the server in a tree? Is it not even planned to implement the reload/refresh for a branch of a tree?

Best regards, Ralph

Ralph, as I wrote above, Tree responds to notification (onNew, onChange, etc, see the dojo.data Notification API), but that requires the store to be aware of what elements changed and then notify the Tree.

Various people have asked for an option to reload the Tree (refetching all data), but I don't see a ticket for it so I'll use this one.

However, the simple thing to do is just to recreate your tree. get('path') on the old tree followed by set('path', ...) on the new tree will restore it to it's old position.

comment:9 Changed 9 years ago by Tourman

The above tree example seems to be broken on 1.5 using FF 3.6.10 on WinXP Pro 32bit with a JsonRestStore?:

[Widget digicult.dijit.Tree, digicult_dijit_Tree_0] { _attachPoints=, more...} : error loading root children: TypeError?: args.item is undefined { message="args.item is undefined", more...} bootstrap.js (line 1824) TypeError?: args.item is undefined [Break on this error] (676 out of range 505) bootstrap.js (line 676) TypeError?: args.item is undefined [Break on this error] (676 out of range 505) bootstrap.js (line 676)

The error happens after this._load is called on the last line in Tree.reload():

rebuild the tree this._load();

}

});

I can confirm the tree works perfectly in 1.4.3, but will not reload on 1.5.

comment:10 Changed 9 years ago by Florian

Hi Tourman,

I am working with this updated digicult.dijit.Tree (but I still hope for an 'official' refresh in dijit.Tree):

/**
 * 
 * Extension to dijit.Tree to provide a reload-method 
 * and prevent caching with JsonRestStore 
 * 
 * @author floriank
 * @date 2010/07/30
 * @link http://www.digicult-sh.de/  
 * 
 */
dojo.provide("digicult.dijit._TreeNode");
dojo.provide("digicult.dijit.Tree");

dojo.require("dijit.Tree");

dojo.declare("digicult.dijit._TreeNode", dijit._TreeNode, {
	_loadObjectFunction: null
});

dojo.declare("digicult.dijit.Tree", dijit.Tree, {
	
	/**
	 * initialize the tree-persist-cookie to be aware of 
	 * tree-expands (necessary for refreshing of tree) 
	 */
	onLoad: function(){
		// clear and init the tree's persist-cookie
		if (!this.get('persist')) {
			dojo.cookie(this.cookieName, '');
			this.set('persist', true);
			this._initState();
		}
	},	
	
	/** 
	 * remember the original _loadObject-method in TreeNode to be 
	 * used in onOpen-event 
	 */
	_expandNode: function(/*_TreeNode*/ node, /*Boolean?*/ recursive){
		if(node._expandNodeDeferred && !recursive){
			// there's already an expand in progress (or completed), so just return
			return node._expandNodeDeferred;	// dojo.Deferred
		}
		
		item = node.item;
		if (item._loadObject && !node._loadObjectFunction) {
			node._loadObjectFunction = item._loadObject;
		}
		return this.inherited(arguments);
	}, 
	
	_createTreeNode: function(/*Object*/ args){
		// summary:
		//		creates a TreeNode
		// description:
		//		Developers can override this method to define their own TreeNode class;
		//		However it will probably be removed in a future release in favor of a way
		//		of just specifying a widget for the label, rather than one that contains
		//		the children too.
		return new digicult.dijit._TreeNode(args);
	},
	
	/**
	 * reset the _loadObject-method of the item to its default 
	 * to force a "fresh reload" at the next _expandNode() 
	 */
	onOpen: function(/* dojo.data */ item, /*TreeNode*/ node){
		node.state = "UNCHECKED";
		if (node._loadObjectFunction && !item._loadObject) {
			item._loadObject = node._loadObjectFunction;
			delete node._loadObjectFunction;
		}
	}, 
	
	/**
	 * reload the whole tree
	 *  
	 * (many thanks to the tips from Rob Weiss found here: 
	 * http://mail.dojotoolkit.org/pipermail/dojo-interest/2010-April/045180.html)
	 */
	reload: function () {
		// remember path
		path = this.get('path');
		
		// remove the rootNode 
		if (this.rootNode) {
			this.rootNode.destroyRecursive();
		}
	
		// unset the JsonRestStore's cache 
		storeTarget = this.model.store.target;
		for (var idx in dojox.rpc.Rest._index) {
			if (idx.match("^" + storeTarget)) {
				delete dojox.rpc.Rest._index[idx];
			}
		}

		// reset the state of the rootNode
		this.rootNode.state = "UNCHECKED";
		
		// reset the tree.model's root
		this.model.constructor(this.model);
		
		// rebuild the tree
		this.postMixInProperties();
		this._load();
		this.set('path', path).then(
				// focus node after setPath has finished
				dojo.hitch(this, function() { 
					this.focusNode(this.get('selectedNode')); 
				} 
		));
	} 
});

Regards,

Florian

comment:11 Changed 8 years ago by Florian

Update for compatibility with 1.6 (plus some minor changes for modularity). To be able to reload just use de.domcura.dijit.RefreshableTree? / de.domcura.dijit.RefreshableUncachedTree? instead of dijit.Tree and call .reload() on it.

To dijit.Tree-developers: Are there any plans for an 'official' support for reloading a dijit.Tree in nearer future?

/**
 * package de.domcura.dijit.RefreshableTree
 * 
 * Extension to dijit.Tree to provide a reload-method 
 * 
 * @author floriank 
 * @date 2011/06/16
 * @link http://www.domcura-ag.de/
 */
dojo.provide('de.domcura.dijit.RefreshableTree');
dojo.provide('de.domcura.dijit._UncachedTreeNode');
dojo.provide('de.domcura.dijit.RefreshableUncachedTree');

dojo.require('dijit.Tree');

dojo.declare('de.domcura.dijit.RefreshableTree', [dijit.Tree], {

	_reloadPaths: null, 
	_reloadOnLoadConnect: null, 
	
	/**
	 * initialize the tree-persist-cookie to be aware of 
	 * tree-expands (necessary for refreshing of tree) 
	 */
	onLoad: function(){
		this.inherited(arguments);
		
		// clear and init the tree's persist-cookie
		if (!this.get('persist')) {
			dojo.cookie(this.cookieName, '');
			this.set('persist', true);
			this._initState();
		} 
	},
	
	/** 
	 * Unset tree's attributes but leave the widget untouched. 
	 * This code is copied from dijit.Tree's .destroy()-method. 
	 * 
	 * dijit.Tree.destroy() could be reduced to 
	 * 
	 * destroy: function() {
	 * 		this._destroy();
	 * 		this.inherited(arguments);
	 * }
	 * 
	 * if someone of the dijit.Tree-developers can 
	 * implement this ._destroy()-method.
	 *  
	 **/
	_destroy: function(){
		if(this._curSearch){
			clearTimeout(this._curSearch.timer);
			delete this._curSearch;
		}
		if(this.rootNode){
			this.rootNode.destroyRecursive();
		}
		if(this.dndController && !dojo.isString(this.dndController)){
			this.dndController.destroy();
		}
		this.rootNode = null;
	},
	
	/**
	 * reload the whole tree
	 *  
	 * (many thanks to the tips from Rob Weiss found here: 
	 * http://mail.dojotoolkit.org/pipermail/dojo-interest/2010-April/045180.html)
	 */
	reload: function () {
		/* remember current paths: 
		 * simplify all nodes of paths-array to strings of identifiers because 
		 * after reload the nodes may have different ids and therefore the 
		 * paths may not be applied */ 
		this._reloadPaths = dojo.map(this.get('paths'), dojo.hitch(this, function(path) {
			return dojo.map(path, dojo.hitch(this, function(pathItem) {
				return this.model.getIdentity(pathItem) + '';
			}));
		}));
		
		/* reset tree: */
		this._destroy();
		this.dndController = "dijit.tree._dndSelector";
		
		/* unset the store's cache (if existing).  
		 * TODO: currently only tested on JsonRestStore! */ 
		if (dojox && dojox.rpc && dojox.rpc.Rest && dojox.rpc.Rest._index) {
			for (idx in dojox.rpc.Rest._index) {
				if (idx.match("^" + this.model.store.target)) {
					delete dojox.rpc.Rest._index[idx];
				}
			};
		}

		// reset the tree.model's root
		this.model.constructor({
			rootId: '0',
			rootLabel: ''
		});
		
		// rebuild the tree
		this.postMixInProperties();
		this.postCreate();
		
		/* reset the paths */
		this._reloadOnLoadConnect = dojo.connect(this, 'onLoad', dojo.hitch(this, function() {
			/* restore old paths.   
			 * FIXME: this will result in an error if a formerly selected item 
			 * is no longer existent in the tree after reloading! */
			this.set('paths', this._reloadPaths).then(dojo.hitch(this, function() {
				if (this.get('selectedNode')) {
					this.focusNode(this.get('selectedNode'));
				}
				this._reloadPaths = null;
				dojo.disconnect(this._reloadOnLoadConnect);
				this._reloadOnLoadConnect = null;
			}));
		}));
	} 
});

/**
 * FIXME: This "uncached-behaviour" is simply copied from 
 * digicult.dijit.Tree from 2010/07/30 and mostly untested!!! 
 * (there is currently no usage of lazy-load in my projects) 
 */
dojo.declare('de.domcura.dijit._UncachedTreeNode', dijit._TreeNode, {
	_loadObjectFunction: null
});

dojo.declare('de.domcura.dijit.RefreshableUncachedTree', de.domcura.dijit.RefreshableTree, {
	/** 
	 * remember the original _loadObject-method in TreeNode to be 
	 * used in onOpen-event 
	 */
	_expandNode: function(/*_TreeNode*/ node, /*Boolean?*/ recursive){
		if(node._expandNodeDeferred && !recursive){
			// there's already an expand in progress (or completed), so just return
			return node._expandNodeDeferred;	// dojo.Deferred
		}
		if (node.item._loadObject && !node._loadObjectFunction) {
			node._loadObjectFunction = node.item._loadObject;
		}
		return this.inherited(arguments);
	}, 
	
	_createTreeNode: function(/*Object*/ args){
		// summary:
		//		creates a TreeNode
		// description:
		//		Developers can override this method to define their own TreeNode class;
		//		However it will probably be removed in a future release in favor of a way
		//		of just specifying a widget for the label, rather than one that contains
		//		the children too.
		return new de.domcura.dijit._UncachedTreeNode(args);
	},
	
	/**
	 * reset the _loadObject-method of the item to its default 
	 * to force a "fresh reload" at the next _expandNode() 
	 */
	onOpen: function(/* dojo.data */ item, /*TreeNode*/ node){
		node.state = "UNCHECKED";
		if (node._loadObjectFunction && !item._loadObject) {
			item._loadObject = node._loadObjectFunction;
			delete node._loadObjectFunction;
		}
	} 
});

Regards,

Florian

comment:12 Changed 7 years ago by bjoerns

Has anyone tested this with dojo 1.8?

we are using this modification in a tree widget witch is declarative defined. Now we have the problem, that connected onClick functions are connected again for each refresh.

i.e: loading tree, clicking tree node -> normal behavior refreshing tree, clicking tree node -> onClick function is executed twice refreshing tree again, clicking tree node -> onClick function is executed three times

this happens regardless if I use dojo/on, dojo/connect or dojo/method in the markup

Last edited 7 years ago by bjoerns (previous) (diff)

comment:13 Changed 7 years ago by bill

I didn't read through all that RefreshableTree code, but presumably something changed because Tree events like clicking are now handled at the root level, via event delegation.

comment:14 in reply to:  12 Changed 7 years ago by Alexander Kläser

Replying to bjoerns:

Has anyone tested this with dojo 1.8?

we are using this modification in a tree widget witch is declarative defined. Now we have the problem, that connected onClick functions are connected again for each refresh.

Yes, this is due to the fact that postCreate() gets called during reload, and in postCreate(), event handlers are registered.

comment:15 Changed 4 years ago by dylan

Milestone: future1.12

We should either consider and land something like this for 1.12, or close as patchwelcome and encourage creation of an updated patch.

comment:16 Changed 4 years ago by bill

Resolution: patchwelcome
Status: newclosed

Closing as patchwelcome. If someone submits a PR including automated tests, will definitely consider it, but not going to work on it otherwise.

Note: See TracTickets for help on using tickets.