Introduction
We will start by creating a simple private library, where the user can create authors, books, and book instances (inspired by the previous project described in Usage).
We have already implemented some code for you in myLibrary. For this tutorial, you have two options: you can either follow the step-by-step implementation provided here, or attempt to implement each step by yourself first, then verify your implementation afterward. The explanation of each method is split between this documentation and myLibrary.
Pro Mode: A more challenging mode intended for experienced users. You are encouraged to attempt implementations independently, referring to the provided examples only if necessary.
Set Up
The solution and skeletons for this task are available to clone on this repository
git clone git@github.com:Taack/myLibrary.git
myLibrary) and copy the following line.git checkout part-1-skeleton
Or in IntelliJ you can select the wanted branch directly. Select in the top left corner main/myLibrary/remote, and you will see all the branches. Select a branch add checkout.
Note for each todo implementation a new branch has been implemented for your convenience. If anything happens, you can always checkout to that branch instead of starting over. Note: for example, part-1-todo-4 will have todo for implemented for you.
git checkout part-1-todo-4
Define Domain Classes
Edit: app/library/src/main/groovy/myLibrary/domain/
Pro Mode: implement each class by yourself. All the information required is in the file. After implementing MyLibraryAuthor, MyLibraryBook and MyLibraryBookInstance check here. [TODO 1.1, 1.2 & 1.3]
MyLibraryAuthor class (TODO 1.1):import grails.compiler.GrailsCompileStatic
import taack.ast.annotation.TaackFieldEnum
@GrailsCompileStatic
@TaackFieldEnum
class MyLibraryAuthor {
String firstName
String lastName
Date dateOfBirth
List<MyLibraryBook> listOfBooks //(1)
Boolean isActive = true //(2)
static constraints = {}
static hasMany = [listOfBooks: MyLibraryBook] //(3)
String toString() {
return firstName + ' ' + lastName
}
}
-
Bookswritten by thisAuthorwill be stored inlistOfBooks. -
isActivewill allow soft delete. -
hasManyis a GORM association keyword that allows us to define a one-to-many relationship between domain classes. It is used to automatically manage related collections (like a list of books belonging to one author). For more information, see the GORM Documentation.
MyLibraryBook class (TODO 1.2):import grails.compiler.GrailsCompileStatic
import taack.ast.annotation.TaackFieldEnum
@GrailsCompileStatic
@TaackFieldEnum
class MyLibraryBook {
String title
MyLibraryAuthor author
String description
int numberOfPages
List<MyLibraryBookInstance> listOfBookInstance //(1)
int count = 0
static constraints = {}
static hasMany = [listOfBookInstance: MyLibraryBookInstance]
int getNumberOfInstances() {
if (!listOfBookInstance) { return 0}
count = listOfBookInstance.count {it.isActive} as int
return count
}
}
-
BookInstancesof thisBookwill be stored inlistOfBookInstance.
MyLibraryBookInstance class (TODO 1.3):import grails.compiler.GrailsCompileStatic
import taack.ast.annotation.TaackFieldEnum
@GrailsCompileStatic
@TaackFieldEnum
class MyLibraryBookInstance {
MyLibraryBook book
Boolean isActive = true
Integer serialNumber = new Random().nextInt(100000) //(1)
static constraints = {}
static belongsTo = [book: MyLibraryBook] //(2)
}
-
Random().nextInt(100000)will create a random number between 0 and 100 000. -
belongsTois a GORM association keyword that defines the owning side of a many‑to‑one relationship between domain classes. It automatically manages the foreign‑key link and cascade behaviour (e.g., each Book belongs to a single Author). For more information, see the GORM Documentation.
You have now created all the necessary classes for this section!
Go to src/main/groovy/mylibrary.
NumberForInstances class:import grails.compiler.GrailsCompileStatic
import grails.validation.Validateable
import taack.ast.annotation.TaackFieldEnum
@TaackFieldEnum
@GrailsCompileStatic
class NumberForInstances implements Validateable {
Integer numberOfInstances
}
There is nothing to implement here. This class will be used later for capturing user input.
Define Author Menu
In this section, we will mainly review what has been done in Usage. After this section, you will master using UiTableSpecifier, UiFormSpecifier, UiShowSpecifier and UiFilterSpecifier.
We are going to create a:
-
table displaying all the authors,
-
button to create new authors using a form,
-
button to delete authors,
-
button to reactivate authors,
-
filter authors.
Author Table
We want to display the authors in a table. For that we need to create a UiTableSpecifier. Go to app/library/services/myLibrary/MyLibraryUiService in Author Menu.
Pro Mode: implement the Table by yourself [TODOS 2.1.1—2.1.6].
buildAuthorTable:MyLibraryAuthor author = new MyLibraryAuthor()
UiTableSpecifier authorTableSpecifier = new UiTableSpecifier()
authorTableSpecifier.ui {
header {
column {label author.firstName_}
label author.lastName_
if(!isSelect) {
label author.isActive_
label "Delete Author"
}
}
TaackFilter.FilterBuilder filter = taackFilterService.getBuilder(MyLibraryAuthor)
.setMaxNumberOfLine(10)
.setSortOrder(TaackFilter.Order.ASC, author.lastName_)
if(isSelect) {
filter.addFilter(buildIsActiveAuthorFilter(author))
}
iterate(
filter.build()) { MyLibraryAuthor authorIterator ->
rowColumn {
// TODO 2.10: Add rowAction to showAuthor with author's first name as label
if (isSelect) {
// TODO 3.5.6: Add SELECT action icon to select author with their id and string representation
}
}
rowField authorIterator.lastName_
if(!isSelect) {
rowField authorIterator.isActive_
rowColumn {
// TODO 2.7.1: Add DELETE action icon linked to deleteAuthor controller action
// TODO 2.7.2: Add ACTIVATE action icon linked to activateAuthor controller action
}
}
}
}
If you have trouble understanding this code please refer to Usage.
Note that we have added a buildIsActiveAuthorFilter, which has not yet been implemented. Let’s implement it now.
Note: The name of the author will not be displayed until TODO 2.10 is done.
Pro Mode: implement the Filter by yourself [TODO 2.2.1].
buildIsActiveAuthorFilter:UiFilterSpecifier buildIsActiveAuthorFilter(MyLibraryAuthor author) {
UiFilterSpecifier isActiveAuthorFilter = new UiFilterSpecifier()
isActiveAuthorFilter.ui MyLibraryAuthor, {
section "Filter", {
filterFieldExpressionBool "Is Active", new FilterExpression(true, Operator.EQ, author.isActive_) //(1)
}
}
}
-
We create a FilterExpression that takes a value, an operator and a FieldInfo.
There are different operators available:
-
IN: in a list of values,
-
NI: not in a list of values,
-
EQ: equal to another value,
-
NE: not equal to another value,
-
LT: less than another value,
-
IL: is like another value,
-
GT: greater than another value,
-
IS_EMPTY: is empty
We also want to add a filter that filters the authors by lastName let’s implement this filter.
Pro Mode: implement the Filter by yourself [TODO 2.2.2].
buildAuthorFilter:UiFilterSpecifier buildAuthorFilter() {
MyLibraryAuthor author = new MyLibraryAuthor()
UiFilterSpecifier authorFilterSpecifier = new UiFilterSpecifier()
authorFilterSpecifier.ui MyLibraryAuthor, {
section "Author Filter", {
filterField author.lastName_
}
}
}
If you have trouble understanding this code please refer to Usage.
Your table is now complete; we just need to render it on the page. To render previously built UiSpecifiers, we need to use taackUiService. We will do this in a module called listAuthor. Go to app/library/controller/myLibrary/MyLibraryController in Author Menu.
Pro Mode: implement listAuthor by yourself [TODO 2.3].
listAuthor method:def listAuthor() {
UiTableSpecifier tableAuthorSpecifier = myLibraryUiService.buildAuthorTable()
UiFilterSpecifier filterAuthorSpecifier = myLibraryUiService.buildAuthorFilter()
taackUiService.show(new UiBlockSpecifier().ui {
tableFilter filterAuthorSpecifier, tableAuthorSpecifier, {
menuIcon ActionIcon.CREATE, this.&createAuthor as MethodClosure
}
}, myLibraryUiService.buildMenu())
}
Note: ActionIcon has multiple Icons the main ones that we will use are: CREATE, EDIT, SAVE, DELETE, SHOW, IMPORT, EXPORT.
You can now start the server and navigate to your new app (myLibrary.app). The table should be functional, but currently you will only see the table headers since there are no authors in your database. So let’s proceed with creating a form and saving objects into the database. We now have to implement createAuthor, which requires creating a UiFormSpecifier.
Let’s go back to MyLibraryUiService.
Author Form
Pro Mode: implement buildAuthorForm by yourself [TODO 2.4].
buildAuthorForm:UiFormSpecifier buildAuthorForm(MyLibraryAuthor author) {
author ?= new MyLibraryAuthor(params)
UiFormSpecifier createAuthorSpecifier = new UiFormSpecifier()
createAuthorSpecifier.ui author, {
section "Author details", {
field author.firstName_
field author.lastName_
field author.dateOfBirth_
field author.isActive_
}
formAction MyLibraryController.&saveAuthor as MC
}
}
Let’s go back to MyLibraryController. We will render this form in a modal.
Pro Mode: implement createAuthor [TODO 2.5]
def createAuthor(MyLibraryAuthor author) {
UiFormSpecifier formAuthorSpecifier = myLibraryUiService.buildAuthorForm(author)
taackUiService.show(new UiBlockSpecifier().ui {
modal {
form formAuthorSpecifier
}
})
}
We now need to save the author. Let’s implement saveAuthor.
Pro Mode: implement saveAuthor [TODO 2.6].
@Transactional
def saveAuthor() {
taackSaveService.saveThenReloadOrRenderErrors(MyLibraryAuthor)
}
We have a fully functional table where we can display and add authors. Start the server again. You should now be able to click the button and fill out the form. We now want to be able to delete and reactivate authors. Let’s implement the deleteAuthor and reactivateAuthor methods. Let’s navigate to MyLibraryUiService in buildAuthorTable.
Delete and reactivate Author
Go to myLibraryUiService in buildAuthorTable.
Pro Mode: add the buttons [TODO 2.7].
rowAction ActionIcon.DELETE * IconStyle.SCALE_DOWN, MyLibraryController.&deleteAuthor as MC, authorIterator.id
rowAction ActionIcon.CREATE * IconStyle.SCALE_DOWN, MyLibraryController.&activateAuthor as MC, authorIterator.id
Navigate back to MyLibraryController to implement the methods deleteAuthor & activateAuthor.
Pro Mode: implement the methods [TODO 2.8 & 2.9]
deleteAuthor:@Transactional
def deleteAuthor(MyLibraryAuthor author) {
author.isActive = false
redirect action: 'listAuthor'
}
activateAuthor:@Transactional
def activateAuthor(MyLibraryAuthor author) {
author.isActive = true
redirect action: 'listAuthor'
}
We want to be able to display more information about the author in a modal.
Let’s now implement the showAuthor method. We first need to create a link to this method and a UiShowSpecifier in myLibraryUiService.
Let’s navigate to MyLibraryUiService in buildAuthorTable.
Pro Mode: add the buttons [TODO 2.10].
Add the show action. Replace the TODO 2.10:
rowAction authorIterator.firstName, MyLibraryController.&showAuthor as MC, authorIterator.id
Note: this time there is no ActionIcon. Instead, a link will be added to the authorName that will redirect to showAuthor.
Pro Mode: implement buildAuthorShow [TODO 2.11]
buildAuthorShow:UiShowSpecifier buildAuthorShow(MyLibraryAuthor author) {
UiShowSpecifier authorShowSpecifier = new UiShowSpecifier()
authorShowSpecifier.ui(author, {
fieldLabeled author.firstName_
fieldLabeled author.lastName_
fieldLabeled author.dateOfBirth_
fieldLabeled author.isActive_
})
}
We will render the buildAuthorShow in a modal using showAuthor.
Pro Mode: implement showAuthor [TODO 2.12]
showAuthor:def showAuthor(MyLibraryAuthor author) {
UiTableSpecifier tableBookSpecifier = myLibraryUiService.buildBookTable(author)
UiFilterSpecifier filterBookSpecifier = myLibraryUiService.buildBookFilter()
UiShowSpecifier showAuthorSpecifier = myLibraryUiService.buildAuthorShow(author)
taackUiService.show(new UiBlockSpecifier().ui {
modal {
show showAuthorSpecifier
tableFilter filterBookSpecifier, tableBookSpecifier
}
})
}
Congratulations! You have now implemented everything you need to display, add, delete, reactivate, and show authors. Start the server again and test all the new implementations.
Define Book Menu
We now want to display books in a table. For that we need to create another UiTableSpecifier. Go to app/library/services/myLibrary/MyLibraryUiService in Book Menu.
Pro Mode: implement the Table by yourself [TODOS 3.1.1 - 3.1.6].
buildBookTable:UiTableSpecifier buildBookTable(MyLibraryAuthor author = null) {
MyLibraryBook book = new MyLibraryBook()
UiTableSpecifier bookTableSpecifier = new UiTableSpecifier()
bookTableSpecifier.ui {
header {
column {label book.title_}
if (!author) {sortableFieldHeader book.author_}
column {
label "Number of instances "//book.numberOfInstances
}
if (!author) {
column {
label "Modify number of Book Instances"
}
}
}
TaackFilter.FilterBuilder filter = taackFilterService.getBuilder(MyLibraryBook)
.setMaxNumberOfLine(10)
.setSortOrder(TaackFilter.Order.ASC, book.title_)
if(author) {
filter.addRestrictedIds(author.listOfBooks*.id as Long[])
}
iterate(
filter.build()) { MyLibraryBook bookIterator ->
rowColumn {
// TODO 3.13: Add SHOW (showBook) action above title field in the rowColumn, this will be used to modify a book.
rowAction ActionIcon.EDIT * IconStyle.SCALE_DOWN, MyLibraryController.&createBook as MC, bookIterator.id
rowField bookIterator.title_
}
if (!author) {rowField bookIterator.author_}
rowColumn {
// TODO 3.8.1: Display number of instances (call getNumberOfInstances) and show as rowField.
}
if (!author) {
rowColumn {
// TODO 3.9: If author is null, add purchase and select actions for managing book instances.
}
}
}
}
}
We want to add a filter to filter books by title. Let’s implement it now.
Pro Mode: implement the Filter by yourself [TODO 3.2].
buildBookFilter:UiFilterSpecifier buildBookFilter() {
MyLibraryBook book = new MyLibraryBook()
UiFilterSpecifier bookFilterSpecifier = new UiFilterSpecifier()
bookFilterSpecifier.ui MyLibraryBook, {
section "Book Filter", {
filterField book.title_
}
}
}
We have now a table and a filter; we can render a page containing them. Go to app/library/controller/myLibrary/MyLibraryController in Book Menu.
Pro Mode: implement listBook by yourself [TODO 3.3].
listBook method:def listBook() {
UiTableSpecifier tableBookSpecifier = myLibraryUiService.buildBookTable()
UiFilterSpecifier filterBookSpecifier = myLibraryUiService.buildBookFilter()
taackUiService.show(new UiBlockSpecifier().ui {
tableFilter filterBookSpecifier, tableBookSpecifier, {
menuIcon ActionIcon.CREATE, this.&createBook as MethodClosure
}
}, myLibraryUiService.buildMenu())
}
You can now start the server. The book table should be functional, but currently you will only see the table headers since there are no books in your database. So let’s proceed with creating a form and saving objects into the database. We now have to implement createBook, which requires creating a UiFormSpecifier.
Let’s go back to MyLibraryUiService.
Pro Mode: implement buildBookForm by yourself [TODO 3.4].
buildBookForm:UiFormSpecifier buildBookForm(MyLibraryBook book) {
book ?= new MyLibraryBook(params)
UiFormSpecifier bookFormSpecifier = new UiFormSpecifier()
bookFormSpecifier.ui book, {
section "Book details", {
field book.title_
ajaxField book.author_, MyLibraryController.&selectAuthor as MC //(1)
field book.numberOfPages_
field book.description_
}
formAction MyLibraryController.&saveBook as MC
}
}
-
Here we use an
AjaxFieldto retrieve the information fromselectAuthor.
Let’s go back to MyLibraryController, and implement selectAuthor to clarify.
Pro Mode: implement selectAuthor by yourself, located in Author Menu [TODO 3.5]. Note 3.5.6 is in buildAuthorTable
selectAuthor:def selectAuthor() {
UiTableSpecifier tableAuthorSpecifier = myLibraryUiService.buildAuthorTable(true) //(1)
UiFilterSpecifier filterAuthorSpecifier = myLibraryUiService.buildAuthorFilter()
taackUiService.show(new UiBlockSpecifier().ui {
modal {
tableFilter filterAuthorSpecifier, tableAuthorSpecifier
}
})
}
-
Note that buildAuthorTable takes a Boolean to display the isSelect version of the UiTableSpecifier.
rowAction tr('default.role.label'), ActionIcon.SELECT * IconStyle.SCALE_DOWN, authorIterator.id, authorIterator.toString()
Pro Mode: implement createBook [TODO 3.6]
def createBook(MyLibraryBook book) {
UiFormSpecifier tableFormSpecifier = myLibraryUiService.buildBookForm(book)
taackUiService.show new UiBlockSpecifier().ui {
modal {
form tableFormSpecifier
}
}
}
We now need to save the book let’s implement saveBook.
Pro Mode: implement saveBook [TODO 3.7].
@Transactional
def saveBook() {
taackSaveService.saveThenReloadOrRenderErrors(MyLibraryBook)
}
You have a fully functional table where we can display and add books. Let’s implement the purchaseBook and selectBookInstance we want to be able to purchase bookInstances of a specific book and delete them. Let’s navigate to `MyLibraryUiService in buildBookTable.
Pro Mode: Implement the buttons and display the numberOfInstances [TODOS 3.8].
rowField bookIterator.numberOfInstances_
rowAction ActionIcon.DELETE * IconStyle.SCALE_DOWN, MyLibraryController.&selectBookInstance as MC, bookIterator.id
rowAction ActionIcon.ADD * IconStyle.SCALE_DOWN, MyLibraryController.&purchaseBook as MC, bookIterator.id
We want the user to select how many BookInstances they want to purchase. Therefore, we need to implement a UiFormSpecifier buildBookPurchase.
Pro Mode: implement the UiFormSpecifier [TODO 3.9]
buildBookPurchase:UiFormSpecifier buildBookPurchase(MyLibraryBook book) {
NumberForInstances numberForInstances = new NumberForInstances()
book ?= new MyLibraryBook(params)
UiFormSpecifier bookPurchaseSpecifier = new UiFormSpecifier()
bookPurchaseSpecifier.ui book, {
section "Purchase Number", {
field numberForInstances.numberOfInstances_ //(1)
}
formAction MyLibraryController.&purchaseAndSaveBook as MC
}
}
-
Note that we are using a class to retrieve the numberOfInstances the user wants to purchase. This number is only used for input and will not be saved to the database, unlike other class instances.
Navigate back to MyLibraryController to implement the methods purchaseBook & selectBookInstance.
Pro Mode: implement the methods [TODO 3.10–3.12]
purchaseBook:def purchaseBook(MyLibraryBook book) {
UiFormSpecifier tableAddBookInstanceSpecifier = myLibraryUiService.buildBookPurchase(book)
taackUiService.show new UiBlockSpecifier().ui {
modal {
form tableAddBookInstanceSpecifier
}
}
}
purchaseAndSaveBook:@Transactional
def purchaseAndSaveBook(NumberForInstances numberForInstances, MyLibraryBook book) {
for (int i = 0; i < (numberForInstances.numberOfInstances) as Integer; i++) {
MyLibraryBookInstance newBookInstance = new MyLibraryBookInstance()
newBookInstance.book = book
book.addToListOfBookInstance(newBookInstance)
}
taackUiService.ajaxReload()
}
selectBookInstance:def selectBookInstance(MyLibraryBook book) {
UiTableSpecifier bookInstanceTableSpecifier = myLibraryUiService.buildInstanceBookTable(book)
taackUiService.show new UiBlockSpecifier().ui {
modal true, {
table bookInstanceTableSpecifier
}
}
}
Let’s now implement the showBook method. We first need to create a UiShowSpecifier in myLibraryUiService.
Let’s navigate to MyLibraryUiService in buildBookTable.
Pro Mode: add the buttons [TODO 3.13].
Add the show action. Replace the TODO 3.13:
rowAction ActionIcon.SHOW * IconStyle.SCALE_DOWN, MyLibraryController.&showBook as MC, bookIterator.id
Pro Mode: implement buildBookShow [TODO 3.14]
buildBookShow: UiShowSpecifier buildBookShow(MyLibraryBook book) {
UiShowSpecifier bookShowSpecifier = new UiShowSpecifier()
bookShowSpecifier.ui(book, {
fieldLabeled book.title_
fieldLabeled book.author_
fieldLabeled book.numberOfPages_
fieldLabeled book.description_
fieldLabeled book.numberOfInstances_
})
}
Pro Mode: implement showBook [TODO 3.15]
showBook:def showBook(MyLibraryBook book) {
UiShowSpecifier showBookSpecifier = myLibraryUiService.buildBookShow(book)
taackUiService.show(new UiBlockSpecifier().ui {
modal {
show showBookSpecifier
}
})
}
We are now going to implement deleteBookInstance. We want to select the specific book instance that we would like to erase. Therefore, we need to create a buildBookInstanceTable with an isSelect option, We also need a buildIsActiveBookInstances filter since this is a soft delete (we will filter out inactive BookInstances`).
Let’s start by implementing the buildIsActiveBookInstances filter.
Pro Mode: implement buildIsActiveBookInstances [TODO 3.16].
buildIsActiveBookInstances.UiFilterSpecifier buildIsActiveBookInstances(MyLibraryBook book) {
MyLibraryBookInstance bookInstance = new MyLibraryBookInstance()
UiFilterSpecifier bookInstanceFilterSpecifier = new UiFilterSpecifier()
bookInstanceFilterSpecifier.sec MyLibraryBookInstance, {
filterFieldExpressionBool new FilterExpression(true, Operator.EQ, bookInstance.isActive_)
}
}
We can now implement the buildBookInstanceTable.
Pro Mode: implement buildBookInstanceTable [TODO 3.17].
buildBookInstanceTable.UiTableSpecifier buildInstanceBookTable(
MyLibraryBook book,
MyLibraryBookInstance bookInstance = null) {
UiTableSpecifier table = new UiTableSpecifier()
table.ui {
header {
label "Serial Number"
column {label "Delete"}
}
TaackFilter.FilterBuilder filter = taackFilterService
.getBuilder(MyLibraryBookInstance)
.addRestrictedIds(book.listOfBookInstance*.id as Long[])
filter.addFilter(buildIsActiveBookInstances(book))
iterate(filter.build()) { MyLibraryBookInstance bookInstanceIterator ->
rowField bookInstanceIterator.serialNumber_
rowColumn {
rowAction ActionIcon.DELETE * IconStyle.SCALE_DOWN,
MyLibraryController.&deleteBookInstances as MC,
bookInstanceIterator.id, [bookId:book.id]
}
}
}
}
Let’s now implement deleteBookInstances.
Pro Mode: implement deleteBookInstances [TODO 3.18].
deleteBookInstances:@Transactional
def deleteBookInstances(MyLibraryBookInstance bookInstance) {
MyLibraryBook book = MyLibraryBook.get(params.long('bookId'))
UiTableSpecifier bookInstanceTable = myLibraryUiService.buildInstanceBookTable(book)
bookInstance.isActive = false
bookInstance.save(flush: true, validate: false)
taackUiService.show new UiBlockSpecifier().ui {
closeModalAndUpdateBlock { //(1)
tableFilter(
myLibraryUiService.buildBookFilter(), //(2)
myLibraryUiService.buildBookTable(), //(2)
) {
menuIcon ActionIcon.CREATE, this.&createBook as MethodClosure //(2)
}
modal {
table bookInstanceTable //(3)
}
}
}
}
-
Using
closeModalAndUpdateBlockallows the user to instantly see the updated state of the deletedBookInstance. It closes the modal immediately after the user selects aBookInstance. -
We call the builders used to display the books so that the numberOfInstances is updated when we close the modal in the table displaying the books
-
We open a modal again. When the user selects a
BookInstance, the modal closes and then reopens, so the selectedBookInstanceno longer appears.
Congratulations! You have completed the first part of this project.