Snippetを作成する
次に、Snippetクラスを作成します。Snippetは通常のScalaのクラスとして、snippetパッケージ内に作成する必要がありますので、プロジェクトディレクトリ以下の 「src/main/scala/demo/twitterclone/snippet/」ディレクトリに、「Twit.scala」というファイル名で新しいファイルを作成します。
// パッケージ宣言 package demo.twitterclone.snippet // 利用するクラスのインポート import _root_.scala.collection.mutable.ListBuffer import _root_.scala.xml.{NodeSeq, Text } import _root_.net.liftweb.http.RequestVar import _root_.net.liftweb.http.SHtml._ import _root_.net.liftweb.util.Helpers._ import _root_.net.liftweb.util.{Box, Full} import _root_.demo.twitterclone.model._ class Twit { object message extends RequestVar(Full("") ) // <lift:Twit.post>タグで呼び出されるSnippet関数 // 入力フォームを生成する def post( xhtml:NodeSeq ):NodeSeq = { *1 // User.currentUserで現在ログインしているユーザーを取得。 // パターンマッチを利用して、ログインしていない場合は"Guest"を設定している var name = User.currentUser match { case Full(user) => user.shortName case _ => "Guest" } // bind関数を利用して、引数のXHTML内で<twit:...>で始まるタグの // 内容を置き換える bind("twit", xhtml, "name" -> name, "status" -> textarea( "", m => message(Full(m)), "cols" -> "40", "rows" -> "3"), "submit" -> submit( "投稿する", () => { Twit.add( message ) }) ) } // <lift:Twit.show>タグで呼び出されるSnippet関数 // 投稿されたメッセージを表示する def show( xhtml:NodeSeq ):NodeSeq = *2 // <xml:Group>で複数のタグをグルーピング <xml:Group>{ Twit.messages.map( m => bind( "twit", xhtml, "message" -> m )) }</xml:Group> } // 投稿されたメッセージを保持しておくためのシングルトンオブジェクト object Twit { *3 // scala.collection.mutable.LiftBufferオブジェクトに // 投稿されたメッセージを保持する val messages:ListBuffer[String] = new ListBuffer // ListBuffer型のフィールドmessagesに、投稿されたメッセージを // 追加する関数 def add(message:Box[String] ) = message match { // パターンマッチでmessageが設定されている場合のみ追加 case Full(m) => messages += m case _=> {} } }
上記のTwitクラスには、2つの関数が定義されています。*1のpost
関数は、リスト4のテンプレートで<lift:Twit.post>
タグから呼び出されるSnippet関数です。このpost
関数では、引数として渡されたXHTMLの中で、<twit:name/>
のようなtwitという名前空間のタグを置き換えて、入力フォームと投稿ボタンを生成しています。
*2のshow
関数では、それまでに投稿されたメッセージをすべて表示するようになっています。
*3は、投稿されたメッセージを保持しておくためのシングルトンオブジェクトを定義しています。
もう少し、それぞれの関数を詳しく解説します。
post関数
以下のリスト6は、post
関数の抜粋です。どのように出力を生成しているかについて解説します。
object message extends RequestVar(Full("") ) *1 // <lift:Twit.post>タグで呼び出されるSnippet関数 // 入力フォームを生成する def post( xhtml:NodeSeq ):NodeSeq = { // User.currentUserで現在ログインしているユーザーを取得。 // パターンマッチを利用して、ログインしていない場合は"Guest"を設定している var name = User.currentUser match { *2 case Full(user) => user.shortName case _ => "Guest" } // bind関数を利用して、引数のXHTML内で<twit:...>で始まるタグの // 内容を置き換える bind("twit", xhtml, *3 "name" -> name, *4 "status" -> textarea( "", m => message(Full(m)), *5 "cols" -> "40", "rows" -> "3"), "submit" -> submit( "投稿する", () => { Twit.add( message ) }) *6 ) }
(1)ブラウザから送信された内容を保持するmessageオブジェクトを定義する
*1で宣言されているのは、RequestVar
を継承したmessage
オブジェクトです。このmessage
オブジェクトが、ブラウザから送信された内容を保持します。
RequestVar
クラスは、Snippet間でもリクエスト情報を共有するためのLiftのクラスです。RequestVar
に設定された内容は、他のSnippetからも利用できます。
なぜこのような形でリクエストされたパラメータを保持しているのかと言うと、*5および*6で設定されている2つの関数オブジェクトの間で、送信されたパラメータを共有するためです。
(2)bind関数による出力の生成
*2は、ログインしているユーザーの名前を設定しています。生成されたプロジェクトには、あらかじめユーザーの登録や認証機能が用意されているので、その機能を利用しています。
*3は、net.liftweb.util.Helpers#bind
関数を利用して、post
関数の引数に渡されたXHTML内で、「twit」という名前空間のタグを置き換えています。
bind関数は、第1引数に置換対象の名前空間(例ではtwit)を指定します。第2引数は置換対象のNodeSeqオブジェクトです。第3引数以降は、タグ名と置き換え内容のペアを指定します。*4のように、"name" -> username
と指定すると、テンプレート内で<twit:name>
となっている部分がusername
変数の内容で置き換わります(例では、ログインしているユーザー名またはGuest)。
(3)SHtmlオブジェクトによるフォーム要素の生成
*5では、「status」タグの内容として、textareaを生成しています。LiftのSnippetでは、net.liftweb.http.SHtml
オブジェクトが持つ各種のヘルパー関数を利用して、form要素を生成することができます。SHtml.textarea
関数の第1引数は初期値として表示される値、第2引数は、(String) => Any
型の関数オブジェクトで、ここで指定された関数オブジェクトはformが送信された際に実行されます。例では、*1で宣言したmessage
オブジェクトに、textareaに入力された文字列を設定します。第3引数以降は、textareaタグに設定する属性を、属性名 -> 値
のペアで指定しています。
*6は、SHtml
オブジェクトのsubmit
関数を利用して、submitボタンを生成しています。submit関数の第1引数はボタンの表示名、第2引数は() => Any
型の関数オブジェクトです。ここで渡した関数オブジェクトが、フォームが送信された際に実行されます。この関数オブジェクトの処理内容は、投稿されたメッセージの内容が*1で定義したmessage
オブジェクトに設定されているので、内容をシングルトンオブジェクトであるTwit
オブジェクトのadd
関数に渡して、メッセージを保存しています。
SHtmlオブジェクト
以下の表は、SHtml
オブジェクトで利用できるヘルパー関数を抜粋したものです。SHtml
オブジェクトのほとんどの関数は、引数にクリックまたはsubmitされた際に実行する関数オブジェクトを設定できます。また、引数に任意の属性と値のペアを渡すことで、生成されるタグにcssクラス名などの属性を追加することが可能です。
SHtml
オブジェクトの詳細は、LiftのAPIドキュメントを参照ください。
関数名 | 説明 |
a |
クリックされた時に、引数に設定したJavaScriptを実行するリンクを生成 |
ajaxButton |
クリックされた時に、引数に設定したJavaScriptを実行するボタンを生成 |
checkbox |
チェックボックスを生成 |
fileUpload |
ファイルアップロード用の<input type="file">タグを生成する。引数に、送信されたファイルを処理する関数オブジェクトを設定する |
hidden |
Hiddenタグを生成 |
link |
引数のXHMTLを含むaタグを生成 |
password |
パスワード入力フィールドを生成 |
radio |
ラジオボタンを生成 |
select |
引数のリストからセレクトフィールドを生成 |
span |
クリックされた時に、引数に設定したJavaScriptを実行するspanタグを生成 |
submit |
submitボタンを生成。引数に、submitされた時に実行する処理を関数オブジェクトで設定する |
text |
テキストフィールドを生成 |
textarea |
テキストエリアを生成 |
show関数
リスト7は、show
関数の内容です。このshow
関数は、それまで投稿されたメッセージを保持しているLiftBuffer
型のTwit.messages
のmap
関数を呼び出して、各メッセージをbind
関数で表示させています。この例のように、Snippet関数の引数のxhtmlを繰り返しにより何度もbind
関数に適用することで、表の生成など要素の繰り返しを簡単に処理できます。
def show( xhtml:NodeSeq ):NodeSeq = // <xml:Group>で複数のタグをグルーピング <xml:Group>{ Twit.messages.map( m => bind( "twit", xhtml, "message" -> m )) *1 }</xml:Group>
show
関数の戻り値が、<xml:Group>
タグで囲まれています。このようにする理由としては、*1で保存されいてるメッセージのそれぞれにbind
関数を適用した結果が、<li>foo</li><li>bar</li>
のような、Rootとなるノードを持たないXMLになってしまうからです。<xml:Group>
タグをRootノードとすることで、XMLとして整形しています。
Snippet関数をよく見ると、処理の中にHTMLタグがまったく記述されていないことが分かると思います。bind関数を利用すると、テンプレートにあるHTMLの構造をそのまま生かして、必要な箇所のみをSnippet関数で処理させることができるのです。LiftのテンプレートとSnippetは、このようなbind関数を利用した仕組みで、デザインと処理の分離を実現しています。