Play2.0でセレクトボックスにEnumを貼れるようにする
スタプラわー!(挨拶) 第8回全国戦優勝者です(本当)
携帯ゲームって初めてやったけど運営が伏せたルールをプレイヤーが当てる遊び的な感じですね。
折角攻略法見つけても「あれ不具合だから修正しました」て言われる恐ろしい展開もある。
閑話休題、"Play 2.0"もとい"Playframewok 2.0"もとい"Play framework 2.0"で*1、勉強も兼ねて自分用のWebアプリをちょこちょこ作ってるんだけど、いろいろプリミティブな感じで歯がゆい。
例えば、データベースの値をフォームに貼付けるのにいろいろ準備が面倒だと思う。computer-databaseサンプルのComputer編集フォームでセレクトボックスでCompanyを選択する場合で説明すると、次のような感じになる。
- ケースクラスComputerにOption[Long]のcompanyIdというフィールドを用意する
- Computer編集用のフォームcomputerFormのマッピングに
"company" -> optional(longNumber)
というマッピングを加える - ケースクラスCompanyのコンパニオンオブジェクトにoptionsというメソッドを用意して、DBから取得したCompanyオブジェクトを(IDの文字列、名称)のペアを作ってSeq[(String,String)]として戻すようにする。
- テンプレート上で以下のようにヘルパーを呼び出す。
@select( computerForm("company"), Company.options, '_label -> "Company", '_default -> "-- Choose a company --", '_showConstraints -> false )
どの辺がいけてないなーと思うかというと、UI部品であるselectが要求するSeq[(String,String)]をモデルオブジェクト側で用意しているところと、そうやって用意しておいても、結局フォーム受信時にはLongとして受け取っていて、選択されたCompanyの内容を利用しようとするとDBから取得し直さないところ。
このselectを一つの再利用可能な部品としてみた時に、普通に考えれば「Companyのリストを渡したら、ユーザが選択したCompanyが取得出来る」というのが一番便利だと思うんだけど、そんな風に作れないものなのか。
ということで練習としてセレクトボックスにEnumを貼って、選択されたEnumを受け取れるようにしてみた。
Enum(列挙型)およびFormatterの定義
衣装データを形状と色とで表現したい。形状は文字列で入力するとして、色はあらかじめ決められた8色から選択するようにする。
package models import Color._ case class Costume(shape:String, color:Color)
package models object Color extends Enumeration { type Color = Value val Black = Value("黒") val White = Value("白") val Brown = Value("茶") val Green = Value("緑") val Yellow = Value("黄") val Red = Value("赤") val Purple = Value("紫") val Blue = Value("青") }
これを、フォームの値として持てるようにしたい。Formのmappingで扱えるようにするには、bindとunbindを定義したFormatterをその型用に用意する。
package data.format import scala.util.control.Exception.allCatch import models.Color import models.Color._ import play.api.data.format.Formats.intFormat import play.api.data.format.Formatter import play.api.data.FormError object ColorFormat { implicit def colorFormat: Formatter[Color] = new Formatter[Color] { override val format = Some("format.color", Nil) def bind(key: String, data: Map[String, String]): Either[Seq[FormError], Color] = { for ( n <- intFormat.bind(key, data).right; c <- allCatch.either(Color(n)).left.map(_ => Seq(FormError(key, "error.range", Nil))).right) yield c } def unbind(key: String, value: Color) = Map(key -> value.id.toString) } }
フォーム上での値としてはidを文字列化したものを使う。bindでは文字列→Colorの変換を、unbindではColor→文字列の変換を行う。
このFormatterを使って、例えば文字列とColorの二つのフィールドを持つフォームは次のように書ける。
import models.Costume import models.Color._ import play.api.data.Forms._ import play.api.data.Form import data.format.ColorFormat.colorFormat // colorFormatをimportしておくと ... val sampleForm = Form( mapping( "shape" -> text, "color" -> of[Color] // implicitなFormatter[Color]として利用される )(Costume.apply)(Costume.unapply) )
これで、サーバ側ではColorとして扱い、HTML側ではidの文字列として扱えるようになった。
フォームヘルパーの定義
基本的にはselect.scala.htmlを参考に作りたいけど、HTMLテンプレート上で型パラメータが取れないようなのでScalaで書き下す。
package views.html.helper import play.api.data.format.Formatter import play.api.data.Field import play.api.templates.Html import play.api.templates.HtmlFormat._ import play.api.templates.PlayMagic._ object dataselect { def apply[A](field: Field, options: Seq[A], args: (Symbol,Any)*)(f: A => Html) (implicit handler: FieldConstructor, lang: play.api.i18n.Lang, binder: Formatter[A]): Html = { // 標準のフィールドコンストラクタで装飾 input(field, args:_*) { (id, name, value, htmlArgs) => // 開始タグ Html("""<select id="%s" name="%s" %s>""".format(escape(id), escape(name), toHtmlArgs(htmlArgs)) + // 空値用の選択肢があれば表示 args.toMap.get('_default).map { defaultValue => """<option class="blank" value="">%s</option>""".format(escape(defaultValue.toString())) }.getOrElse("")) + // options分optionタグを繰り返す。中身のレンダリングは引数で受け取った関数でおこなう options.map { (a:A) => binder.unbind("_tmpkey_", a).get("_tmpkey_").map {v => Html("""<option value="%s" %s>""".format(v, if(value == Some(v)) "selected" else "")) + f(a) + Html("""</option>""") }.getOrElse(Html("")) }.reduce(_ + _) + // 閉じタグ Html("""</select>""") } } }
implicitでスコープ上で定義されたFormatterを取得し、選択肢に対応する値を取り出している。*2 *3 また、optionの中身は関数を渡してレンダリングするようにしている。
これを使って、セレクトボックスを含むフォームを書ける。
@(sampleForm:Form[Costume]) @import helper._ @import views.html.helper.dataselect @import data.format.ColorFormat.colorFormat @form(routes.Sample.update()) { @inputText(sampleForm("shape"), '_label -> "衣装の形状") @dataselect(sampleForm("color"), Color.values.toSeq, '_label -> "衣装の色") { color => @color色 } <input type="submit" value="送信" /> }
これで、「黒色」〜「青色」のセレクトボックスが表示され、選択した値がColorとしてフォームから取り出せるようになる。