Sunday, April 06, 2008

Persistent iGoogle Dashboards :: Redux

From time to time, I get questions about the iGoogle imitation example. The most often asked question is how to persist the layout once you've moved the containers. One way to do it is to use cookies. Because the original example used the YUI library to handle drag and drop and because YUI 2.5.1 now supports a beta cookie library, we'll take the cookie approach.

Keep in mind that this is very simple example. Our containers don't contain data and we don't have a backend ( i.e. PHP, JSP, etc. ). We're just going to persist layout and that's pretty easy to do with cookies.


Here's the
example.

The general idea is that if the layout cookie isn't set, we'll show the default positions ( the default layout is already there; we've just set the display to "none" ). Otherwise, we'll layout based on what's found in the cookie.

What does the cookie look like?

Well, all of our containers ( "Container 1", "Container 2", etc. ) have static positions and are found in three columns. So, we'll use three subcookies ( c1, c2 and c3 ) where each subcookie contains a string consisting of element IDs. The cookie's name is "igoogimi" and expires every 14 days. If we were to look at the cookie on April 6, 2008, we'd see something like --


Name :: igoogimi

Value :: c1=Rec1%2CRec2&c2=Rec3%2CRec4%2CAbout&c3=Rec5%2CRec6
Expires :: Sun, 20 Apr 2008 05:41:55 GMT

Setting and getting a subcookie is pretty straightforward and it's
well documented. Setting our subcookie is interesting because we use the method YAHOO.util.Dom.getChildrenBy to query the DOM looking for the column IDs. Here's our setCookies method --
        var setCookies = function() {
// BEGIN :: Calculate cookie expiration to 14 days from today
var date = new Date();
date.setTime(date.getTime()+(14*24*60*60*1000));
// END :: Calculate cookie expiration to 14 days from today
var getNode = function(node) {
return (node.id==="Rec1"||node.id==="Rec2"||node.id==="Rec3"||node.id==="Rec4"||node.id==="Rec5"||node.id==="Rec6"||node.id==="About");
}
var createString = function(colId) {
var nodes = YAHOO.util.Dom.getChildrenBy(document.getElementById(colId), getNode);
var list = [];
var l = nodes.length;
for(var i=0;i<l;i++) {
list[i] = nodes[i].id;
}
return list.toString();
}
YAHOO.util.Cookie.setSub("igoogimi", "c1", createString("Column1"), {expires: date});
YAHOO.util.Cookie.setSub("igoogimi", "c2", createString("Column2"), {expires: date});
YAHOO.util.Cookie.setSub("igoogimi", "c3", createString("Column3"), {expires: date});
}

Notice that the method
createString creates the subcookies values by looking for all children of the node with the matching column ID which pass the boolean test defined as the getNode method. In this method, we check to see if any of the children have IDs equal to the containers. If so, then those children ( these are nodes ) are returned. From those children, a string consisting of IDs in the order of their appearance in the DOM is created.

We set the igoogimi cookie when the unload event is fired. This occurs when we navigate to another page, close the browser, etc. This ensures that the cookie is always set.


When we read the cookie, a number of things happen.


The entire page is hidden through CSS ( the body's display is set to "none" ). We do this because the default layout is always there. We don't want it to appear and then be replaced by the layout found in the cookie.


Once we've determined whether the cookie exists, we remove all the nodes while keeping a reference to them. Then, we'll add them back based on the order that they're found in the igoogimi cookie --

          var containerRef = [];
var node;

// BEGIN :: Removing the nodes
var cArray1 = c1.split(",");
var len = cArray1.length;
for(var i=0;i<len;i++){
node = document.getElementById(cArray1[i]);
node?containerRef[cArray1[i]] = node.parentNode.removeChild(node):"";
}
var cArray2 = c2.split(",");
len = cArray2.length;
for(var i=0;i<len;i++){
node = document.getElementById(cArray2[i]);
node?containerRef[cArray2[i]] = node.parentNode.removeChild(node):"";
}
var cArray3 = c3.split(",");
len = cArray3.length;
for(var i=0;i<len;i++){
node = document.getElementById(cArray3[i]);
node?containerRef[cArray3[i]] = node.parentNode.removeChild(node):"";
}
// END :: Removing the nodes

// BEGIN :: Adding the nodes
len = cArray1.length;
var col = document.getElementById("Column1");
var tmpCR;
for(var i=0;i<len;i++){
tmpCR = containerRef[cArray1[i]];
tmpCR?col.appendChild(tmpCR):"";
}
len = cArray2.length;
var col = document.getElementById("Column2");
for(var i=0;i<len;i++){
tmpCR = containerRef[cArray2[i]];
tmpCR?col.appendChild(tmpCR):"";
}
len = cArray3.length;
var col = document.getElementById("Column3");
for(var i=0;i<len;i++){
tmpCR = containerRef[cArray3[i]];
tmpCR?col.appendChild(tmpCR):"";
}
// END :: Adding the nodes

Keep in mind that we're passing through the DOM twice -- once for removal and another for insertion. So, it's not terribly efficient. Note that the removal and the insertions dynamically update the DOM and if we didn't hide the body, then the page would be jumping all over the place ( this is known as reflow where DOM manipulations are done when rendering is completed ). Once the last node has been added, we set the body's display to "block" and all the containers appear.


You can see our
example here. To review the code, simply view the source.

Feel free to use and make it better.

Have fun!

7 comments:

Unknown said...

Skypoet, you ROCK!!!! This is just what i have been looking for...

Neil

Jojiju said...

nice dashboard,but it is creating one problem.the marker is appended at the end of the column when I start to drag the first div of first column.

I mean what should I do if I started to drag and before I end drag I want to keep it in original position like in Igoogle?
marker is not created in the position I leave instead at the end of column.so its creating me a problem.

Jojiju said...

nice dashboard,but it is creating one problem.the marker is appended at the end of the column when I start to drag the first div of first column.

I mean what should I do if I started to drag and before I end drag I want to keep it in original position like in Igoogle?
marker is not created in the position I leave instead at the end of column.so its creating me a problem.

Unknown said...

Nice. But I'd like to create x icon for each panel and - to minimize the panel. How can I do this? Any help would be appreciated.

skypoet said...

Hi Vu,

Check this out for an example.

Click on the left diamond to open and close the pane. View the source and look for the hideShow method. I think you want to do something like that.

Have fun!

Anonymous said...

Cool, works like a charm ! Great piece of code ;-)

Stefanovitch said...

Hi Skypoet,

This is excellent, just what I have been looking for, one feature that I would like is to limit the number of portals that can go vertically, on a page with a fixed height the portals can extend beyond the page - not a problem with a liquid height – but is with a fixed height, therefore is it possible to set a limit so that no more than 5 portals can occupy a column? I am looking through the code to see if it is possible but my coding skills are a bit limited.

Love the rest of your work.

Take care

Stefanovitch