h1. Viewing and posting comments

The blog home page is now set, and we will continue by writing the post details page. This page will show all the comments about the current post, and will include a form to post new comments.

h2. <a>Creating the ‘show’ action</a>

To display the post details page, we will need a new action method on the **Application** controller. Let’s call it **show**:

bc. def show(id: Long) = {
    Post.byIdWithAuthorAndComments(id).map {
        html.show(_)
    } getOrElse {
        NotFound("No such Post")
    }
}

As you can see this action is pretty simple. We declare the **id** method parameter to automatically retrieve the HTTP **id** parameter as a **Long** Scala value. This parameter will be extracted either from the query string, from the URL path or from the request body.

p(note). If we try to send an **id** HTTP parameter that is not a valid number, Play will automatically add a validation error to the **errors** container.

This action will display the **/yabe/app/views/Application/show.scala.html** template:

bc. @(post:(models.Post,models.User,Seq[models.Comment]))
 
@main(title = post._1.title) {
    
    @display(post, mode = "full")
    
}

Because we’ve already written the **display** function, this page is really simple to write.

h2. <a>Adding links to the details page</a>

In the display tag we’ve left all links empty (using **#**). It’s now time to make these links point to the **Application.show** action. With Play you can easily build links in a template using the **action(…) helper**. It uses the router to ‘reverse’ the URL needed to call the specified action. 

Let’s edit the **/yabe/app/views/tags/display.scala.html** tag:

bc. ...
<h2 class="post-title">
    <a href="@action(controllers.Application.show(post._1.id()))">
        @post._1.title
    </a>
</h2>
...

You can new refresh the home page, and click a post title to display the post.

!images/guide4-0!

It’s great, but it lacks a link to go back to the home page. Edit the **/yabe/app/views/main.scala.html** template to complete the title link:

bc. ...
<div id="title">
    <span class="about">About this blog</span>
    <h1>
        <a href="@action(controllers.Application.index)">
            @play.Play.configuration.get("blog.title")
        </a>
    </h1>
    <h2>@play.Play.configuration.get("blog.baseline")</h2>
</div>
... 

We can now navigate between the home page and the post detail pages.

h2. <a>Specifying a better URL</a>

As you can see, the post detail page URL looks like:

bc. /application/show?id=1

This is because Play has used the default ‘catch all’ route.

bc. *       /{controller}/{action}                  {controller}.{action}

We can have a better URL by specifying a custom path for the **Application.show** action. Edit the **/yabe/conf/routes** file and add this route after the first one:

bc. GET     /posts/{id}                             Application.show

p(note). This way the **id** parameter will be extracted from the URL path. 

Refresh the browser and check that it now uses the correct URL.

h2. <a>Adding pagination</a>

To allow users to navigate easily through posts, we will add a pagination mechanism. We’ll extend the Post class to be able to fetch previous and next post as required.

Here is the SQL query we will use to fetch both previous and next post from a particular element:

bc. (
    select *, 'next' as pos from post 
    where postedAt < {date} order by postedAt desc limit 1
)
    union
(
    select *, 'prev' as pos from post 
    where postedAt > {date} order by postedAt asc limit 1
)
 
order by postedAt desc

And we will translate the result as a @(Option[Post], Option[Post])@ value, where the first element of the tuple is the previous post, and the second element is the next post:

bc. opt('pos.is("prev") ~> Post.on("")) ~ opt('pos.is("next") ~> Post.on(""))

Here is the new @prevNext@ method we need to add to the @Post@ class:

bc. def prevNext = {        
    SQL(
        """
            (
                select *, 'next' as pos from post 
                where postedAt < {date} order by postedAt desc limit 1
            )
                union
            (
                select *, 'prev' as pos from post 
                where postedAt > {date} order by postedAt asc limit 1
            )
 
            order by postedAt desc
            
        """
    ).on("date" -> postedAt).as( 
        opt('pos.is("prev")~>Post.on("")) ~ opt('pos.is("next")~>Post.on("")) 
        ^^ flatten
    )
}

Now, let's add this information to the *show.html* template:

bc. def show(id: Long) = {
    Post.byIdWithAuthorAndComments(id).map { post =>
        html.show(post, post._1.prevNext)
    } getOrElse {
        NotFound("No such Post")
    }
}

We will call these methods several times during a request so they could be optimized, but they’re good enough for now. Also, add the pagination links at the top of the **show.scala.html** template:

bc. @(
    post:(models.Post,models.User,Seq[models.Comment]),
    pagination:(Option[models.Post],Option[models.Post])
)
 
@main(title = post._1.title) {
    
    <ul id="pagination">
        @pagination._1.map { post =>
            <li id="previous">
                <a href="@action(controllers.Application.show(post.id()))">
                    @post.title
                </a>
            </li>
        }
        @pagination._2.map { post =>
            <li id="next">
                <a href="@action(controllers.Application.show(post.id()))">
                    @post.title
                </a>
            </li>
        }
    </ul>
    
    @display(post, mode = "full")
    
}

It’s better now.

h2. <a>Adding the comment form</a>

Now it’s time to set up a comments form. We’ll start by adding the **postComment** action method to the Application controller.

bc. def postComment(postId:Long) = {
    val author = params.get("author")
    val content = params.get("content")
    Comment.create(Comment(postId, author, content))
    Action(show(postId))
}

As you we return an @Action@ value to indicate that we want to redirect to the @show(postId)@ action.

Let’s write the HTML form in the **show.html** template (after the **#{display /}** tag in fact):

bc. <h3>Post a comment</h3>
 
@form(controllers.Application.postComment(post._1.id())) {
    <p>
        <label for="author">Your name: </label>
        <input type="text" name="author" />
    </p>
    <p>
        <label for="content">Your message: </label>
        <textarea name="content"></textarea>
    </p>
    <p>
        <input type="submit" value="Submit your comment" />
    </p>
}

You can now try posting a new comment. It should just work.

!images/guide4-1!

h2. <a>Adding validation</a>

Currently we don’t validate the form content before creating the comment. We would like to make both fields required. We can easily use the Play validation mechanism to ensure that the HTTP parameters are correctly filled in. Modify the **postComment** action to some validation and check that no error occurs:

bc. def postComment(postId:Long) = {
    val author = params.get("author")
    val content = params.get("content")
    Validation.required("author", author)
    Validation.required("content", content)
    if(Validation.hasErrors) {
        show(postId)
    } else {
        Comment.create(Comment(postId, author, content))
        Action(show(postId))
    }
}

p(note). **Don’t forget** to import **play.data.validation._** as well.

As you can see, in case of validation errors, we re-display the post detail page. We have to modify the form code to display the error message:

bc. <h3>Post a comment</h3>
 
@form(controllers.Application.postComment(post._1.id())) {
    
    @if(errors) {
        <p class="error">
            All fields are required!
        </p>
    }
    
    <p>
        <label for="author">Your name: </label>
        <input type="text" name="author" value="@params.get("author")">
    </p>
    <p>
        <label for="content">Your message: </label>
        <textarea name="content">@params.get("content")</textarea>
    </p>
    <p>
        <input type="submit" value="Submit your comment" />
    </p>
}

Note that here we reference @errors@ and @params@. So we need to add them to the template parameters list, and we can even mark them as *implicit*:

bc. @(
    post:(models.Post,models.User,Seq[models.Comment]),
    pagination:(Option[models.Post],Option[models.Post])
)(
    implicit 
    params:play.mvc.Scope.Params,
    flash:play.mvc.Scope.Flash,
    errors:Map[String,play.data.validation.Error]
)
 
…

To make the UI feedback more pleasant for the poster, we will add a little JavaScript to automatically set focus on the comment form in case of an error. As this script uses "JQuery Tools Expose":files/jquery.tools.min.js as support libraries, you have to include them. Download these two libraries to the **yabe/public/javascripts/** directory and modify the **main.html** template to include them:

bc. …
<script src="@asset("public/javascripts/jquery-1.5.2.min.js")"></script>
<script src="@asset("public/javascripts/jquery.tools.min.js")"></script>
…

Now you can add this script to the **show.scala.html** template (add it at the end of the page):

bc. <script type="text/javascript" charset="utf-8">
    $(function() {         
        // Expose the form 
        $('form').click(function() { 
            $('form').expose({api: true}).load(); 
        }); 
        
        // If there is an error, focus to form
        if($('form .error').size()) {
            $('form').expose({api: true, loadSpeed: 0}).load(); 
            $('form input[type=text]').get(0).focus();
        }
    });
</script>

!images/guide4-2!

The comment form looks pretty cool now. We will add two more things.

First, we will display a success message after a comment is successfully posted. For that, we use the flash scope that allows us to pass messages from one action call to the next one.

Modify the **postComment** action to add a success message:

bc. def postComment(postId:Long) = {
    val author = params.get("author")
    val content = params.get("content")
    Validation.required("author", author)
    Validation.required("content", content)
    if(Validation.hasErrors) {
        show(postId)
    } else {
        Comment.create(Comment(postId, author, content))
        flash += "success" -> ("Thanks for posting " + author)
        Action(show(postId))
    }
}

and display the success message in **show.html** if present (add it at the top the page):

bc. …
@if(flash.get("success")) {
    <p class="success">@flash.get("success")</p>
}
…

!images/guide4-3!

The last thing we will adjust in this form is the URL used for the **postComment** action. As always, it uses the default catch-all route because we didn’t define any specific route. So add this route to the application routes file:

bc. POST    /posts/{postId}/comments                Application.postComment

That’s done.

p(note). Next: %(next)"Setting up a Captcha":guide5%. 

