Thursday, October 22, 2009

Session Notes - Not Your Father's Custom Tags

Presenter: Dave Klein

Custom Tags are Valuable

  • build reusable components
  • keep code out of pages
  • make pages more readable
    • even putting presentation/html code into custom tags can make the page more readable
  • encapsulate domain knowledge
    • easy for page designers to use without knowing much about the domain model, etc.

JSP Custom Tags are Painful

  • create handler class for each tag
  • implement one of several interfaces
  • implement interface's methods
  • <pure_evil>define a TLD</pure_evil>
  • add a page directive for each TLD
  • great once they exist, but because they're a pain to create people avoid them and don't get the benefit
  • JSP custom tag for hello wolrd is 2+ pages of code, equivalent GSP tag is about 6 lines

The Power of JSP Without the Pain

  • convention over configuration
  • no tld, no xml
  • no interfaces to implement
  • a TagLibrary is a single groovy class
    • can contain multiple tags within this class
  • each tag is a closure
  • don't need to declare within the page
    • if it's in the project, it's available on every page

GSP: A Quick-Start Guide

What you can do in a GSP tag

  • accept and use attributes
  • accept and conditionally use a body
  • access your domain model including all the GORM methods
  • call service classes
  • call other tags
    • other tags are called as methods from within a tag
  • access the session, request, and response

Implicit Objects in a GSP Tag

  • session (GrailsHttpSession)
  • request (HttpRequest)
  • response (GrailsContentBufferngResponse)
  • out (GSPResponseWriter)

Example Tag -- Output Groovy Group List

  • def groupLinks = {
    def groups = GroovyGroup.list()
    out << "<br/><ul>"
    groups.each { group ->
    out << "<li><a href='"
    out << createLink(action:'show', id:group.id)
    out << "'>${group}"
    out << "</a></li>"
    }
    out << "</ul>
    }
  • call tag as <g:groupLinks />

Example Tag With a Body

  • def ifLoggedIn = { attrs, body ->
    def user = session.use
    if (user)
    out << body()
    }
  • <g:ifLoggedIn>
    info for logged in users goes here
    </g:ifLoggedIn>
  • note that in the tag code, the first parameter will be treated as a map of attrs so even if you're not using attrs, but you're using the body, still need to have a dummy parameter for attrs in order for things to work correctly

Using Custom Tags Instead of <g:if>

  • def buttonBar = {
    def user = session.user
    out << "<div class='buttons'>"
    if (user) {
    // output buttons for logged in users here
    if (user.isAdmin()) {
    // output admin buttons here
    }
    } else {
    out << "<input type='button' value='Login' />"
    }
    out << "</div>"
    }
  • call tag as <g:buttonBar />

Testing Tags

  • very easy
  • tags when called as methods return a string
  • TagLibUnitTestCase makes it even easier
    • includes mocks for
      • session
      • request
      • response
  • includes other mocks from GrailsUnitTestCase

A Sample Test Class

  • import grails.test.*
    class DemoTagLibTests extends TagLibUnitTestCase {

    public void setUp() {
    super.setUp()
    tagLib.metaClass.createLink = { params ->
    "/groovyGroups/show/${params.id}"
    } // Had to add this because otherwise createLink wouldn't be available in the test
    // This would also be true of custom tags that aren't in the same tag library
    }

    void testButtonBarWithAdmin() {
    mockSession.user = [isAdmin:{-> true}] // map is serving as mock object
    def output = tagLib.buttonBar() // tag itself is a closure, so can call as a method here
    assert output.toString().contains('Create Stuff')
    }

    void testIfLoggedIn() {
    mockSession.user = "Anything can go here" // doesn't matter what goes here since we're just checking session.user
    def output = tagLib.ifLoggedIn([:]) {
    'User is logged in'
    }
    assertEquals output.toString(), 'User is logged in'
    }

    void testGroupLinks() {
    mockDomain(GroovyGroup, // new GroovyGroup objects here ...)
    def output = tagLib.groupLinks()
    assert output = tagLib.groupLinks()
    assert output.toString().contains('<a href=')
    assert output.toString().contains('Group2')
    }
    }

  • remember that unit tests run much faster but don't have everything available, so if you find yourself mocking a ton of stuff in unit tests, that's a good indication you need integration tests

Namespaces

  • if you don't declare a namespace, uses g
  • put static namespace = 'demo' at the top of your taglib class to declare a namespace
  • use the namespace as a prefix when calling the tag as a method, e.g. namespace.tag()

Distributing TagLibs With Plugins

  • create a plugin project
  • create or copy a TagLib to the /taglib directory
  • package your plugin
  • Install your plugin in another application
  • your custom tags are now available in the application

Demo

  • showing demo of simple custom tag to replace a lot of the redundant stuff in the grails scaffolding
  • one limitation of gsp tags is you can't have nested tags
    • you can fake this out a bit by leveraging the request scope
  • great use of gsp tags is to bundle up html, css, and javascript that gets output to the page
  • FieldData plugin available that does a lot of these sorts of things
  • check out the custom tags available in the grails core for good examples of how to do things

No comments: