Friday, January 2, 2009

Grails Rich UI: Maintaining the tab on refresh

Ahh, after hours of hacking, I finally got this to work! (and I do mean hacking) It may not be the most elegant way of doing this, but it works. I'll re-engineer it later if necessary. I don't profess to be the worlds greatest JavaScripter, or Ajaxer, but here goes...

PROBLEM:
I use RichUI for my tabs in a Grails application. The tabs look good, and it works well, except for one problem. When you refresh the page, the tab reverts back to the first tab. So, if you do cool stuff like adding in something with a modal or dialog, and you go back to your page, it reverts back to the first tab. Sort of annoying and the customer complained.

RESEARCH:
Eric Miraglia at ericmiraglia.com sent me this helpful url: http://ericmiraglia.com/yui/demos/tabcookie.php
(Thanx!)

Which was close to what I needed. I also saw Tom Duerr's reply at the bottom of this page which was also very helpful:

However, I couldn't get Tom's stuff to work out of the box. The Ajax call just would not work for me. So here's what I did to modify it a bit.

THEORY:
I wanted to store the tab index in a session, since most of my other temp stuff was stored in the session. I didn't want to start down the cookie route in case people didn't want cookies on their machine. So, I used Tom's mods to RichUI to allow for a tab index to be inserted into the Tab View, and I stored the tab info in the session. Sounds easy, right? Well, it took me most of the day to get it working well.

IMPLEMENTATION:
First, make the mods to RichUI's TabViewRenderer.groovy just as Tom describes in the above link. Next, update your instantiation of tab viewer in your gsp page from the standard:

<richui:tabView id="tabView">

to this

< richui:tabView id="tabView" curTab="${curTab}"
event="processTab('type',${myObj?.id});">

The curTab parameter is where you insert the current tab index, starting at 0 for the left most tab, 1 for the next tab to the right, etc.

The event parameter holds a JavaScript method you want called whenever someone hits a tab.(i.e. an event occurrs) You don't have to pass anything to this JavaScript method. However, I pass in a "type" and an ID. I do this because I don't want the same tab coming up whenever the user selects another object, or if I reuse this code on another page. For instance, say some one wants to look at object 1, and they hit tab 3. Now, if they browse around and find object 2, they want to see tab 0 to start off with, not tab 3. So, I use the type, id, and tab number to make sure you are on the same page, viewing the same object. If a user selects a different item from the list, they start at the 0 tab again.

The JavaScript processTab method looks like this: (added to the gsp where the tab view is)

<script>
function processTab(type, id) {
var idx = tabView.get('activeIndex');
new Ajax.Request("/yourProject/yourController/currentTab?type=" +
type + "&id=" + id + "&currentTab=" + idx, { method:'get' });
}
</script>

Basically, the tabView gives you the active index (the one someone just clicked on). This is the value you want to pass to your controller method, along with the type of object and the id. The controller will handle setting the values in the session. (I couldn't figure out how to set the session in JavaScript, so I let the Grails controller do it via an asynchronous Ajax call.) To get the parameters to the controller, I passed the parameters in the URL call. The prototype manual indicated you can do this with 'parameter', which is probably the proper way to do this, but I couldn't figure it out right away (and my frustration with this grew exponentially by the minute), so I just went all brute force on it and passed the parameters in the URL myself. Feel free to make it better.

Now, it was time to go to the controller and create the "currentTab" method where I grabbed the values from the params object:

def currentTab = {
session.curTab = params.currentTab
session.tabType = params.type
session.tabId = new Long(params.id)
return
}

Finally, back in the .gsp where the tab view is, I added this gem of logic:
<g:if test="${(session.curTab != 0) &&
(session.tabType == 'type') &&
(session.tabId == myObj?.id)}">
<g:set var="curTab" value="${session.curTab}"/>
</g:if>
<g:else>
<g:set var="curTab" value="0"/>
</g:else>

What this basically means is, set the curTab parameter to zero UNLESS the planets align and you actually viewing the same object, of the same type, with a tab index not already zero. (the one and only true case where I actually want to save the tab index!).

After all that, it finally worked. Hopefully someone out there will figure out an easier way, or just add all this into the next release of RichUI! You can also do something similar with GrailsUI or even YUI if you want. Eric's link above does it for you using YUI and cookies if you want to go that route.

Now, I just have to figure out how I can write a test to test this! Of course, I know what a real Agile developer would say: I should have written the test first. But, I'm just glad I got this bad boy working for now. I need to eventually go live with this thing.

-Keith

PS. I modified the plugin to allow for curTab to be set. Also, I used richui 0.4 and grails 1.0.4 for this. Using the latest richui 0.6 doesn't seem to work, so I'll have to do more research. Anyway, modify the TabViewRenderer.groovy in the src directory of the plugin and change line 17 to look like this:

builder.yieldUnescaped " var tabView = new YAHOO.widget.TabView(\"${attrs.id}\", {activeIndex:\"${attrs.curTab}\"});\n"

That is necessary for the plugin to tell YUI to set the activeIndex.

Thanks to Kevin Kruse for pointing this out!