Search code examples
grailsgrails-2.0custom-tags

Loading static resources from a Grails custom tag, with disposition: 'head'


I'm trying to write a Grails custom tag that (among other things) triggers inclusion of a resource, so something like <myTags:view name="foo"/> would load, say, js/views/foo.js. And I want it loaded with disposition: 'head'.

I could use <r:external/>, but that wouldn't put it in the <head>, it would just produce an inline <script/> tag. And I could use <r.script/>, but that doesn't let me reference a path; I'd have to have my custom tag read the file and dump it to out.

Now, if foo.js was its own module, I could do something like: r.require([module: 'foo']), but it's not; part of the point of this is that I don't want to have to declare all of these files in ApplicationResources.groovy. But maybe I could have ApplicationResources.groovy create the modules programmatically, by reading through the available files -- is that possible? Or is there a better way?


Solution

  • I ended up going in the direction of having ApplicationResources.groovy create modules programmatically, so the custom tag can use <r:require/>.

    The idea is, for each Backbone view, under web-app/myApp/views, there's a Backbone view in a .js file, and a Handlebars template in a .handlebars file (with the same name, by convention). The .handlebars file gets declared as an ordinary module, but gets precompiled by the Handlebars-Resources plugin.

    Some code in ApplicationResources.groovy finds all the views and creates corresponding resource modules:

    GrailsApplication grailsApplication = Holders.getGrailsApplication()
    File viewsDir = grailsApplication.parentContext.getResource("myApp/views").file;
    if (viewsDir.exists() && viewsDir.isDirectory() && viewsDir.canRead()) {
        String[] viewsJS = viewsDir.list().findAll { name -> 
            name.endsWith("View.js") 
        }
        String[] views = viewsJS.collect { name ->
            name.substring(0, name.length() - ".js".length())
        }
    
        for (view in views) {
            "${view}" {
                dependsOn 'backbone', 'backbone_relational', 'handlebars'
                resource url: "dpg/views/${view}.handlebars", 
                         attrs: [type: 'js'], 
                         disposition: 'head'
                resource url: "dpg/views/${view}.js", 
                         disposition: 'head'
    
            }
        }
    }
    

    Then the taglib:

    class ViewsTagLib {
        static namespace = "myApp"
    
        def view = { attrs ->
            r.require(module: "${attrs.name}View")
            out << "<${attrs.tagName} id='${attrs.id}'></${attrs.tagName}>"
        }
    }
    

    Invoking it in a GSP:

    <myApp:view tagName="div" name="foo" id="foo1"/>
    <myApp:view tagName="div" name="foo" id="foo2"/>
    

    Produces:

    <html>
        <head>
            ...
            <!--
                Automagically generated modules (included only once).
                Should put these in the same bundle, but handlebars-resources
                gets confused.
            -->
            <script src="/myApp/static/bundle-bundle_fooView_handlebars.js" 
                    type="text/javascript" ></script>
            <script src="/myApp/static/bundle-bundle_fooView_head.js" 
                    type="text/javascript" ></script>
        </head>
        <body>
            ...
            <div id="foo1"></div> <!-- backbone placeholder for 1st view instance-->
            <div id="foo2"></div> <!-- backbone placeholder for 2nd view instance-->
        </body>
    </html>
    

    It's not pretty but the mess is mostly hidden, and it should cut down considerably on boilerplate and on opportunities to forget to add magic strings to multiple files.