Question

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?

Was it helpful?

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top