Sunday, February 3, 2008

How to remember selection with grails

I am new to the grails web framework and coming with intermediate background on JSF. That may be the reason why I needed so much time to find out how to remember selection within tag.

The scenario is following:
When application starts, from the combo box user selects context in which he wants to work and then all queries on domain objects are restricted to this context.

E.g. You select an author, and then when searching for book you always get only books from selected author.

So this selection should be remembered as long as it is not changed. After all, of course, solution is very simple. Just place your selection into session context and then before executing any action read selection from the session.
(This is obvious now but need not to be so obvious to somebody with JSF background as for the most of the time you need not to place explicitly into session when working with JSF)

So let make very short and not full example.

Selection tag may look like this:

g:select name="author.id" from="${Author.list()}" value="${session.authorId}" optionkey="id" noselection="['':'--select author--']"

Action in controller may look like this:

def list = {
if(!params.max) params.max = 50
params.sort = "priority"
params.order = "desc"

def list

//PART WHERE WE TAKE CARE ABOUT AUTHOR BEING CORRECTLY PLACED INTO //SESSION
boolean authorIdInParams = params.author && params.author.id
boolean authorIdInSession = session.authorId

if (!authorIdInSession && !authorIdInParams) {
println "author id unspecified"
[bookList:list]
return
}

// if author is within params it means we have reselected another author
if (authorIdInParams) {
session.authorId = params.author.id
}

// if we are here author is definitly within session
def author = Author.get(session.authorId)

def result = Book.findAllWhere(author:author)

[ authorList: result ]
}

5 comments:

Anonymous said...

Jan... being a newbie to grails I like your samples but this time I'm confused:

I tried to put your two samples (with the template and taglib sample) together but it doesn't work. somehow the author doesn't get saved in the session and there's no visible button for the dropdown combo either (to trigger the selection)

With that, the book list remains empty, no matter what.

I know, you don't get payed by this, therefore I really appreciate your effort but I'm afraid you need to provide more details (codes) and don't mix the domains please:
stick to either author/book or manager/project otherwise the code is hard to follow (for me at least).

Sorry for the critics, hope you won't hate me for this.

jan said...

I appreciate your comment very much. I think you are right. Using different domains just increase confusion. Also when I reread post I can see that it could be written much better. I will try to improve things you mentioned.

Thanks

Anonymous said...

Hi Jan,
sticking to the book/author subject, I've put the code together, however slightly modified with a remoteFunction call (note: standard html opening and closing brackets are exchanged to "-=" and "=-"):

--------------- Book.groovy ----------------

class Book {
String title
String isbn

// define 1:n cardinalities (parent):
Author writer
static belongsTo = Author

}

--------------- _authorSelectionTemplate.gsp: ---------------
-=g:javascript library="prototype" /=-


-=g:form method="post" controller="book"=-
-=g:select name="author.id" update:'booklist', onchange="${remoteFunction(controller: 'book', action: 'list', params: '\'authorId=\' + this.options[this.selectedIndex].value')}" from="${Author.list()}" optionKey="id" optionValue="name" noSelection="['':'Select book writer...']"=-
-=/g:select=-
-=/g:form=-


-------------- BookController->list(): --------------

def list = {

if(!params.max) params.max = 10

//PART WHERE WE TAKE CARE ABOUT AUTHOR BEING CORRECTLY PLACED INTO
//SESSION
if (!params.authorId && !session.authorId) {
def booklist = Book.list()
println "### " + booklist[0].writer.name

render(view:'list', model:[bookInstanceList:booklist])
} else {

// if author is within params it means we have reselected another
// author and the session variable gets updated
if (params.authorId){
session.authorId = params.authorId
}

// Retrieves an instance of the Author domain class for the specified id
def authorObj = Author.get(session.authorId)

def result = Book.findAllWhere(writer:authorObj)
println "### " + result[0].writer.name

render(view:'list', model:[bookInstanceList:result])
}
}

--------------------------------------
Hope it's readable.
The only thing that needs improvement is the render calls at the end of list() method: I always need to refresh the browser to be able to see the updated table content. No idea why it's not working.

cheers

Anonymous said...

I've got it - never mind my previous post.
For other grails-rookies like me, as future reference here's everything focusing on the real time table update based on the chosen combobox element (just like before, html brackets are exchanged to "-=" and "=-"):

-------------------- grails-app/views/common/_authorSelectionTemplate.gsp ------------------

-=g:javascript library="prototype" /=-


-=g:form method="post" controller="book"=-
-=g:select name="author.id" update:'booklist', onchange="${remoteFunction(controller: 'book', action: 'listSelection', update:'booklist', params: '\'authorId=\' + this.options[this.selectedIndex].value')}" from="${Author.list()}" optionKey="id" optionValue="name" noSelection="['':'Select book writer...']"=-
-=/g:select=-
-=/g:form=-

---------------------- code snippet from grails-app/controllers/BookController.groovy ---------------------

def list = {
if(!params.max) params.max = 10
[ bookInstanceList: Book.list( params ) ]
}



def listSelection = {
if(!params.max) params.max = 10

//PART WHERE WE TAKE CARE ABOUT AUTHOR BEING CORRECTLY PLACED INTO
//SESSION
if (!params.authorId && !session.authorId) {
println "TODO: missing error page!"
} else {
// if author is within params it means we have reselected another
// author and the session variable gets updated
if (params.authorId){
session.authorId = params.authorId
}

// Retrieves an instance of the Author domain class for the specified id
def authorObj = Author.get(session.authorId)

def result = Book.findAllWhere(writer:authorObj)

render ( template:"/grails-app/views/common/authorBooks", model:[bookInstanceList:result] )
}
}

------------------------ grails-app/views/book/list.gsp ---------------------------------

-=html=-
-=head=-
-=meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/=-
-=meta name="layout" content="main" /=-
-=title=-Book List-=/title=-
-=/head=-
-=body=-
-=div class="nav"=-
-=span class="menuButton"=--=a class="home" href="${createLinkTo(dir:'')}"=-Home-=/a=--=/span=-
-=span class="menuButton"=--=g:link class="create" action="create"=-New Book-=/g:link=--=/span=-
-=/div=-
-=div class="body"=-
-=h1=-Book List-=/h1=-
-=g:if test="${flash.message}"=-
-=div class="message"=-${flash.message}-=/div=-
-=/g:if=-

-=janblog:selectAuthor/=-
-=div id="booklist"=-
-=g:render template="/grails-app/views/common/authorBooks" /=-
-=/div=-
-=/div=-
-=/body=-
-=/html=-


--------------- grails-app/taglib/AuthorSelectionTagLib.groovy ----------------------------

class AuthorSelectionTagLib {

static namespace = 'janblog'



def selectAuthor = {attrs, body -=-
out -=-= render(template:"/common/authorSelectionTemplate")
}
}

----------------- grails-app/views/common/_authorBooks.gsp --------------------

-=div class="list"=-
-=table=-
-=thead=-
-=tr=-

-=g:sortableColumn property="id" title="Id" /=-

-=g:sortableColumn property="isbn" title="Isbn" /=-

-=g:sortableColumn property="title" title="Title" /=-

-=th=-Writer-=/th=-

-=/tr=-
-=/thead=-
-=tbody=-
-=g:each in="${bookInstanceList}" status="i" var="bookInstance"=-
-=tr class="${(i % 2) == 0 ? 'odd' : 'even'}"=-

-=td=--=g:link action="show" id="${bookInstance.id}"=-${fieldValue(bean:bookInstance, field:'id')}-=/g:link=--=/td=-

-=td=-${fieldValue(bean:bookInstance, field:'isbn')}-=/td=-

-=td=-${fieldValue(bean:bookInstance, field:'title')}-=/td=-

-=td=-${fieldValue(bean:bookInstance, field:'writer')}-=/td=-

-=/tr=-
-=/g:each=-
-=/tbody=-
-=/table=-
-=/div=-
-=div class="paginateButtons"=-
-=g:paginate total="${Book.count()}" /=-
-=/div=-

-------------------------------------------------------------------------

key element is the "update" parameter of the remoteFunction. After that, with a bit of code customizing, finally works everything as planned.

enjoy

Anonymous said...

Great example!! thank you very much!