yabeチュートリアル その4 コメントの投稿と表示
※ 本記事はPage not found — Playframeworkを和訳したものです。
ブログのホームページは出来上がっているので、続いてPost詳細ページをコーディングします。このページは今のPostに対する全てのコメントを表示し、新しいコメントを投稿するフィールドを含めます。
showアクション作成
Post詳細ページを表示するために、Applicationコントローラーに新しいActionが必要です。showアクションとしてこれを作りましょう。
def show(id: Long) = { Post.byIdWithAuthorAndComments(id).map { html.show(_) } getOrElse { NotFound("No such Post") } }
このようにshowアクションはとてもシンプルです。Long型のScala値としてHTTPのidパラメータを自動的に受け取るメソッドパラメータをidとして宣言しています。このパラメータは、クエリストリングか、URLパスか、リクエスト電文のBody部かのいずれかから引き抜かれます。
もし送信したHTTPパラメータのidが、数値型でなければあ、errorsコンテナにバリデーションエラーを自動的に加えます。
このアクションは/yabe/app/views/Application/show.scala.htmlテンプレートを表示します。
@(post:(models.Post,models.User,Seq[models.Comment])) @main(title = post._1.title) { @display(post, mode = "full") }
display関数は既に作ってあるので、このページはとてもシンプルに書けます。
詳細ページにリンクを追加
displayタグの中では、全てのリンクは#を使って空にしてあります。Application.showアクションにリンクポイントを設定しましょう。Playフレームワークでは、テンプレートの中でaction
ヘルパーを使うことで簡単にリンクを構築することができます。
... <h2 class="post-title"> <a href="@action(controllers.Application.show(post._1.id()))"> @post._1.title </a> </h2> ...
ホームページをリロードし、Postタイトルをクリックし、Postページを表示してください。
すばらしい、ですが、ホームページに戻るリンクがありませんね。/yabe/app/views/main.scala.htmlテンプレートを編集し、タイトルリンクを完成させましょう。
... <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> ...
これでホームページとPost詳細ページの間をナビゲートすることができるようになりました。
URLをもう少しよくしましょう。
見て分かるように、Post詳細ページのURLは以下のような具合です。
/application/show?id=1
Playフレームワークはデフォルトの、「catch all」ルートを使っているからです。
* /{controller}/{action} {controller}.{action}
Application.showアクションへのカスタムパス特定することによって、URLを良くすることができます。/yabe/conf/routes ファイルを編集し、最初のものの後ろに、以下のルートを追加してください。
GET /posts/{id} Application.show
この方法では、URLパスからidが取得されます。
ブラウザを再読み込みして、挙動を確認してください。
ページングを追加
Postを簡単にナビゲーションするために、ページングの仕掛けを追加しましょう。Postクラスを拡張して、前と次をくくり付けられるようにします。
ここにあるSQLクエリは、特定のエレメントから前と次のPostを括り付けるために使うものです。
( 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
そしてクエリ結果を(Option[Post], Option[Post])の値に変換させます。このふたつのうち、一つ目が前の要素がPostで、二つ目の要素が次のPostに該当します。
opt('pos.is("prev") ~> Post.on("")) ~ opt('pos.is("next") ~> Post.on(""))
これがPostクラスに追加する新しいprevNextメソッドです。
case class Post( id: Pk[Long], title: String, content: String, postedAt: Date, author_id: Long ){ 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 ) } }
そしてshow.htmlテンプレートに、以下の情報を追加しましょう。
def show(id: Long) = { Post.byIdWithAuthorAndComments(id).map { post => html.show(post, post._1.prevNext) } getOrElse { NotFound("No such Post") } }
これらのメソッドをリクエストの間、何回か呼ぶことになりますが、まだ十分ではありません。show.scala.htmlテンプレートのトップにページングリンクを追加しましょう。
@( 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") }
コメントフォームを追加
コメントフォームをセットアップしましょう。postCommentアクションをApplicationContorllerに追加します。
def postComment(postId:Long) = { val author = params.get("author") val content = params.get("content") Comment.create(Comment(postId, author, content)) Action(show(postId)) }
Action値を返すようにしたのは、show(postId)にリダイレクトさせたいということを指し示しています。
show.htmlテンプレートの、#{display /} タグの後ろに、HTMLフォームを書きましょう。
<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> }
さて、新しいコメントをPostしてみましょう。
バリデーションを追加
現在、コメント登録前にコンテンツのバリデーションをしていません。両方の入力を必須にしたいと思います。Playバリデーション機能を使えば、HTTPパラメータが埋められていることを簡単に確実にすることができます。postCommentアクションを編集して、バリデーションを行い、エラーが起きていないことを確認てください。
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)) } }
play.data.validation._ をImportするのを忘れないでください。
見て分かるように、バリデーションエラーになった場合、Post詳細ページを再表示しています。エラーメッセージを表示するように修正しましょう。
<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> }
errorsとparamsを参照しています。それをテンプレートパラメータリストに加えて、暗黙値としてマークする必要があります。
@( 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] ) …
投稿者に対してもっと分かりやすいフィードバックUIを作るために、エラー時に自動的に入力欄にフォーカスが当たる小さなJavaScriptを追加します。このスクリプトはサポートライブラリとしてJQuery Tools Exposeを使っているので、これをインクルードしておきます。このライブラリをダウンロードして、yabe/public/javascripts/ディレクトリに格納して、main.htmlテンプレートを以下のインクルード文を含めるように修正してください。
… <script src="@asset("public/javascripts/jquery-1.5.2.min.js")"></script> <script src="@asset("public/javascripts/jquery.tools.min.js")"></script> …
これで以下のスクリプトを、show.scala.htmlテンプレートに追加できるようになりました。(ページの最後に追加するように御願いします。)
<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>
コメントフォームはとってもかっこ良くなりました。もう2つやりましょう。
ひとつは、Postが成功した時に、成功メッセージを出す処理を追加します。このためにflash scopeを使います。
postCommentを編集し、成功メッセージを追加してください。
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)) } }
show.htmlに表示を追加します。
… @if(flash.get("success")) { <p class="success">@flash.get("success")</p> } …
最後にURLをシンプルにしましょう。ルートファイルに以下定義を追加してください。
POST /posts/{postId}/comments Application.postComment
以上。