John
John

Reputation: 11861

Grails why does respond not work and renders nothing?

I have a domain class TmMessage for which I use generate-all to create the scaffolded controller and views. The auto-generated show() method looks like:

def show(TmMessage tmMessage) {
    respond tmMessage
}

Scaffolding is defined in my BuildConfig.groovy:

plugins {
    compile ":scaffolding:2.1.2"
}

The list of TmMessage objects is given by controller's method:

def index(Integer max) {
    params.max = Math.min(max ?: 10, 100)
    respond TmMessage.list(params), model:[tmMessageCount: TmMessage.count()]
}

The TmMessages are stored in a hasMany List of a parent object, TmBulkMessage, and I can see the TmMessages listed ok in when inspecting a TmBulkMessage. However, the list of TmMessage objects displays nothing (I can see a number of pages of TmMessage objects, but the details for them don't display). When I click on one of the links from the TmBulkMessage to look at a specific TmMessage object, nothing displays. I believe that's because the tmMessage being displayed is null.

The show() method is very different to what I've seen elsewhere, where it looks like (taken straight from Grails docs):

def show() {
    def book = Book.get(params.id)
    log.error(book)
    [bookInstance : book]
}

The auto-generated unit tests all use the first method, so what's going on here please? Is there something missing from the scaffolded code?

EDIT:

From the Grails docs, what's new in 2.3 (I'm using 2.4):

Domain Classes As Command Objects When a domain class is used as a command object and there is an id request parameter, the framework will retrieve the instance of the domain class from the database using the id request parameter.

So it would appear that the domain class / command object interface provided by Grails is returning null.

FURTHER EDIT:

Thanks to Gregor's help, it would appear that the domain object binding is working ok, but that the respond isn't working as advertised.

The show.gsp is below:

<%@ page import="com.example.TmMessage" %>
<!DOCTYPE html>
<html>
    <head>
        <meta name="layout" content="main">
        <g:set var="entityName" value="${message(code: 'tmMessage.label', default: 'TmMessage')}" />
        <title><g:message code="default.show.label" args="[entityName]" /></title>
    </head>
    <body>
        <a href="#show-tmMessage" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content&hellip;"/></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="index"><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-tmMessage" 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 tmMessage">

                <g:if test="${tmMessage?.bulkMessage}">
                <li class="fieldcontain">
                    <span id="bulkMessage-label" class="property-label"><g:message code="tmMessage.bulkMessage.label" default="Bulk Message" /></span>

                        <span class="property-value" aria-labelledby="bulkMessage-label"><g:link controller="tmBulkMessage" action="show" id="${tmMessage?.bulkMessage?.id}">${tmMessage?.bulkMessage?.encodeAsHTML()}</g:link></span>

                </li>
                </g:if>

                <g:if test="${tmMessage?.message}">
                <li class="fieldcontain">
                    <span id="message-label" class="property-label"><g:message code="tmMessage.message.label" default="Message" /></span>

                        <span class="property-value" aria-labelledby="message-label"><g:fieldValue bean="${tmMessage}" field="message"/></span>

                </li>
                </g:if>

            </ol>
            <g:form url="[resource:tmMessage, action:'delete']" method="DELETE">
                <fieldset class="buttons">
                    <g:link class="edit" action="edit" resource="${tmMessage}"><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>

The output of tmMessage?.dump() within show() is:

<com.example.TmMessage@6d6cf0a5 message=abc errors=grails.validation.ValidationErrors: 0 errors $changedProperties=null id=1 version=0 bulkMessage=com.example.TmBulkMessage : 1>

If I amend the gsp to read:

<ol class="property-list tmMessage">
    <% System.out.println "tmMessage : " + tmMessage %>

Then I get "tmMessage : null" written to the console when I view the page.

I have changed show() to read:

def show(TmMessage tmMessage) {
    respond tmMessage, [model: [tmMessage : tmMessage]]
}

Which appears to fix the rendering issue for show. I don't know what needs to be changed for index(). When I select "edit" from the show page, I get a blank textfield for the message field and I don't know if this is expected behaviour or not, but it would be much preferred if the field was preloaded with the existing value.

Upvotes: 1

Views: 3552

Answers (3)

Gregor Petrin
Gregor Petrin

Reputation: 2931

I think I know now what the problem is: respond has a really weird variable naming convention. If you respond with a single TmMessage instance, the variable will be called tmMessageInstance in the view. If you respond with a list of them, the variable will be called tmMessageInstanceList. If you return a set... well, you know what I mean.

So in the GSP code above you can probably replace all tmMessage with tmMessageInstance and get rid of [model: [tmMessage : tmMessage]] in the controller. A habit of mine is to explicitly test for the presence and type of every expected model variable in every single GSP I write, like so: <% assert tmModelInstance instanceof com.package.TmModel %>. Those lines then serve as documentation and if the controller passes something unexpected into your GSP (can happen frequently during active development, especially when filling the data model from services), your code fails quite obviously with a nice diagnostic message.

In my opinion a better option for Grails would be to stick to a single variable for respond renderers (e.g. model), document it in several places just so nobody misses this, and then people can detect what was in there when necessary (how often does it happen anyway that you don't know if you will have a list or a single instance for a single view/template?).

EDIT: Apparently you can use the Map syntax with respond and have it be used as the model to get fixed variable names, it was just poorly documented: https://github.com/grails/grails-doc/commit/13cacbdce73ca431619362634321ba5f0be570a1

Upvotes: 8

John Rellis
John Rellis

Reputation: 553

This was happening me for when I had inheritance in my domain model.

For instance, if we have

class Vehicle {}

and

class Car extends Vehicle {}

The scaffolded controller action was passing carInstanceList into the view when the view was trying to render vehicleInstanceList.

As stated in previous answers, the respond method creates variable names by convention, the convention seems to fail here

def index(Integer max) {
  params.max = Math.min(max ?: 10, 100)
  respond Vehicle.list(params), model:[vehicleInstanceCount: Vehicle.count()] //actually injecting carInstanceList
}

Had to be changed to :

def index(Integer max) {
  params.max = Math.min(max ?: 10, 100)
  def vehicles = Vehicle.list(params)
  respond vehicles, model:[vehicleInstanceCount: Vehicle.count(), vehicleInstanceList:vehicles]
}

I think it is to do with checking the class of the first element in the list maybe and if that is a car, naming it carInstanceList, if the first was a vehicle, the issue probably wouldn't present itself

Upvotes: 1

John
John

Reputation: 11861

With thanks to Gregor, whose help put me on the right track, the issue is with the generated code. It would appear that there is not a model being passed to the view, hence it's rendering nothing. Below are the changes to index(), show() edit()

def index(Integer max) {
    params.max = Math.min(max ?: 10, 100)
    respond TmMessage.list(params), model:[tmMessageCount: TmMessage.count(), tmMessageList : TmMessage.list(params)]
}


def show(TmMessage tmMessage) {
    respond tmMessage, [model: [tmMessage: tmMessage]]
}

def edit(TmMessage tmMessage) {
    respond tmMessage, [model: [tmMessage: tmMessage]]
}

This preloaded the text fields with the correct values.

I also had to amend the parameters sent when there was an error when creating by passing the model along with the desired view. Below is the example for save():

@Transactional
def save(TmMessage tmMessage) {
    if (tmMessage == null) {
        notFound()
        return
    }

    if (tmMessage.hasErrors()) {
        respond tmMessage.errors, [view:'create', model: [tmMessage: tmMessage]] 
        return
    }

    tmMessage.save flush:true

    request.withFormat {
        form multipartForm {
            flash.message = message(code: 'default.created.message', args: [message(code: 'tmMessage.label', default: 'TmMessage'), tmMessage.id])
            redirect tmMessage
        }
        '*' { respond tmMessage, [status: CREATED] }
    }
}

Upvotes: 1

Related Questions