source: trunk/grails-app/taglib/dbnp/studycapturing/WizardTagLib.groovy @ 101

Last change on this file since 101 was 101, checked in by duh, 10 years ago
  • refactored the wizard initial page to dynamically load pageOne, instead of rendering pageOne within the template itself. This was done because otherwise both the mainPage and pageOne had to contain duplicate logic. The ajaxified webflow works better this way.
  • added an <wizard:ajaxFlowRedirect...> tag which renders javascript code executing a jQuery ajax call (it actually wraps around <wizard:button...> tag but lifts the ajax call out of the button and wraps javascript tags around it)
  • improved the help / tooltip workings
  • extended the <wizard:button...> with an afterSuccess argument which executes some javascript after success. This is different from the default submitToRemote 'after' behaviour which is always executed in parallel with the ajax success call (hence, you javascript cannot access ajax result data while the afterSuccess method can)
  • Property svn:keywords set to Rev Author Date
File size: 6.9 KB
Line 
1package dbnp.studycapturing
2
3import org.codehaus.groovy.grails.plugins.web.taglib.JavascriptTagLib
4
5/**
6 * Wizard tag library
7 *
8 * @author Jeroen Wesbeek
9 * @since 20100113
10 * @package wizard
11 *
12 * Revision information:
13 * $Rev: 101 $
14 * $Author: duh $
15 * $Date: 2010-01-20 16:01:26 +0000 (wo, 20 jan 2010) $
16 */
17class WizardTagLib extends JavascriptTagLib {
18        // define the tag namespace (e.g.: <wizard:action ... />
19        static namespace = "wizard"
20
21        // define the AJAX provider to use
22        static ajaxProvider = "jquery"
23
24        // define default text field width
25        static defaultTextFieldSize = 25;
26
27        /**
28         * ajaxButton tag, this is a modified version of the default
29         * grails submitToRemote tag to work with grails webflows.
30         * Usage is identical to submitToRemote with the only exception
31         * that a 'name' form element attribute is required. E.g.
32         * <wizard:ajaxButton name="myAction" value="myButton ... />
33         *
34         * you can also provide a javascript function to execute after
35         * success. This behaviour differs from the default 'after'
36         * action which always fires after a button press...
37         *
38         * @see http://blog.osx.eu/2010/01/18/ajaxifying-a-grails-webflow/
39         * @see http://www.grails.org/WebFlow
40         * @see http://www.grails.org/Tag+-+submitToRemote
41         * @todo perhaps some methods should be moved to a more generic
42         *        'webflow' taglib
43         * @param Map attributes
44         * @param Closure body
45         */
46        def ajaxButton = { attrs, body ->
47                // get the jQuery version
48                def jQueryVersion = grailsApplication.getMetadata()['plugins.jquery']
49
50                // fetch the element name from the attributes
51                def elementName = attrs['name'].replaceAll(/ /, "_")
52
53                // javascript function to call after success
54                def afterSuccess = attrs['afterSuccess']
55
56                // generate a normal submitToRemote button
57                def button = submitToRemote(attrs, body)
58
59                /**
60                 * as of now (grails 1.2.0 and jQuery 1.3.2.4) the grails webflow does
61                 * not properly work with AJAX as the submitToRemote button does not
62                 * handle and submit the form properly. In order to support webflows
63                 * this method modifies two parts of a 'normal' submitToRemote button:
64                 *
65                 * 1) replace 'this' with 'this.form' as the 'this' selector in a button
66                 *    action refers to the button and / or the action upon that button.
67                 *    However, it should point to the form the button is part of as the
68                 *    the button should submit the form data.
69                 * 2) prepend the button name to the serialized data. The default behaviour
70                 *    of submitToRemote is to remove the element name altogether, while
71                 *    the grails webflow expects a parameter _eventId_BUTTONNAME to execute
72                 *    the appropriate webflow action. Hence, we are going to prepend the
73                 *    serialized formdata with an _eventId_BUTTONNAME parameter.
74                 */
75                if (jQueryVersion =~ /^1.([1|2|3]).(.*)/) {
76                        // fix for older jQuery plugin versions
77                        button = button.replaceFirst(/data\:jQuery\(this\)\.serialize\(\)/, "data:\'_eventId_${elementName}=1&\'+jQuery(this.form).serialize()")
78                } else {
79                        // as of jQuery plugin version 1.4.0.1 submitToRemote has been modified and the
80                        // this.form part has been fixed. Consequently, our wrapper has changed as well...
81                        button = button.replaceFirst(/data\:jQuery/, "data:\'_eventId_${elementName}=1&\'+jQuery")
82                }
83 
84                // add an after success function call?
85                // usefull for performing actions on success data (hence on refreshed
86                // wizard pages, such as attaching tooltips)
87                if (afterSuccess) {
88                        button = button.replaceFirst(/\.html\(data\)\;/, '.html(data);' + afterSuccess + ';')
89                }
90
91                // replace double semi colons
92                button = button.replaceAll(/;{2,}/, '!!!')
93               
94                // render button
95                out << button
96        }
97
98        /**
99         * generate ajax webflow redirect javascript
100         *
101         * As we have an Ajaxified webflow, the initial wizard page
102         * cannot contain a wizard form, as upon a failing submit
103         * (e.g. the form data does not validate) the form should be
104         * shown again. However, the Grails webflow then renders the
105         * complete initial wizard page into the success div. As this
106         * ruins the page layout (a page within a page) we want the
107         * initial page to redirect to the first wizard form to enter
108         * the webflow correctly. We do this by emulating an ajax post
109         * call which updates the wizard content with the first wizard
110         * form.
111         *
112         * Usage: <wizard:ajaxFlowRedirect form="form#wizardForm" name="next" url="[controller:'wizard',action:'pages']" update="[success:'wizardPage',failure:'wizardError']" />
113         * form = the form identifier
114         * name = the action to execute in the webflow
115         * update = the divs to update upon success or error
116         *
117         * Example initial webflow action to work with this javascript:
118         * ...
119         * mainPage {
120         *      render(view: "/wizard/index")
121         *      onRender {
122         *              flow.page = 1
123         *      }
124         *      on("next").to "pageOne"
125         * }
126         * ...
127         *
128         * @param Map attributes
129         * @param Closure body
130         */
131        def ajaxFlowRedirect = { attrs, body ->
132                // define AJAX provider
133                setProvider([library: ajaxProvider])
134
135                // generate an ajax button
136                def button = this.ajaxButton(attrs, body)
137
138                // strip the button part to only leave the Ajax call
139                button = button.replaceFirst(/<[^\"]*\"jQuery.ajax/,'jQuery.ajax')
140                button = button.replaceFirst(/return false.*/,'')
141
142                // change form if a form attribute is present
143                if (attrs.get('form')) {
144                        button = button.replaceFirst(/this\.form/,
145                                "\\\$('" + attrs.get('form') + "')"
146                        )
147                }
148
149                // generate javascript
150                out << '<script language="JavaScript">'
151                out << '$(document).ready(function() {'
152                out << button
153                out << '});'
154                out << '</script>'
155        }
156
157        /**
158         * wizard navigation buttons render wrapper, in order to be able to add
159         * functionality in the future
160         */
161        def previousNext = {attrs ->
162                // define AJAX provider
163                setProvider([library: ajaxProvider])
164
165                // render navigation buttons
166                out << render(template: "/wizard/common/buttons")
167        }
168
169        /**
170         * render the content of a particular wizard page
171         * @param Map attrs
172         * @param Closure body
173         */
174        def pageContent = {attrs, body ->
175                // define AJAX provider
176                setProvider([library: ajaxProvider])
177
178                // render new body content
179                out << render(template: "/wizard/common/tabs")
180                out << '<div class="content">'
181                out << body()
182                out << '</div>'
183                out << render(template: "/wizard/common/navigation")
184        }
185
186        /**
187         * render a textFieldElement
188         */
189        def textFieldElement = {attrs, body ->
190                // set default size, or scale to max length if it is less than the default size
191                if (!attrs.get("size")) {
192                        if (attrs.get("maxlength")) {
193                                attrs.size = ((attrs.get("maxlength") as int) > defaultTextFieldSize) ? defaultTextFieldSize : attrs.get("maxlength")
194                        } else {
195                                attrs.size = defaultTextFieldSize
196                        }
197                }
198
199                // render a text element
200                out << '<div class="element">'
201                out << ' <div class="description">'
202                out << attrs.get('description')
203                out << ' </div>'
204                out << ' <div class="input">'
205                out << textField(attrs)
206                out << ' </div>'
207
208                // add help icon?
209                if (body()) {
210                        out << ' <div class="help">'
211                        out << '  <div class="icon"></div>'
212                        out << '  <div class="content">'
213                        out << '    ' + body()
214                        out << '  </div>'
215                        out << ' </div>'
216                }
217
218                out << '</div>'
219        }
220}
Note: See TracBrowser for help on using the repository browser.