Grails Cookbook - A collection of tutorials and examples

Grails Tutorial for Beginners - Scaffolding

Scaffolding is a popular feature of web frameworks where a programmer can quickly create working components of a system to handle data manipulation. Code to handle create, read, update and delete operations (CRUD) for a specific table can be implemented or generated within several minutes only through the use of code generators or other shortcuts.

Benefits

Scaffolding is similar with what is used in building houses and other structures. The concept is to build temporary structures that will help the team build the real structure later. They are usually made up of modular materials that can be put together quickly.
Here are some advantages of scaffolding in software development.
- quickly create screens that can do CRUD operations.
- users can immediately see a working software to help them visualize and refine what they really want in the final product.
- can be used by the developers and testers to create their manual test data.
- developers may focus first on important business features, as simple CRUD screens can be created easily.
- developers may copy certain portions of the generated code, saving them time on writing repetitive code.

Requirement


The model class is the only code needed to be prepared manually before going through scaffolding. Here is an example of a simple model class:
package asia.grails.test.scaff
class Person {
	String firstName
	String lastName
	Date dateOfBirth
	static constraints = {
		firstName (blank: false)
		lastName (blank: false)
		dateOfBirth (blank: false)
	}
}

Dynamic Scaffolding

Dynamic scaffolding means there is no need to generate code, as the functionality is generated at runtime.
If you prefer to use convention, just create a controller with the same name as the model, and set scaffold property to true:
class PersonController {
	static scaffold = true   
}
You may also use a different name for the controller, by setting the scaffold property to the name of the data model:
class TestController {
	static scaffold = Person
}
With just these 3 lines of code, the following screens and their corresponding business logic are created at runtime:
Listing Page

List all records of the table with column sorting and pagination. Link to create a new record is provided.

sca02
Create Page

A form where the user can create a new record. A link to the listing page is provided.

sca01
Show Page

Show details of a table record. Fields are read only and can not be modified. But buttons to go to the edit page is provided. The user can also delete the record or go back to the listing page.

sca03
Edit Page

Edit form of a table record. The record can either be updated or deleted. A link back to the listing page is available.

sca04
The behavior is complete, including field validations and optimistic lock checking.
This quick approach is ideal if you are only interested in having working screens and no need to customize anything.

Static Scaffolding

Since dynamic scaffolding is generated at runtime, it has limitations. It is possible to generate the real code so that it can be further be customized. This is called static scaffolding as the code can be viewed and modified, and not generated at runtime.

You can generate the controller code, view code, or both. Before you can generate any code, run this console command on the project root.

grails compile

Controller

Type the command below on the console to generate the controller code:

grails generate-controller asia.grails.test.scaff.Person

and the following code will be generated:

package asia.grails.test.scaff
import org.springframework.dao.DataIntegrityViolationException
class PersonController {
    static allowedMethods = [save: "POST", update: "POST", delete: "POST"]
    def index() {
        redirect(action: "list", params: params)
    }
    def list(Integer max) {
        params.max = Math.min(max ?: 10, 100)
        [personInstanceList: Person.list(params), personInstanceTotal: Person.count()]
    }
    def create() {
        [personInstance: new Person(params)]
    }
    def save() {
        def personInstance = new Person(params)
        if (!personInstance.save(flush: true)) {
            render(view: "create", model: [personInstance: personInstance])
            return
        }
        flash.message = message(code: 'default.created.message', args: [message(code: 'person.label', default: 'Person'), personInstance.id])
        redirect(action: "show", id: personInstance.id)
    }
    def show(Long id) {
        def personInstance = Person.get(id)
        if (!personInstance) {
            flash.message = message(code: 'default.not.found.message', args: [message(code: 'person.label', default: 'Person'), id])
            redirect(action: "list")
            return
        }
        [personInstance: personInstance]
    }
    def edit(Long id) {
        def personInstance = Person.get(id)
        if (!personInstance) {
            flash.message = message(code: 'default.not.found.message', args: [message(code: 'person.label', default: 'Person'), id])
            redirect(action: "list")
            return
        }
        [personInstance: personInstance]
    }
    def update(Long id, Long version) {
        def personInstance = Person.get(id)
        if (!personInstance) {
            flash.message = message(code: 'default.not.found.message', args: [message(code: 'person.label', default: 'Person'), id])
            redirect(action: "list")
            return
        }
        if (version != null) {
            if (personInstance.version > version) {
                personInstance.errors.rejectValue("version", "default.optimistic.locking.failure",
                          [message(code: 'person.label', default: 'Person')] as Object[],
                          "Another user has updated this Person while you were editing")
                render(view: "edit", model: [personInstance: personInstance])
                return
            }
        }
        personInstance.properties = params
        if (!personInstance.save(flush: true)) {
            render(view: "edit", model: [personInstance: personInstance])
            return
        }
        flash.message = message(code: 'default.updated.message', args: [message(code: 'person.label', default: 'Person'), personInstance.id])
        redirect(action: "show", id: personInstance.id)
    }
    def delete(Long id) {
        def personInstance = Person.get(id)
        if (!personInstance) {
            flash.message = message(code: 'default.not.found.message', args: [message(code: 'person.label', default: 'Person'), id])
            redirect(action: "list")
            return
        }
        try {
            personInstance.delete(flush: true)
            flash.message = message(code: 'default.deleted.message', args: [message(code: 'person.label', default: 'Person'), id])
            redirect(action: "list")
        }
        catch (DataIntegrityViolationException e) {
            flash.message = message(code: 'default.not.deleted.message', args: [message(code: 'person.label', default: 'Person'), id])
            redirect(action: "show", id: id)
        }
    }
}

Explanation of controller code:
  • Allowed Methods - restricts if GET or POST are allowed for the specified actions.
    E.g. static allowedMethods = [save: "POST", update: "POST", delete: "POST"]
    This means only HTTP POST are allowed for the actions save, update, and delete. GET is not permitted. But for the other actions specified, POST and GET are allowed.
  • index - Just redirect to the list action, passing the parameters it received if any.
  • list - retrieves paginated records by using params.max and params.offset into the query. The total number of records are also computed to be able to determine the number of pages available.
  • create - prepare and display a create form page
  • save - the action invoked to create a new record. If successful, the show page is displayed together with a success message. Otherwise, the form is displayed again so that the user can correct the form contents.
  • show - retrieves the record and show the details on a page. If record with the given id is not found, the page is redirected to the list screen.
  • edit - retrieves a record and show an update form. If record with the given id is not found, the page is redirected to the list screen.
  • update - the action invoked to update a record. If successful, the show page is displayed together with a success message. Otherwise, the edit form is displayed again so that the user can correct the form contents.
  • delete - action invoked to delete a record. If successful, the list page is shown. Otherwise the show page is displayed again.
Views

Type the command below on the console to generate the view code:

grails generate-views asia.grails.test.scaff.Person

and the following code will be generated:

list.gsp

<%@ page import="asia.grails.test.scaff.Person" %>
<!DOCTYPE html>
<html>
	<head>
		<meta name="layout" content="main">
		<g:set var="entityName" value="${message(code: 'person.label', default: 'Person')}" />
		<title><g:message code="default.list.label" args="[entityName]" /></title>
	</head>
	<body>
		<a href="#list-person" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
		<div class="nav" role="navigation">
			<ul>
				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
			</ul>
		</div>
		<div id="list-person" class="content scaffold-list" role="main">
			<h1><g:message code="default.list.label" args="[entityName]" /></h1>
			<g:if test="${flash.message}">
			<div class="message" role="status">${flash.message}</div>
			</g:if>
			<table>
				<thead>
					<tr>
						<g:sortableColumn property="firstName" title="${message(code: 'person.firstName.label', default: 'First Name')}" />
						<g:sortableColumn property="lastName" title="${message(code: 'person.lastName.label', default: 'Last Name')}" />
						<g:sortableColumn property="dateOfBirth" title="${message(code: 'person.dateOfBirth.label', default: 'Date Of Birth')}" />
					</tr>
				</thead>
				<tbody>
				<g:each in="${personInstanceList}" status="i" var="personInstance">
					<tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
						<td><g:link action="show" id="${personInstance.id}">${fieldValue(bean: personInstance, field: "firstName")}</g:link></td>
						<td>${fieldValue(bean: personInstance, field: "lastName")}</td>
						<td><g:formatDate date="${personInstance.dateOfBirth}" /></td>
					</tr>
				</g:each>
				</tbody>
			</table>
			<div class="pagination">
				<g:paginate total="${personInstanceTotal}" />
			</div>
		</div>
	</body>
</html>

create.gsp

<%@ page import="asia.grails.test.scaff.Person" %>
<!DOCTYPE html>
<html>
	<head>
		<meta name="layout" content="main">
		<g:set var="entityName" value="${message(code: 'person.label', default: 'Person')}" />
		<title><g:message code="default.create.label" args="[entityName]" /></title>
	</head>
	<body>
		<a href="#create-person" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
		<div class="nav" role="navigation">
			<ul>
				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
				<li><g:link class="list" action="list"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
			</ul>
		</div>
		<div id="create-person" class="content scaffold-create" role="main">
			<h1><g:message code="default.create.label" args="[entityName]" /></h1>
			<g:if test="${flash.message}">
			<div class="message" role="status">${flash.message}</div>
			</g:if>
			<g:hasErrors bean="${personInstance}">
			<ul class="errors" role="alert">
				<g:eachError bean="${personInstance}" var="error">
				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
				</g:eachError>
			</ul>
			</g:hasErrors>
			<g:form action="save" >
				<fieldset class="form">
					<g:render template="form"/>
				</fieldset>
				<fieldset class="buttons">
					<g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
				</fieldset>
			</g:form>
		</div>
	</body>
</html>

edit.gsp

<%@ page import="asia.grails.test.scaff.Person" %>
<!DOCTYPE html>
<html>
	<head>
		<meta name="layout" content="main">
		<g:set var="entityName" value="${message(code: 'person.label', default: 'Person')}" />
		<title><g:message code="default.edit.label" args="[entityName]" /></title>
	</head>
	<body>
		<a href="#edit-person" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
		<div class="nav" role="navigation">
			<ul>
				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
				<li><g:link class="list" action="list"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
			</ul>
		</div>
		<div id="edit-person" class="content scaffold-edit" role="main">
			<h1><g:message code="default.edit.label" args="[entityName]" /></h1>
			<g:if test="${flash.message}">
			<div class="message" role="status">${flash.message}</div>
			</g:if>
			<g:hasErrors bean="${personInstance}">
			<ul class="errors" role="alert">
				<g:eachError bean="${personInstance}" var="error">
				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
				</g:eachError>
			</ul>
			</g:hasErrors>
			<g:form method="post" >
				<g:hiddenField name="id" value="${personInstance?.id}" />
				<g:hiddenField name="version" value="${personInstance?.version}" />
				<fieldset class="form">
					<g:render template="form"/>
				</fieldset>
				<fieldset class="buttons">
					<g:actionSubmit class="save" action="update" value="${message(code: 'default.button.update.label', default: 'Update')}" />
					<g:actionSubmit class="delete" action="delete" value="${message(code: 'default.button.delete.label', default: 'Delete')}" formnovalidate="" onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" />
				</fieldset>
			</g:form>
		</div>
	</body>
</html>

_form.gsp

<%@ page import="asia.grails.test.scaff.Person" %>
<div class="fieldcontain ${hasErrors(bean: personInstance, field: 'firstName', 'error')} required">
	<label for="firstName">
		<g:message code="person.firstName.label" default="First Name" />
		<span class="required-indicator">*</span>
	</label>
	<g:textField name="firstName" required="" value="${personInstance?.firstName}"/>
</div>
<div class="fieldcontain ${hasErrors(bean: personInstance, field: 'lastName', 'error')} required">
	<label for="lastName">
		<g:message code="person.lastName.label" default="Last Name" />
		<span class="required-indicator">*</span>
	</label>
	<g:textField name="lastName" required="" value="${personInstance?.lastName}"/>
</div>
<div class="fieldcontain ${hasErrors(bean: personInstance, field: 'dateOfBirth', 'error')} required">
	<label for="dateOfBirth">
		<g:message code="person.dateOfBirth.label" default="Date Of Birth" />
		<span class="required-indicator">*</span>
	</label>
	<g:datePicker name="dateOfBirth" precision="day"  value="${personInstance?.dateOfBirth}"  />
</div>

show.gsp

<%@ page import="asia.grails.test.scaff.Person" %>
<!DOCTYPE html>
<html>
	<head>
		<meta name="layout" content="main">
		<g:set var="entityName" value="${message(code: 'person.label', default: 'Person')}" />
		<title><g:message code="default.show.label" args="[entityName]" /></title>
	</head>
	<body>
		<a href="#show-person" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
		<div class="nav" role="navigation">
			<ul>
				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
				<li><g:link class="list" action="list"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
			</ul>
		</div>
		<div id="show-person" class="content scaffold-show" role="main">
			<h1><g:message code="default.show.label" args="[entityName]" /></h1>
			<g:if test="${flash.message}">
			<div class="message" role="status">${flash.message}</div>
			</g:if>
			<ol class="property-list person">
				<g:if test="${personInstance?.firstName}">
				<li class="fieldcontain">
					<span id="firstName-label" class="property-label"><g:message code="person.firstName.label" default="First Name" /></span>
						<span class="property-value" aria-labelledby="firstName-label"><g:fieldValue bean="${personInstance}" field="firstName"/></span>
				</li>
				</g:if>
				<g:if test="${personInstance?.lastName}">
				<li class="fieldcontain">
					<span id="lastName-label" class="property-label"><g:message code="person.lastName.label" default="Last Name" /></span>
						<span class="property-value" aria-labelledby="lastName-label"><g:fieldValue bean="${personInstance}" field="lastName"/></span>
				</li>
				</g:if>
				<g:if test="${personInstance?.dateOfBirth}">
				<li class="fieldcontain">
					<span id="dateOfBirth-label" class="property-label"><g:message code="person.dateOfBirth.label" default="Date Of Birth" /></span>
						<span class="property-value" aria-labelledby="dateOfBirth-label"><g:formatDate date="${personInstance?.dateOfBirth}" /></span>
				</li>
				</g:if>
			</ol>
			<g:form>
				<fieldset class="buttons">
					<g:hiddenField name="id" value="${personInstance?.id}" />
					<g:link class="edit" action="edit" id="${personInstance?.id}"><g:message code="default.button.edit.label" default="Edit" /></g:link>
					<g:actionSubmit class="delete" action="delete" value="${message(code: 'default.button.delete.label', default: 'Delete')}" onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" />
				</fieldset>
			</g:form>
		</div>
	</body>
</html>

All

Type the command below on the console to generate both the controller and view codes:

grails generate-all asia.grails.test.scaff.Person