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

Last change on this file since 240 was 240, checked in by duh, 11 years ago
  • added ajaxOnChange parameter support for <wizard:...> elements. ajaxOnChange now performs an ajaxSubmit to the current form. Usefull for handeling select changes and dynamic forms. When using the ajaxOnChange attribute you can also add the 'url', 'update' and 'afterSuccess' parameters. In your webflow the action is triggered that you put in the ajaxOnChange argument.
  • example:

<wizard:templateElement name="template" description="Template" value="${study?.template}" entity="${dbnp.studycapturing.Subject}" ajaxOnChange="switchTemplate" url="[controller:'wizard',action:'pages']" update="[success:'wizardPage',failure:'wizardError']" afterSuccess="onWizardPage()" >

The template to use for this study

</wizard:templateElement>

Will submit on change, triggering the 'switchTemplate' action in your current webflow:

on("switchTemplate") {

println params

}.to "study"

  • Property svn:keywords set to
    Date
    Author
    Rev
File size: 15.0 KB
Line 
1package dbnp.studycapturing
2
3import org.codehaus.groovy.grails.plugins.web.taglib.JavascriptTagLib
4import dbnp.studycapturing.*
5import dbnp.data.*
6
7/**
8 * Wizard tag library
9 *
10 * @author Jeroen Wesbeek
11 * @since 20100113
12 * @package wizard
13 *
14 * Revision information:
15 * $Rev: 240 $
16 * $Author: duh $
17 * $Date: 2010-03-05 16:26:05 +0000 (vr, 05 mrt 2010) $
18 */
19class WizardTagLib extends JavascriptTagLib {
20        // define the tag namespace (e.g.: <wizard:action ... />
21        static namespace = "wizard"
22
23        // define the AJAX provider to use
24        static ajaxProvider = "jquery"
25
26        // define default text field width
27        static defaultTextFieldSize = 25;
28
29        /**
30         * ajaxButton tag, this is a modified version of the default
31         * grails submitToRemote tag to work with grails webflows.
32         * Usage is identical to submitToRemote with the only exception
33         * that a 'name' form element attribute is required. E.g.
34         * <wizard:ajaxButton name="myAction" value="myButton ... />
35         *
36         * you can also provide a javascript function to execute after
37         * success. This behaviour differs from the default 'after'
38         * action which always fires after a button press...
39         *
40         * @see http://blog.osx.eu/2010/01/18/ajaxifying-a-grails-webflow/
41         * @see http://www.grails.org/WebFlow
42         * @see http://www.grails.org/Tag+-+submitToRemote
43         * @todo perhaps some methods should be moved to a more generic
44         *        'webflow' taglib or plugin
45         * @param Map attributes
46         * @param Closure body
47         */
48        def ajaxButton = { attrs, body ->
49                // get the jQuery version
50                def jQueryVersion = grailsApplication.getMetadata()['plugins.jquery']
51
52                // fetch the element name from the attributes
53                def elementName = attrs['name'].replaceAll(/ /, "_")
54
55                // javascript function to call after success
56                def afterSuccess = attrs['afterSuccess']
57
58                // src parameter?
59                def src = attrs['src']
60                def alt = attrs['alt']
61
62                // generate a normal submitToRemote button
63                def button = submitToRemote(attrs, body)
64
65                /**
66                 * as of now (grails 1.2.0 and jQuery 1.3.2.4) the grails webflow does
67                 * not properly work with AJAX as the submitToRemote button does not
68                 * handle and submit the form properly. In order to support webflows
69                 * this method modifies two parts of a 'normal' submitToRemote button:
70                 *
71                 * 1) replace 'this' with 'this.form' as the 'this' selector in a button
72                 *    action refers to the button and / or the action upon that button.
73                 *    However, it should point to the form the button is part of as the
74                 *    the button should submit the form data.
75                 * 2) prepend the button name to the serialized data. The default behaviour
76                 *    of submitToRemote is to remove the element name altogether, while
77                 *    the grails webflow expects a parameter _eventId_BUTTONNAME to execute
78                 *    the appropriate webflow action. Hence, we are going to prepend the
79                 *    serialized formdata with an _eventId_BUTTONNAME parameter.
80                 */
81                if (jQueryVersion =~ /^1.([1|2|3]).(.*)/) {
82                        // fix for older jQuery plugin versions
83                        button = button.replaceFirst(/data\:jQuery\(this\)\.serialize\(\)/, "data:\'_eventId_${elementName}=1&\'+jQuery(this.form).serialize()")
84                } else {
85                        // as of jQuery plugin version 1.4.0.1 submitToRemote has been modified and the
86                        // this.form part has been fixed. Consequently, our wrapper has changed as well...
87                        button = button.replaceFirst(/data\:jQuery/, "data:\'_eventId_${elementName}=1&\'+jQuery")
88                }
89 
90                // add an after success function call?
91                // usefull for performing actions on success data (hence on refreshed
92                // wizard pages, such as attaching tooltips)
93                if (afterSuccess) {
94                        button = button.replaceFirst(/\.html\(data\)\;/, '.html(data);' + afterSuccess + ';')
95                }
96
97                // got an src parameter?
98                if (src) {
99                        def replace = 'type="image" src="' + src + '"'
100
101                        if (alt) replace = replace + ' alt="' + alt + '"'
102
103                        button = button.replaceFirst(/type="button"/, replace)
104                }
105
106                // replace double semi colons
107                button = button.replaceAll(/;{2,}/, ';')
108               
109                // render button
110                out << button
111        }
112
113        /**
114         * generate a ajax submit JavaScript
115         * @see WizardTagLib::ajaxFlowRedirect
116         * @see WizardTagLib::baseElement (ajaxSubmitOnChange)
117         */
118        def ajaxSubmitJs = { attrs, body ->
119                // define AJAX provider
120                setProvider([library: ajaxProvider])
121
122                // got a function name?
123                def functionName = attrs.remove('functionName')
124                if (functionName && !attrs.get('name')) {
125                        attrs.name = functionName
126                }
127
128                // generate an ajax button
129                def button = this.ajaxButton(attrs, body)
130
131                // strip the button part to only leave the Ajax call
132                button = button.replaceFirst(/<[^\"]*\"jQuery.ajax/,'jQuery.ajax')
133                button = button.replaceFirst(/return false.*/,'')
134
135                // change form if a form attribute is present
136                if (attrs.get('form')) {
137                        button = button.replaceFirst(/this\.form/,
138                                "\\\$('" + attrs.get('form') + "')"
139                        )
140                }
141
142                out << button
143        }
144
145        /**
146         * generate ajax webflow redirect javascript
147         *
148         * As we have an Ajaxified webflow, the initial wizard page
149         * cannot contain a wizard form, as upon a failing submit
150         * (e.g. the form data does not validate) the form should be
151         * shown again. However, the Grails webflow then renders the
152         * complete initial wizard page into the success div. As this
153         * ruins the page layout (a page within a page) we want the
154         * initial page to redirect to the first wizard form to enter
155         * the webflow correctly. We do this by emulating an ajax post
156         * call which updates the wizard content with the first wizard
157         * form.
158         *
159         * Usage: <wizard:ajaxFlowRedirect form="form#wizardForm" name="next" url="[controller:'wizard',action:'pages']" update="[success:'wizardPage',failure:'wizardError']" />
160         * form = the form identifier
161         * name = the action to execute in the webflow
162         * update = the divs to update upon success or error
163         *
164         * OR: to generate a JavaScript function you can call yourself, use 'functionName' instead of 'name'
165         *
166         * Example initial webflow action to work with this javascript:
167         * ...
168         * mainPage {
169         *      render(view: "/wizard/index")
170         *      onRender {
171         *              flow.page = 1
172         *      }
173         *      on("next").to "pageOne"
174         * }
175         * ...
176         *
177         * @param Map attributes
178         * @param Closure body
179         */
180        def ajaxFlowRedirect = { attrs, body ->
181                // generate javascript
182                out << '<script type="text/javascript">'
183                out << '$(document).ready(function() {'
184                out << ajaxSubmitJs(attrs, body)
185                out << '});'
186                out << '</script>'
187        }
188
189        /**
190         * render the content of a particular wizard page
191         * @param Map attrs
192         * @param Closure body  (help text)
193         */
194        def pageContent = {attrs, body ->
195                // define AJAX provider
196                setProvider([library: ajaxProvider])
197
198                // render new body content
199                out << render(template: "/wizard/common/tabs")
200                out << '<div class="content">'
201                out << body()
202                out << '</div>'
203                out << render(template: "/wizard/common/navigation")
204                out << render(template: "/wizard/common/error")
205        }
206
207        /**
208         * generate a base form element
209         * @param String        inputElement name
210         * @param Map           attributes
211         * @param Closure       help content
212         */
213        def baseElement = { inputElement, attrs, help ->
214                // work variables
215                def description = attrs.remove('description')
216                def addExampleElement = attrs.remove('addExampleElement')
217                def addExample2Element  = attrs.remove('addExample2Element')
218
219                // got an ajax onchange action?
220                def ajaxOnChange = attrs.remove('ajaxOnChange')
221                if (ajaxOnChange) {
222                        if (!attrs.onChange) attrs.onChange = ''
223
224                        // add onChange AjaxSubmit javascript
225                        attrs.onChange += ajaxSubmitJs(
226                                [
227                                        functionName: ajaxOnChange,
228                                        url: attrs.get('url'),
229                                        update: attrs.get('update'),
230                                        afterSuccess: attrs.get('afterSuccess')
231                                ],
232                                ''
233                        )
234                }
235
236                // execute inputElement call
237                def renderedElement = "$inputElement"(attrs)
238
239                // if false, then we skip this element
240                if (!renderedElement) return false
241
242                // render a form element
243                out << '<div class="element">'
244                out << ' <div class="description">'
245                out << description
246                out << ' </div>'
247                out << ' <div class="input">'
248                out << renderedElement
249                if(help()) {
250                        out << '        <div class="helpIcon"></div>'
251                }
252
253                // add an disabled input box for feedback purposes
254                // @see dateElement(...)
255                if (addExampleElement) {
256                        def exampleAttrs = new LinkedHashMap()
257                        exampleAttrs.name = attrs.get('name')+'Example'
258                        exampleAttrs.class  = 'isExample'
259                        exampleAttrs.disabled = 'disabled'
260                        exampleAttrs.size = 30
261                        out << textField(exampleAttrs)
262                }
263
264                // add an disabled input box for feedback purposes
265                // @see dateElement(...)
266                if (addExample2Element) {
267                        def exampleAttrs = new LinkedHashMap()
268                        exampleAttrs.name = attrs.get('name')+'Example2'
269                        exampleAttrs.class  = 'isExample'
270                        exampleAttrs.disabled = 'disabled'
271                        exampleAttrs.size = 30
272                        out << textField(exampleAttrs)
273                }
274
275                out << ' </div>'
276
277                // add help content if it is available
278                if (help()) {
279                        out << '  <div class="helpContent">'
280                        out << '    ' + help()
281                        out << '  </div>'
282                }
283
284                out << '</div>'
285        }
286
287        /**
288         * render a textFieldElement
289         * @param Map attrs
290         * @param Closure body  (help text)
291         */
292        def textFieldElement = { attrs, body ->
293                // set default size, or scale to max length if it is less than the default size
294                if (!attrs.get("size")) {
295                        if (attrs.get("maxlength")) {
296                                attrs.size = ((attrs.get("maxlength") as int) > defaultTextFieldSize) ? defaultTextFieldSize : attrs.get("maxlength")
297                        } else {
298                                attrs.size = defaultTextFieldSize
299                        }
300                }
301
302                // render template element
303                baseElement.call(
304                        'textField',
305                        attrs,
306                        body
307                )
308        }
309
310
311        /**
312         * render a select form element
313         * @param Map attrs
314         * @param Closure body  (help text)
315         */
316        def selectElement = { attrs, body ->
317                baseElement.call(
318                        'select',
319                        attrs,
320                        body
321                )
322        }
323
324        /**
325         * render a checkBox form element
326         * @param Map attrs
327         * @param Closure body  (help text)
328         */
329        def checkBoxElement = { attrs, body ->
330                baseElement.call(
331                        'checkBox',
332                        attrs,
333                        body
334                )
335        }
336
337        /**
338         * render a dateElement
339         * NOTE: datepicker is attached through wizard.js!
340         * @param Map attrs
341         * @param Closure body  (help text)
342         */
343        def dateElement = { attrs, body ->
344                // transform value?
345                if (attrs.value instanceof Date) {
346                        // transform date instance to formatted string (dd/mm/yyyy)
347                        attrs.value = String.format('%td/%<tm/%<tY', attrs.value)
348                }
349               
350                // set some textfield values
351                attrs.maxlength = (attrs.maxlength) ? attrs.maxlength : 10
352                attrs.addExampleElement = true
353               
354                // render a normal text field
355                //out << textFieldElement(attrs,body)
356                textFieldElement.call(
357                        attrs,
358                        body
359                )
360        }
361
362        /**
363         * render a dateElement
364         * NOTE: datepicker is attached through wizard.js!
365         * @param Map attrs
366         * @param Closure body  (help text)
367         */
368        def timeElement = { attrs, body ->
369                // transform value?
370                if (attrs.value instanceof Date) {
371                        // transform date instance to formatted string (dd/mm/yyyy)
372                        attrs.value = String.format('%td/%<tm/%<tY %<tH:%<tM', attrs.value)
373                }
374
375                attrs.addExampleElement = true
376                attrs.addExample2Element = true
377                attrs.maxlength = 16
378
379                // render a normal text field
380                //out << textFieldElement(attrs,body)
381                textFieldElement.call(
382                        attrs,
383                        body
384                )
385        }
386       
387        /**
388         * Template form element
389         * @param Map           attributes
390         * @param Closure       help content
391         */
392        def speciesElement = { attrs, body ->
393                // render template element
394                baseElement.call(
395                        'speciesSelect',
396                        attrs,
397                        body
398                )
399        }
400
401        /**
402         * Button form element
403         * @param Map           attributes
404         * @param Closure       help content
405         */
406        def buttonElement = { attrs, body ->
407                // render template element
408                baseElement.call(
409                        'ajaxButton',
410                        attrs,
411                        body
412                )
413        }
414
415        /**
416         * render a species select element
417         * @param Map attrs
418         */
419        def speciesSelect = { attrs ->
420                // fetch the speciesOntology
421                // note that this is a bit nasty, probably the ontologyName should
422                // be configured in a configuration file... --> TODO: centralize species configuration
423                def speciesOntology = Ontology.findByName('NCBI Taxonomy')
424
425                // fetch all species
426                attrs.from = Term.findAllByOntology(speciesOntology)
427
428                // got a name?
429                if (!attrs.name) {
430                        // nope, use a default name
431                        attrs.name = 'species'
432                }
433
434                out << select(attrs)
435        }
436
437        /**
438         * Template form element
439         * @param Map           attributes
440         * @param Closure       help content
441         */
442        def templateElement = { attrs, body ->
443                // render template element
444                baseElement.call(
445                        'templateSelect',
446                        attrs,
447                        body
448                )
449        }
450       
451        /**
452         * render a template select element
453         * @param Map attrs
454         */
455        def templateSelect = { attrs ->
456                def entity = attrs.remove('entity')
457
458                // fetch templates
459                attrs.from = (entity) ? Template.findAllByEntity(entity) : Template.findAll()
460
461                // got a name?
462                if (!attrs.name) {
463                        attrs.name = 'template'
464                }
465
466                // got result?
467                if (attrs.from.size() >0) {
468                        out << select(attrs)
469                } else {
470                        // no, return false to make sure this element
471                        // is not rendered in the template
472                        return false
473                }
474        }
475
476        /**
477         * Term form element
478         * @param Map           attributes
479         * @param Closure       help content
480         */
481        def termElement = { attrs, body ->
482                // render term element
483                baseElement.call(
484                        'termSelect',
485                        attrs,
486                        body
487                )
488        }
489
490        /**
491         * render a term select element
492         * @param Map attrs
493         */
494        def termSelect = { attrs ->
495                // fetch all terms
496                attrs.from = Term.findAll()     // for now, all terms as we cannot identify terms as being treatment terms...
497
498                // got a name?
499                if (!attrs.name) {
500                        attrs.name = 'term'
501                }
502
503                out << select(attrs)
504        }
505
506        def show = { attrs ->
507                // is object parameter set?
508                def o = attrs.object
509
510                println o.getProperties();
511                o.getProperties().each {
512                        println it
513                }
514
515                out << "!! test version of 'show' tag !!"
516        }
517
518        /**
519         * render table headers for all subjectFields in a template
520         * @param Map attributes
521         */
522        def templateColumnHeaders = { attrs ->
523                def template = attrs.remove('template')
524
525                // output table headers for template fields
526                template.fields.each() {
527                        out << '<div class="' + attrs.get('class') + '">' + it + '</div>'
528                }
529        }
530
531        /**
532         * render table input elements for all subjectFields in a template
533         * @param Map attributes
534         */
535        def templateColumns = { attrs, body ->
536                def subject                     = attrs.remove('subject')
537                def subjectId           = attrs.remove('id')
538                def template            = attrs.remove('template')
539                def intFields           = subject.templateIntegerFields
540                def stringFields        = subject.templateStringFields
541                def floatFields         = subject.templateFloatFields
542                def termFields          = subject.templateTermFields
543
544                // output columns for these subjectFields
545                template.fields.each() {
546                        // output div
547                        out << '<div class="' + attrs.get('class') + '">'
548
549                        switch (it.type) {
550                                case 'STRINGLIST':
551                                        // render stringlist subjectfield
552                                        if (!it.listEntries.isEmpty()) {
553                                                out << select(
554                                                        name: attrs.name + '_' + it.name,
555                                                        from: it.listEntries,
556                                                        value: (stringFields) ? stringFields.get(it.name) : ''
557                                                )                                               
558                                        } else {
559                                                out << '<span class="warning">no values!!</span>'
560                                        }
561                                        break;
562                                case 'INTEGER':
563                                        // render integer subjectfield
564                                        out << textField(
565                                                name: attrs.name + '_' + it.name,
566                                                value: (intFields) ? intFields.get(it.name) : ''
567                                        )
568                                        break;
569                                case 'FLOAT':
570                                        // render float subjectfield
571                                        out << textField(
572                                                name: attrs.name + '_' + it.name,
573                                                value: (floatFields) ? floatFields.get(it.name) : ''
574                                        )
575                                        break;
576                                default:
577                                        // unsupported field type
578                                        out << '<span class="warning">!'+it.type+'</span>'
579                                        //out << subject.getFieldValue(it.name)
580                                        break;
581                        }
582
583                        out << '</div>'
584                }
585        }
586}
Note: See TracBrowser for help on using the repository browser.