![]()
The Grails framework is a very powerful MVC web framework based on technology that has proven itself (Spring, Hibernate, Java, etc). For a while now Grails has supported Spring webflows which is a great way to develop wizard like user interfaces and underlying logic. While Grails webflows work very well for full page refreshes, they do not really work in an Ajax manner to allow rendering of fractions of pages. This would be useful for a number of reasons:
- separation of templates and template elements
- limit page rendering to the things that change instead of the complete page
- reduce bandwidth and only download require JavaScript and CSS once when the complete page is loaded.
- bookmarking the wizard would only bookmark the initial wizard page
- etc
In order for a webflow to work with Ajax the webflow should be able to render and refresh only portions of a front-end. While Grails does provide a submitToRemote tag to perform an Ajax submit, the tag itself does not work with webflows. That is because a Grails webflow links form buttons to flow actions. So, if you have a submit button named ‘delete’, the delete action will be executed in the particular flow execution element. This is done by adding ‘_eventId_delete=1′ to the POST data. However, the Grails tag submitToRemote does not do that, so the webflow is not executed properly.
So, to solve this particular issue we have to create a new tag library to extend / modify the default submitToRemote tag behaviour. In the example below I have created a new tag library called myTagLib (grails create-tag-lib com.example.myTagLib) in which I extend the default tag library. It currently contains one method (‘tag’) called ajaxButton which wraps around the submitToRemote tag to add the webflow variable (_eventId_buttonName) to the Ajax call (note that I use jQuery as Ajax provider):
package com.example
import org.codehaus.groovy.grails.plugins.web.taglib.JavascriptTagLib
class MyTagLib extends JavascriptTagLib {
// define the tag namespace (e.g.: <my:action ... />
static namespace = "my"
/**
* ajaxButton tag, this is a modified version of the default
* grails submitToRemote tag to work with grails webflows.
* Usage is identical to submitToRemote with the only exception
* that a 'name' form element attribute is required. E.g.
* <my:ajaxButton name="myAction" value="myButton ... />
*
* @see http://www.grails.org/WebFlow
* @see http://www.grails.org/Tag+-+submitToRemote
* @param Map attributes
* @param Closure body
*/
def ajaxButton = {attrs, body ->
// get the jQuery version
def jQueryVersion = grailsApplication.getMetadata()['plugins.jquery']
// fetch the element name from the attributes
def elementName = attrs['name'].replaceAll(/ /, "_")
// generate a normal submitToRemote button
def button = submitToRemote(attrs, body)
/**
* as of now (grails 1.2.0 and jQuery 1.3.2.4) the grails webflow does
* not properly work with AJAX as the submitToRemote button does not
* handle and submit the form properly. In order to support webflows
* this method modifies two parts of a 'normal' submitToRemote button:
*
* 1) replace 'this' with 'this.form' as the 'this' selector in a button
* action refers to the button and / or the action upon that button.
* However, it should point to the form the button is part of as the
* the button should submit the form data.
* 2) prepend the button name to the serialized data. The default behaviour
* of submitToRemote is to remove the element name altogether, while
* the grails webflow expects a parameter _eventId_BUTTONNAME to execute
* the appropriate webflow action. Hence, we are going to prepend the
* serialized formdata with an _eventId_BUTTONNAME parameter.
*/
if (jQueryVersion =~ /^1.([1|2|3]).(.*)/) {
// fix for older jQuery plugin versions
button = button.replaceFirst(/data\:jQuery\(this\)\.serialize\(\)/, "data:\'_eventId_${elementName}=1&\'+jQuery(this.form).serialize()")
} else {
// as of jQuery plugin version 1.4.0.1 submitToRemote has been modified and the
// this.form part has been fixed. Consequently, our wrapper has changed as well...
button = button.replaceFirst(/data\:jQuery/, "data:\'_eventId_${elementName}=1&\'+jQuery")
}
// render button
out << button
}
}
Now that we have this tag library in place we can add a webflow compatible submitToRemote button by adding the following tag:
<my:ajaxButton name="next" value="next »" url="[controller:'wizard',action:'pages']" update="[success:'wizardPage',failure:'wizardError']" class="prevnext" />
As the new ajaxButton tag is a wrapper of the submitToRemote tag it supports all arguments the submitToRemote tag supports, but in contrast it does not ignore the name argument. In fact, the name argument is required as it is responsible for executing webflow actions. The only thing which remains is to make sure the webflow only renders portions, and not complete pages.
Observe the following WizardController code:
package com.example
class WizardController {
def index = {
redirect(action:'pages')
}
def pagesFlow = {
// render the main wizard page
mainPage {
render(view:"/wizard/index")
on("next") {
println "next page!"
}.to "pageTwo"
}
// render page one
pageOne {
render(view:"_one")
on("next") {
println "next page!"
}.to "pageTwo"
}
// render page two
pageTwo {
render(view:"_two")
on("next") {
println "next page!"
}.to "pageThree"
on("previous") {
println "previous page!"
}.to "pageOne"
}
}
}
As seen in the webflow we have a two paged wizard. The mainpage renders the initial wizard page (index.gsp), while the pageOne and pageTwo actions only render portions of a page (respectively: pages/_one.gsp and pages/_two.gsp). We could split this up into the following four templates. Note that you can seperate templates even more, but for this example I will stick to these four:
index.gsp
<html> <head> <meta name="layout" content="main" /> </head> <body> <g:render template="common/wizard"/> </body> </html>
common/_wizard.gsp
<div id="wizard" class="wizard">
<h1>Proof of concept AJAXified Webflow Wizard</h1>
<g:form action="pages" name="_wizard" >
<div id="wizardPage">
<g:render template="pages/one"/>
</div>
<div id="wizardError" class="error"/>
</g:form>
</div>
pages/_one.gsp
...my form content for page 1...<br /> <my:ajaxButton name="next" value="next »" url="[controller:'wizard',action:'pages']" update="[success:'wizardPage',failure:'wizardError']" class="prevnext" />
pages/_two.gsp
...my form content for page 2...<br /> <my:ajaxButton name="previous" value="« prev" url="[controller:'wizard',action:'pages']" update="[success:'wizardPage',failure:'wizardError']" class="prevnext" />
As you can see, index.gsp will render default layout (add your CSS here) and renders common/_wizard.gsp. Wizard.gsp will, on it’s turn, render the first web flow template (pages/_one.gsp) which includes a form and the ajaxButton we created in the tag library above. This button will now use and Ajax call to submit to our webflow and execute the on(“next”) action in the pageOne part of our webflow, which will make the logic jump to the pageTwo part. Upon entry pageTwo will render pages/_two.gsp and return the rendered content to the Ajax call.
The ajaxButton contains update=”[success:'wizardPage'...]” which makes the Ajax call refresh the wizardPage div with the result of the Ajax call. Hence, the wizardPage div will now contain the rendered content of pages/_two.gsp with it’s own form.
Hence, we have Ajaxified our Grails webflow
Keep up posting Grails-articles, we need them.
Hey Jeroen!
Volgens mij ben je nog steeds in het bezit van chantagemateriaal in de vorm van grasbekijkende, krantenbakbespringende foto’s….
Zou ik toch nog een keer van je krijgen?
Bevalt het weer terug in good old Holland? Of krijg je al weer reiskriebels?
Lees net je geweldige ervaringen met Apple Care…
Sta op het punt om een macbookpro aan te schaffen 13 inch.
Nog nuttige tips?
Verder alles goed?
Groetjes,
Charly
Weet je mischien ook hoe je een searchbox icm een list kunt combineren met een webflow?
Ik kom er niet uit. Elke hulp is welkom!
It worked great thank you !!!!
I just released a Grails plugin that enables ajaxified webflows: http://grails.org/plugin/ajaxflow
Hi Jeroen,
Thanks for the great plugin! I have everything working ok but I am unsure of the best way to handle validation errors using the Ajax webflow. Do you have any suggestions?
Thanks,
Jim.
Hi Jim,
Good to hear you like the plugin
I am sorry though for the late reply. As for propagating validation errors to the view, I use an error template which is rendered in the _page_footer template. This error template generates javascript to mark input fields red, and builds a jquery-ui overlay which sums up invalid fields.
In thecontroller I set the flash.wizardErrors variable with validation errors of all instances in a particular page. You can see a live continuous integration version of the wizard I built here.
Example code:
myInstances.each { if (!it.validate()) { it.errors.getAllErrors.each { error -> flash.errors[ error.getArguments()[0] ] = validationTagLib.message(error: error) } }I hope this helps
Cheers, Jeroen
Hey Jeroen,
Thanks for the great plugin! But, I have certain issues when I tried to implement the Ajaxwebflow with some forms on the pages. I am not able to display the validation errors on the page. Can you give me an example where this is implemented so that I can use it as a guideline.
Thanks.
Hi Vikram, have tried the solution I mentioned above to Jim? Perhaps that might be of help to you as well?
Cheers, Jeroen
Hi Jeroen.
I’ve begun using the Ajaxified Webflows plugin and so far it’s been absolutely excellent.
I’ve have a requirement where I need to use a webflow for editing a domain object. In a standard Controller or a non-ajaxified webflow, I can simply pass an id parameter to the Controller and retrieve the id using the following:
def edit = {
def id = params.eventId
def event = Event.findById(id)
….
}
However, I’m having problems trying to pass such a request parameter into an ajaxified-webflow. Using the wizard example generated by the plugin, the following code does not work:
def pagesFlow = {
// Render the main wizard page which immediately
// triggers the ‘next’ action (hence, the main
// page dynamically renders the study template
// and makes the flow jump to the study logic)
mainPage {
render(view: “index”)
on(“next”) {
def id = params.eventId
def event = Event.findByIdAndClient(id, client)
log.debug(“params: ” + params)
….
}
The log statement always displays:
2012-01-11 11:34:35,950 [http-8080-1] DEBUG myApp.EventController – params: [id:, _eventId_next:1, execution:[e5s1, e5s1], action:pages, controller:event]
Knowing that the _ajaxflow.gsp uses “” to initiate the ajaxified-webflow, I think it makes sense that no original url request parameters make it to the pagesFlow. While that seems to make sense, the question is: how can I pass request parameters into the initial state(s) or an ajaxified-webflow?
Thanks,
Eric
A little more digging and it appears I found the answer to my own question.
http://grails.1312388.n4.nabble.com/how-to-pass-an-id-to-ajaxFlow-td3220999.html
On the other hand, the information in http://grails.1312388.n4.nabble.com/how-to-pass-an-id-to-ajaxFlow-td3220999.html appears to center on the use of the ajaxButton tag. My particular requirements mandate that I pass the id on the initial ajaxified-webflow such as from the initial triggerEvent.
Am I on the right track? Is this currently supported, is there some work around, or would this be considered a future enhancement?
After a short while of digging into the plugin code I realized my problem is account for it the design and very easy to accomplish. My original _ajaxflow.gsp, as generated from the example wizard flow, was:
You can pass additional form parameters to the initial webflow by adding hidden input parameters within the body of your af:flow element. The example that worked for me is:
Excellent work Jeroen!
Hi Eric,
I am also struggling with submitting request params with the … can you please again paste the example you mention in this post .. it seems it didn’t show up.
Thanks
Manoj
Eric,
I am struggling with the same problem as yours .. can you please re-post the changes you made to the _ajaxflow.gsp.
Thanks
Manoj
Hi Eric,
Sorry for not noticing your posts sooner than just now. Unfortunately my cable provider blocks smtp, so my blog is unable to send out notification emails when someone posts comments
Perhaps I should move the blog out of my meter box onto an external server
But I see you already found the solution to your question? So, if I understand correctly you are using a flow to edit the properties of one particular domain object?
It’s indeed possible to add the id to the hidden field in the form specification, however as this id is being used throughout the whole web flow / wizard, I would probably store it in the flow scope instead of passing it along every time in a hidden post variable.
The flow scope is available throughout the lifetime of the web flow. So I would probably either store the id in the flowscope, or instantiate the domain object by id and store that in the flowscope so you can use and modify that object throughout the web flow.
I hope this answer is of help? Let me know if you got things working
Cheers, Jeroen
Can I manage the flow-state on the client side instead of session? For load balancing server applications.
Jie, if your load balancer is configured to direct user sessions to the same server it should not be a problem. However if your load balancer is configured to balance on every requests (so individual requests end up on different servers) you should probably set up tomcat session clustering as well. But then you would probably run into issues with the file uploads themselves as they are being stored locally (a second onSuccess request might be directed to another server where the file does not exist). So it would probably best practice to set up your load balancer to keep user sessions on the same server.