Uber Scaffolding

  • Tags : scaffolding, templating, tools
  • Latest : 0.1.1
  • Last Updated: 08 October 2009
  • Grails version : 1.2-M2 > *
  • Authors : Jean Barmash
2 votes
Dependency :
compile ":uber-scaffolding:0.1.1"

Documentation

Summary

Installation

grails install-plugin uber-scaffolding

Description

UberScaffolding Plugin

Motivation

When trying to iteratively develop applications, it is typical that your domain model changes a lot during early parts of development. Scaffolding is a great crutch to develop CRUD pages, and in fact the scaffolding process can be extended to add additional pages beyond the regular CRUD provided out of the box. Furthermore, Grails' capability to do dynamic scaffolding is a great way to test your domain model, and frequently can serve as basis for future work on GSP pages.

However, the default scaffolding templates are limiting in that they don't allow customizations of scaffolded pages. For example, if I want to NOT display a particular property on the show page, I am not able to do so - I have to generate the scaffolded GSP pages, and then manually edit them. This means that if I need to add another property in the future, I have to either start manually edit the GSP pages, or regenerated scaffolded pages, and remove the unwanted property. This seems to violate DRY.

Additionally, some plugins require modifying the GSP pages. For example, the drill-down plugin requires to add <drilldown:resources/> tag to the header element, as well as other elements into the list table. Our plugin had to provide ability to insert some of those customization so that we can insert plugin integration code into our scaffolded pages.

I wanted solutions to the problems above, and wanted to see how far I can push the dynamic scaffolding process in that my UI layer is absolutely minimal. There will definitely be time for custom UI development, but I wanted to generate as much of the UI as possible.

Introduction

Let's step back for a second. Using dynamically scaffolded pages is a two step process. The first part of the process generates the pages, and the second part executes them agains the application domain model. Our goal is to provide hooks in the first phase of the process, so that the generated (and executed) pages are modified appropriately.

If you examine the scaffolding process, you will see that there are typically two types of insertion points. The first insertion point is fairly static, for example the header of a page. If we have an ability to inject some code into a header, then we can modify the page. Another type of insertion point is when the scaffolding pages iterate over properties of a domain object. This allows us to customize a particular row in the table that displays properties. If we want to not display a particular property, or to modify how it gets rendered,

The goal of the design was to provide a very flexible way to define scaffolding extensions definitions. Because the users may prefer to define their extension definitions differently, we want to enable that flexibility.

As a result, there are many different ways to do it, including one that can be easily put along the constraints definitions during domain object creation, which allows a convenient way to view the definitions without creating unnecessary coupling.

Installation

To install the UberScaffolding plugin type this command from your project's root folder:

grails install-plugin uber-scaffolding

Basic Example Of Usage

Let's say you have a regular Book / Author application. Suppose you are happy with the Grails scaffolding for the book page, but want to add a help message to on top of the list page, say to announce that there is a sale.

You can go into list.gsp scaffolding template, and insert a piece of code on top of the page:

<% def scaffoldingHelper = org.grails.plugins.uberscaffolding.UberScaffoldingHelperResolver.getScaffoldingHelper(className); %>

This declares a scaffolding helper object that you can then use (with the name of current class). className will resolve to "Book". Find the place inside list.gsp that you want to inject some code and add this code:

<% out << scaffoldingHelper.optionalCustomHtml("listPageTop") %>

This declares an area called "listPageTop" that you can now use to inject code. You are done with the list.gsp modifications for now.

Next, somewhere in your code, say bootstrap, you need to create a Scaffolding Helper for Book class.

DefaultUberScaffoldingHelper helper = UberScaffoldingHelperResolver.getScaffoldingHelper("Book") ;
helper["listPageTop"] = '''Sale on All Books!  30% Off'''

Now, create a BookController with dynamic scaffolding (def scaffold = Book).

class BookController {

def scaffold = Book

}

Next time you start your application, you can go to the book list page, and you will see the injected code.

This example just scratches the surface of what you can do.

Controlling visibility of properties

I adopted the following convention to control visibility of properties:

Whether the property is displayed: showDefault, createDefault, listDefault, editDefault

public static Map UI_PROPERTY_SHOW_ONLY =     [showDefault:true, listDefault:false, createDefault:false, editDefault:false]

This constant allows to define that only show page should display the property. (showDefault does not need to be set to true, since that's the default, it's here for clarity and consitency).

UI_PROPERTY_NEVER_SHOW = [showDefault:false, listDefault:false, createDefault:false, editDefault:false]

Other Defined Constants

UI_PROPERTY_LIST_ONLY - only display the property on list page
UI_PROPERTY_CREATE_ONLY - only display property on create page
UI_PROPERTY_EDIT_ONLY - only display property on edit page
UI_PROPERTY_SHOW_AND_EDIT - only display property on show and edit page
UI_PROPERTY_LIST_AND_SHOW - only display property on show and list page, useful for readOnly properties
UI_PROPERTY_NO_LIST - display the property everywhere, but not the list page

A place to insert custom code:

if (scaffoldingHelper.isFragmentEnabled("listDefault", p.name)) { %>
  <td>${fieldValue(bean:${propertyName}, field:'${p.name}')}</td> <%
}
out << scaffoldingHelper.optionalCustomHtml("list", p.name)

Defining scaffolding extension points

<% def scaffoldingHelper = org.grails.plugins.uberscaffolding.UberScaffoldingHelperResolver.getScaffoldingHelper(className); %>

<% if (scaffoldingHelper.isFragmentEnabled("listDefault", p.name)) %> <% if (scaffoldingHelper.isFragmentEnabled("showBasicDefault", p.name, false)) %>

Inject code for an area:

<% out << scaffoldingHelper.optionalCustomHtml("listPageTop") %>

Inject code into an area inside a property iterator - need to pass in the name of the property:

<% out << scaffoldingHelper.optionalCustomHtml("listHeaderCell", p.name)  %>

Debugging

It is useful to examine the generated pages, especially if there are bugs. The pages get generated on-demand, so if you open a page list, that one gets generated, others don't. To examine generated pages, a special page showGSPs exists for each controller (use it instead of /show), it displays the generated code.

showGSPs - they hook into the cache that stores the dynamically scaffolded pages.

listBasic showBasic Multiple Views - While it was always possible to do multiple views, now

How does thiw work with other scaffolding plugins

Other Capabilities Enabled by UberScaffolding Plugin

A nice capability that this approach enables is to dynamically

Usage: install the plugin. After that, you will need to copy the

Here is an example of creation.

WeatherStation.metaClass.static.getScaffoldingHelper = {
   DefaultScaffoldingHelper helper = ScaffoldingHelperResolver.getScaffoldingHelper("WeatherStation") ;
   helper[UberScaffoldingConstants.SHOW_PAGE_HEADER] = "<flot:resources /> "
   helper[UberScaffoldingConstants.SHOW_PAGE_TOP] = '''<g:render template="/templates/showGraph" plugin="graphAll"
             model="[graphingHelper:weatherStationInstance.getGraphingHelper(weatherStationInstance)]"> </g:render>'''
   helper
}

The above code inserts <flot:resources/> code fragment into the header area of the show page, and then redirects some data to a template. Note that you need to know that inside show page scaffolding, the instance of current object is called "weatherStationInstance".

Injecting Code For Properties Iterators / Controlling Property Visibility

In addition to the ScaffoldingHelper here, some scaffolding areas are defined inside WeatherStation class, inside constraints properties. This is done for convenience only, and can be defined in the code block above as well.

static constraints = {
    country(attributes:Constants.UI_PROPERTY_LIST_AND_SHOW)
    weatherData(nullable:true, attributes:Constants.UI_PROPERTY_NO_LIST)
}

This means that property country will be shown in List and Show pages, and property weatherData (which happens to be a list) will not be shown in list page.

Defined Constants:

More Examples:

Adding Buttons:

Integrating a plugin.

Gotchas

During the use of the template, I wanted to generate

Because the only variables available during the scaffolding generation phase are the domainclass, you may need to pass in additional variables.

this.setProperty("p", p)  ("this" refers to a binding object that is passed during template processing).

out << evaluate( '"' + scaffoldingHelper.optionalCustomHtml("show", p.name) + '"')

public static String currencyString = '''<td align=\\"top\\" class=\\"value\\">
    <g:formatNumber number=\\"\\$<{propertyName>?.${p.name}}\\" formatName=\\"currency.format\\" /> </td>'''

Maintaining Your Domain Objects in a Separate Plugin

The plugin defines some of the frequently.

The only gotcha there is that if you choose to use contraints method of setting visibility of properties, there is a problem with dependency on the constants class.

The suggested workaround is to copy the Constants into your own constants. This way, if you want ot maintain your domain objects outside of the main grails project in a separate plugin, you have no coupling between domain object hierarchy and the plugin, and don't have an artificial dependency.

Plans

Plans are to provide a few extra extension points for scaffolding templates. Possible integration / interoperability with Scaffoldtags plugin Ability to define some extension areas inside properties files Possibly a DSL Allow to use generate-all (I think it may be broken, at least in some cases) Have built in constructs (i.e. some constants) for including templates easier, or to swap out other pages / layouts very easily. Make Joda-Time more optional

Conclusion

Importantly, if plugins were to designed to account for this technique, this would allow to ease consumption of those plugins. The infrastructure provided by UberScaffoldig plugin could be used to ease that integration. The example above already demonstrates how to integrate flot plugin, but plugins themselves could interact with ubsercaffolding to insert themselves wherever needed. Thus, the user would only have to specify a config entry to enable capabilities such as filtering, for example, or drill down, or others. The user of the plugins would have to do very little to take advantage of basic plugin functionality, further increasing the DRYness of Grails ecosystem.

Release History

0.1 (Sept 30, 2009) - Initial Release 0.1.1 (Oct 8, 2009) - Some minor changes, resolved issues with release-package so the plugin is now can be installed with grails install-plugin.

This plugin is licensed under the Apache License, Version 2.0.