This lecture about Grails assumes that you have already done the individual programming assignments, so you should know that Grails is a web framework built on the Groovy programming language. You know that Grails uses the MVC design pattern, and if you follow the conventions, much of code is scaffolded for you. Although a goal of Grails is to prefer convention over configuration, there a number of configurations that need to be made. Your programming assignments actually edited almost all the configuration files. You also realize that plugins can extend the basic Grails framework, and you have installed several plugins.
This lecture will not start from the beginning, rather it will introduce more advance features than what your programming assignments introduced. However, this lecture of Grails is not an exhaustive list of all the advance topics in Grails. The Grails documentation is the best source for all the features of Grails.
Much of the material for this lecture comes from “The Definitive Guide to Grails 2” by Jeff Scott Brown and Graeme Rocher, a very good book, but it is a little out of date now because Grails 2.4 uses the assest-pipeline plugin rather than the resource plugin. Now, Grails is on version 3 which uses the gradle build. This lecture will proceed by going through some of the domain, controller and view advance features that I expect you will need for your projects.
Domain
Grails uses GORM for Object Relational Mapping to define the domain objects. Documentation for GORM is at
http://gorm.grails.org/latest/hibernate/manual/
Associations
Domain classes can have associations with each other much like tables in a database can be related to each other. In a database, tables can be related to each other by having a foreign key that points to a primary key in another table. Another way that tables can be associated to each other is by a third table called a join or junction table that pairs keys from each table.
An example of a domain class relation is the Author – Book domain classes in your programming assignments. An Author could have written many books. So an Author entry can be associated with many Book entries.
The simplest relationships are
• Many-to-one
• One-to-one
• One-to-many
• Many-to-many
Your programming assignment is an example of one-to-many. The Author domain has a one-to-many Book domains associated with it.
Many-to-one
The simplest relation to implement in Grails is many-to-one. You just reference the “one” domain in the “many” domain.
class Face { Nose nose } class Nose { }
So a Face can have one Nose, but a Nose can be referenced by many Faces. Grails implements this relation with a Nose “foreign key” in the Face table.
One-to-One
Nose and Face should really be a one-to-one association. To make a one-to-one relations the Nose domain must reference the Face domain.
class Face { Nose nose } class Nose { static belongsTo = [face: Face] }
The static property “belongsTo” puts a Nose foreign key in the Face table, just as above.
The term cascading refers to the behavior of saves, updates and deletes on the domain classes when you perform an operation on associated domains. In this case, the one-to-one association, when you save and delete Face, GROM will save and delete Nose. In other words, saves and deletes will cascade from form Face to Nose. But when you delete a Nose, the Face is not deleted.
To make a Face, you need to make the Nose first.
new Face(nose:new Nose()).save()
A way to make the one-to-one bidirectional is to use the “hasOne” property, and then the cascading can go both directions.
class Face { static hasOne = [nose:Nose] } class Nose { Face face }
This will put the Face foreign key in the Nose table. The cascades will go both directions.
One-to-many
To indicate a one to many association, add a static property “hasMany” to the “one” domain.
class Author { static hasMany = [books: Book] String name } class Book { String title }
Grails will implement this association with a join table. The join table is a third table probably called AuthorBook that will associate Author keys with Book keys.
When you retrieve from the “one” table Grails will automatically inject a Set of the “many.”
def a = Author.get(1) for (book in a.books) { println book.title }
In this case saving and updating on Author will cascade to the Book domain, but deleting an Author will not delete the books. Adding the “belongsTo” property to the “many” domain will cascade the deletes from Author to Book.
class Author { static hasMany = [books: Book] String name } class Book { static belongsTo = [author: Author] String title }
Note sometimes associations can become complicated when the “many” domain class references more than one “one” domain. For example Airports can have many flights, and a flight is associated with two airports, the departure and destination airports. See the GORM documentation for how to handle this.
http://gorm.grails.org/latest/hibernate/manual/#gormAssociation
Many-to-Many
The “many-to-many” association is specified by using “hasMany” in both domain classes. One domain has to be owner by adding the “belongsTo” property to the other domain. Cascading is from the owner domain.
class Book { static belongsTo = Author static hasMany = [authors:Author] String title } class Author { static hasMany = [books:Book] String name }
So in this case you have to make the Books first.
new Author(name:"Stephen King") .addToBooks(new Book(title:"The Stand")) .addToBooks(new Book(title:"The Shining")) .save()
Documentation for GORM associations is at
http://gorm.grails.org/latest/hibernate/manual/#gormAssociation
Queries
Queries are how you retrieve data from a database. You have already seen how the “list” method in the domain class can retrieve all the table entries. That is a query. The response from the list method can be modified with map argument.
You can retrieve a single table entry if you know the id by using the “get” method.
def book = Book.get(23)
Dynamic Finders
GORM has “dynamic finders.”
http://gorm.grails.org/latest/hibernate/manual/#finders
Dynamic finders are methods that are created dynamically at run time. They are best explained by example.
def book = Book.findByTitle("The Stand") book = Book.findByTitleLike("Harry Pot") book = Book.findByReleaseDateBetween(firstDate, secondDate) book = Book.findByReleaseDateGreaterThan(someDate) book = Book.findByTitleLikeOrReleaseDateLessThan("Something", someDate)
See the findBy and findByAll documentation in the Domain class quick reference.
The general form of the method name is
Book.findBy([Property][Comparator][Boolean Operator])?[Property][Comparator]
The part of the expressions marked by “(…)? is optional. The property is a domain field, in other words the column name in the table. The simplest expression is
def book = Book.findByTitle("The Stand")
In this example the equality operator is assumed.
Comparators include:
• InList – In the list of given values
• LessThan – less than a given value
• LessThanEquals – less than or equal a given value
• GreaterThan – greater than a given value
• GreaterThanEquals – greater than or equal a given value
• Like – Equivalent to a SQL like expression
• Ilike – Similar to a Like, except case insensitive. Note the first letter is a capital i.
• NotEqual – Negates equality
• InRange – Between the from and to values of a Groovy Range
• Rlike – Performs a Regexp LIKE in MySQL or Oracle otherwise falls back to Like.
• Between – Between two values (requires two arguments)
• IsNotNull – Not a null value (doesn’t take an argument)
• IsNull – Is a null value (doesn’t take an argument)
Where Querying
The dynamic finders are restricted to queries into a single table. Sometimes you want the query result to dependent on associated tables then you must use the “where” query.
http://gorm.grails.org/latest/hibernate/manual/#whereQueries
You can also use a where query to retrieve results dependent on the same table.
def query = Person.where { firstName == "Bart" } Person bart = query.find()
You first define a query with the test as a closure IN the “where” method for the Domain. Then, you retrieve the results by calling the “find” method on the query. You can use all the Groovy Boolean tests on any property in the domain. You CANNOT define the testing closure outside the where method. In other words this will NOT work.
//This will not work! def callable = { lastName == "Simpson" } def query = Person.where(callable)
To generate results dependent on other tables, the table making the “where” clause must have a reference to the other table. For example.
def query = Pet.where { owner.firstName == "Joe" || owner.firstName == "Fred" }
In this case owner is a property of Pet and references the Owner table.
The above material is a very brief introduction of the material in the GORM and Grails documentation.
http://grails.org/doc/latest/guide/GORM.html
http://gorm.grails.org/latest/hibernate/manual/
Constraints and Validation
Grails uses the “constrain” closure for validation. This is appropriate because the domain class is the representation of the data, so it should be expressed there.
class User { String login String password String email Integer age static constraints = { … } }
In the domain class “constraints” is a closure. You then use methods calls in the constraint closure to specify the constraint for each property of the domain.
class User { ... static constraints = { login size: 5..15, blank: false, unique: true password size: 5..15, blank: false email email: true, blank: false age min: 18 } }
Note that “login size: 5..15, blank: false, unique: true” could have been written
login([ size: 5..15, blank: false, unique: true ])
So “login” is a method and “size”, “blank” and “unique” are keys in a map argument. The methods are named after the properties in the domain class and are built for you by Grails, and the key are different constraints.
Note that “5..15” is using the groovy range operator, “…”, so the size of “login can be 5 to 15 characters long.
The “blank” key specifies if the property value can be empty. For login the string cannot be empty.
The “unique” key specifies that the login string must be unique in the database table. Note that the order of properties in the constraints closure will set the order of properties in the table and the return from the list method.
The possible key-constraints are found in the Quick Reference under the Constraints.
• blank
• creditCard
• email
• inList
• matches
• max
• maxSize
• min
• minSize
• notEqual
• nullable
• range
• scale
• size
• unique
• url
• validator
In general, their use is obvious. Read the documentation on how to use them. For example “inList” constrains the property value to a list that you provide. It will be implemented as a drop down menu in the scaffolded view.
Types, Nulls and Required Constraints
By default, domain properties are required to pass the validation (See below about validation). If you want the property not be required then in the domain’s closure the property’s “nullable” attribute must be set to true. For example
Class Item { Integer number String name int anotherNumber static constraints = { number (nullable: true) name (nullable: true, blank: true) } }
Strings need to be both nullable and blank true. Primitive types, such as anotherNumber above, cannot be nullable true. If you make a primitive type nullable, the scaffolding views will be made and there will be no error, but the view will show a default value of zero and the database generated by Hibernate will show that the column is “NOT NULL.”
Validator
The validator constraint can be used to make custom constraints. For example suppose that the possible answers to a survey is listed in “Survey.answers.” Then to validate the Response.answer
class Response { Survey survey Answer answer static constraints = { survey blank: false answer blank: false, validator: { val, obj -> val in obj.survey.answers } } }
Note that “val” is the value of the form field and “obj” is the domain instance being validated, essentially “this.” There is also a third argument, “errors,” that you can use to attach a message to the Error object associated with the domain.
In the simplest form the validator closure returns a Boolean for the result of the validation.
even validator: { val -> return (val % 2) == 0 }
More typically, validators return true if the entry passes validation and a list if there is an error in validation.
Messages (a short diversion)
To understand the next validator example, we need to first understand “messages”.
It is good practice not to hard code text in the view rather to use something like string variables to represent the text. There is at least two reasons to use variables to represent text:
- If you want to change the text then you only need to change the value of the string variable for the text to change in all the views rather than search for all the occurrences of the text.
- If you want to internationalize you website, meaning using several languages, then you need the provide different values for the variables depending on the language.
Grails uses messages to specify the string/text in your website. You associate the string/text with a message. Messages are found in the grails-app/i18n/messages.properties file. Putting the string-value map in a file in a directory called “i18n” is standard with many web frameworks. Why do you think the directory is called “i18n”? The file i18n/messages.properties is a list of properties and values separated by “=”, for example:
default.paginate.prev=Previous default.paginate.next=Next default.boolean.true=True default.boolean.false=False
In the above example “default.paginate.prev” is a String variable that you can use in your code. The value of the string is “Previous.”
The message value can have “variable” sub-strings that are passed to it in an order list. The sub-strings are called parameters. The parameters in the list are indexed by {0}, {1}, {2} etc, For example the {0} below.
default.create.label=Create {0}
Messages are used in the view with the “g:message” tag and the sub-string list of parameters are passed by the “args” attribute of the tag.
<g:message code="default.create.label" args="[entityName]" />
Back to Validator returning a List
The constraint can also return a list. This is done when the entry is not valid and is used to help generate the error message. The validator will find the message by first mapping to the class name and then “dot” followed by the property name that is being validated. The first string in the list returned by the validator will make the message more specific when looking for the message in i18n/messages.properties. So what will be search in the message.properties file is
className.propertyName.theFristStringInList
The example below test for the existence of an entry. If it does not exist then it creates an error message.
class Person{ String name static constraints = { name validator: { if (!it) return ['entryMissing'] } // This maps to the message: // person.name.entryMissing = Please enter a name in the field {0} }
The parameter list given to the error message from a validator has some parameters automatically added to the list. Parameters 0 to 2 are automatically mapped to
0: property name, 1: class name, 2: property value.
So in the example above, the list returned from the validator, indicates the message, “person.name entryMissing”. The 0 parameter for the message is the property, “name.”
The list returned by the validator can have additional strings. These are mapped from index 3 on in the parameter list for the message.
Domain Don’ts
- Do NOT declare an “id” domain property. Although declaring and managing the domain id yourself can work during development and using a H2 database, it will not work when you release to production using a MySQL database. In fact, the MySQL will not make a table with a declared id. Sometimes, programmers want to manipulate the id do do something special; instead use a boolean property to represent a state.
Controllers
In your programming assignments, you have learned to create a controller, send the model to the view and how to associate a view with the controller. In this section, you’ll learn some more tools to use with controllers. Many of the tools are properties that are injected into your controller when the controller is instantiated.
Logging
Grails uses the Logback logging framework:
Logging prints messages typically to a log file or to the console. These messages are used to record errors, warnings, gather debugging info, etc. In your programming assignment, you probably used “println” to write debugging information, but that is not always the best technique, for example in the production environment, it is better to write to a file.
A logger writes messages at different levels. The priority of the levels are
0. off
1. error
2. warn
3. info
4. debug
5. trace
6. all
Note that “0. off” and “6.all” are not really levels. They exist only to specify the top and bottom of the priority list and to configure a logger.
Loggers are injected in all your grails app artifacts. The injected logger is accessed using the “log” object. To use the logger you call the method corresponding to level of priority of the message. For example
log.debug("My debugging message.")
will write a debug message.
If you do this in a controller, for example
class BooksController { def index { log.debug("In index method) … } }
The debug log message will probably not appear in the console.
You need to configure the logger. Loggers are configured in the grails-app/conf/logback.groovy file. To configure the logger, you call the logger(…) with at least the logger name and the level. For example, you could add to the bottom of the logback.groovy file:
logger("cs4760progassign.BooksController", DEBUG)
In the above, the logger for cs4760progassign.BooksController will be set to print debugging and higher messages.
The name of the logger is the full class name including the package name.
Although this will work for logging in the BooksController, it will not enable logging for any other artifact. If you are logging from one artifact you probably want to log in all artifacts. You can specify a group of loggers. For example
logger("cs4760progassign", DEBUG)
This will enable all the injected loggers to write debug messages and higher.
This is good enough if you only want to log to the console. More configurations are required to write to a file. You need to make an Appender and specify an encoder. There are four types of Appenders.
- OutputStreamAppender – Writes to an OutputStream and the base class for the following three Appenders.
- ConsoleAppender – Logs to the console.
- FileAppender– Logs to a single file.
- RollingFileAppender – Logs to files, for example a new file each day.
The Appenders are created and configured in the Appenders closure in the logback.groovy. For example to create a file Appender, you would add this Appender closure.
def targetDir = BuildSettings.TARGET_DIR appender("LOGFILE", FileAppender) { file = "${targetDir}/log.log" append = true // Must have encoder encoder(PatternLayoutEncoder) { pattern = "%d : %level %logger - %msg%n" } }
This will create an Appender called ‘LOGFILE’ that writes to the file “${targetDir}/myLog.log” which is adjacent to “stacktrace.log” in the “target directory.” The Appender needs an encoder which specifies a pattern for formatting the log messages. The above pattern specifies:
- %d – date and time
- : – writes a colon
- %level – logging level, such as DEBUG
- %logger – the logger name, such as cs4760progassign.BooksController
- – – writes a dash
- %msg – the logging message, such as “In index method”
- %n – a new line
You can learn more patterns in the documentation for the PatternLayout
http://logback.qos.ch/manual/layouts.html
Now, the LOGFILE Appenders needs to be added to the loggers lists of Appenders that it will output to. You replace the logger configuration with:
logger("cs4760progassign", DEBUG, ["LOGFILE"])
Now all the loggers for cs4760progassign will log to both the console and the file.
For documentation, see:
http://grails.org/doc/latest/guide/conf.html#logging
http://logback.qos.ch/manual/index.html
http://logback.qos.ch/manual/groovy.html
Scope Objects
You are familiar with namespace variable scoping in applications, meaning the namespace that a variable is defined in and can be used in. In a web application scoping is more complicated than the namespace. Variables also have time scope. Most objects only exist during a single request. Some objects can exist longer, these objects are
- params – a map of the request parameters in either the URL for example in a GET request or in a form submit for a POST request.
- request – a HttpServletRequest object, and exist for only a single request to the server
- flash – a object that exist with the current request and the next request
- session – a HttpSession object, and exist with a user’s visit to the domain
- servletContext –a ServletContext object, exist for the lifetime of the application
The scope objects params and request are created for every request to the server. A single session parameter is created when the user visits the domain and continues to navigate within the domain. Typically it expires when the user logouts or after some time limit. The servletContext exist for the lifetime of the web app, but you’ll seldom have need for this object.
All of these scope objects are available to all your artifacts, including controllers and views. You can set and get objects in these scope objects by using the dot format. For example
def user = session.username session.myObject = myObject
When a user fills out a form and hits the submit button, a request is made to the server. The form data consists of name-value pairs that are put into the params object. The controller can access them. For example when creating an Author named “Bob Jones” using the create form
In the Author/create view
<form action="/cs4760progassign/author/save" method="post" > <fieldset class="form"> … <div class="fieldcontain required"> <label for="name"> Name <span class="required-indicator">*</span> </label> <input type="text" name="name" required="" value="" id="name" /> </div> </fieldset> <fieldset class="buttons"> <input type="submit" name="create" class="save" value="Create" id="create" /> </fieldset> </form>
After clicking the submit button. A POST request to “/cs4760progassign/author/save” will be made and the params will contain
name="Bob Jones" create="Create" // a hidden form entry, see the submit button.
Grails also puts into the params entries
controller="author" action="save"
Data Binding
Data binding is the process of taking form data, populating the domain object and updating the database. For example in a the Author controller we could have
class AuthorController { def save(){ def author = new Author() author.name = params.name author.save() } }
As our Author domain class becomes more complicated with many more properties, the template code above will get more complicated and difficult to maintain. If the fields in the form data match those of the property names in the domain class then the binding can be automatic.
class AuthorController { def save(){ def author = new Author(params) author.save() } }
Of course, you cannot use the above technique when you are updating a table entry. In that case, you would get the domain instance from the table and set the properties map of the domain.
class AuthorController { def update(){ def author = Author.get(params.id) author.properites = params author.save() } }
Note that Grails will only fill in matching property names.
Validating Data
Validation has two phases. The first phase occurs when the request parameters are matched with the domain properties, such as in this line.
author.properites = params
In this phase, Grails must make type conversions from the params to the Author properties. If the types cannot be converted then Grails will set the database entry to read only and author cannot be updated.
The second phase occurs when the domain instance is saved, such as
author.save()
or explicitly validated
author.validate()
At this points, Grails will check the constraints for each property specified in the constraint closure. If there are errors, they are added to the “errors” property of the domain instance. You can test for errors using the hasErrors() method, and you cycle through the error:
author.errors.allErrors.each { println it.code }
You can also render the errors in the view.
<g: renderErrors bean="${author}" />
Data Binding to Multiple Domain Objects
In the previous example data binding of the author name, the name of the input field for the author’s name was just called “name” and that matched the domain class property, name. If we want to bind multiple domain objects then the names of the input fields need to be more explicit. For example consider a form that creates simultaneously a book and an author.
The view
... <input type="text" name="author.name" /> <input type="text" name="book.title" /> <input type="number" name="book.publishYear" /> ...
Then in the controller
def author = new Author( params["author"] ) .addToBooks( book = new Book( params["book"] ) ) .save()
There is much more to data binding. Read the data binding section in the Web Layer chapter.
http://grails.org/doc/latest/guide/theWebLayer.html#dataBinding
Controller Don’ts
- Be sure to configure “server: contextPath: ‘<app name>’ “. Although during development and deploying to the development server, your app does not need the server context path set because the port number routes the request to the correct web app. When the app is deployed on the production server, the server context path is required for the “load balancer” which uses the path to route to the correct web app.
- Avoid using “render (view: <another name>)” at the end of controller action. Grails prefers convention over configuration. When you route to a view that not have the same name as the controller action, you make it difficult for other developers to follow your code. The developers can use the URL for the view to find the controller action governing the view.
- Also avoid use the URLMappings controller to route requests. This is for the same reason as above.
- Let the framework work for you, don’t force it.
Views
Scriptlet Blocks
Any groovy code can be inserted into the GSP page using the scriptlet block <% … %>
<html> <body> <% 3.times { %> <p>I'm printed three times. </p> <% } %> </body> </html>
Scriptlets do not output to the page. You can use standard output to output to the page.
<% out << "Hello World!" %>
or the short hand
<%= "Hello World!" %>
In practice, you normally do not use the above scriptlet tag. To output to the page use the G-string with curly brackets.
<p>${author.name}</p>
G-Tag
Also, you should not need to use the scriptlets tag for controlling output. A better alternative are the built in g-tags.
Setting Variables
You can set variables using the g:set tag.
<g: set var="bookTitle" value="${book.title}">
Then the variable “bookTitle” will have the value of book.title.
Logical Tags
G-tags include if-else tags.
<g:if test="${book?.publishYear > 2014}"> Year is too new. </g:if> </g:elseif test="${book?.publishYear < 1900} Year is too old. <g:else> Year is ${book?.publishYear} </g:else>
Iterative Tags
You can iterate over collections using the g:each tag.
<g:each in="${author.books}" <span class="tag">${it.title}</span> </g:each>
Or you can name the variable in the each-loop.
<g:each var="b" in="${author.books}" <span class="tag">${b.title}</span> </g:each>
There are also g:while, g:collect and g:findAll tags.
Grails Dynamic Tags
Grails dynamic tags are provided by the Grails tag libraries. You can use them as tags, but you can also call them with methods. This avoids nested tags. For example
<a href="<g:createLink action="list" />" > List </a>
is better written with a method call
<a href="${createLink(action: "list">}"> List </a>
Link Tags
The g:link tag will create an anchor tag. It has the following attributes
- controller: the controller name for the link
- action: the action name for the link
- id: the identifier for the link
- params: map of parameters to pass to the link.
For example
<g:link controller="book" action="show" id="{$book.id}"> Show Book </g:link>
will create
<a href="/cs4760progassin/book/show/1">Show Book</a>
Using params to generate multiple links.
<g:link controller="book" action="list" params=[max:10,order='title']">Show the first 10 books</g:link>
If you do not specify the controller then it defaults to the current controller. If you do not specify the action then it defaults to the index method.
There is also a url attribute which is a map containing controller, action, id and resource. For example linking to the book with id=1.
<g:link url="[controller:'book', action:'show', id='1']"> Show Book</g:link>
But more likely, the view will have a instance of the book, bookInstance, then you can use
<g:link url="[resource:bookInstance, action:'show']" > Show Book</g:link>
Grails will find the id from bookInstance. There are many other attributes, see the reference manual.
http://grails.org/doc/latest/ref/Tags/link.html
Form Tags
The g:form tag has all the attributes of the HTML form tags. They are used the same as the HTML form tag:
<g:form action="save"> … </gform>
But you can use all the attributes from the g:link tag if you wish.
<g:form url="[controller='book', action='save']"> … </gform>
The g:textField Tag
<g:form url="[controller='book', action='save']"> <g:textField name="title" value=""> </g:textField> </g:form>
The g:checkBox and g:radio tags
<g:checkBox name="aBooleanValue" value="${true}" />
and
<fieldset> <g:radio name="myGroup" value="friction" checked="${bookInstance.genre = 'fiction'}" /> Fiction </g:radio> <g:radio name="myGroup" value="nonfriction" checked="${bookInstance.genre == 'nonfiction'}" /> Nonfiction </g:radio> </fieldset>
Select Tag
<g: select name="subject" from="${['math', 'cs', 'pyschology', 'humanities']}" value="${book.subject}" />
The g:datePicker Tag
<g:datePicker name="date" value="$(new Date()}" precision="day" />
Validation and Error
When a user inputs invalid entries, the errors should be shown in the view for the user to correct.
The hasErrors and eachError Tags
The hasError g-tags test for validation errors. Recall that validation occurs after an attempt to save data. The eachError tag iterates through the errors. Both the hasErrors tag and eachError have three attributes.
- bean: a bean instance to inspect for errors. This is typically the domain instance.
- field: the name of the field in the bean to check for errors. This is typically the property of domain instance.
- model: an alternative to specifying a bean.
The eachError tag is used with the hasError tag.
<g:hasErrors bean="${bookInstance}"> <ul class="errors"> <g:eachError bean="${bookInstance}"> <li>${it.defultMessage}</li> </g:eachError> </ul> </g:hasErrors>
A real life example from the book scaffolding.
<g:hasErrors bean="${authorInstance}"> <ul class="errors" role="alert"> <g:eachError bean="${authorInstance}" 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>
In the example above the “error” attribute in the message tag is the same as the “code” attribute, it just looks in a different file. You should not use the error attribute for your own messages.
Also note how the <g:if-tag is used inside the list tag to make the data-field-id attribute for the list tag. This used by Grails code to locate the entry with the error.
A nice example of modifying the styling of a field is using the hasError method call. See the first line.
<div class="fieldcontain ${hasErrors(bean: authorInstance, field: 'name', 'error')} required"> <label for="name"> <g:message code="author.name.label" default="Name" /> <span class="required-indicator">*</span> </label> <g:textField name="name" required="" value="${authorInstance?.name}"/> </div>
The surrounding div has the hasErrors method call. The method call will check authorInstance.name for errors. If there is an error it will output “error” which will add the error class to the div. In this case, the styling will change the border and background.
Views Do and Don’ts
- Use GSP tags when ever possible. GSP tag make you code safer and more portable.
- Avoid using raw anchor and form tags, use g:link and g:form tags. If you hard wire links into your view then views are not portable.