reactive-web: Lift をシンプルに

HTML 要素

TextInput

TextInput は、<input type="text"/> HTML 要素を表します。現在、イベント dblClickkeyUp、および change が存在し、プロパティには valuesize があります。

現在は 2 つのファクトリが存在します。1 つは、単純に Var[String] だけを取り、もう 1 つは、String として初期値を 1 つと、String=>Unit としてコールバックを 1 つ、計 2 つのパラメータリストを取ります。

val text = Var("initial")
val field1 = TextInput(text)
val field2 = TextInput(text.now)(s => text ()= s) // 基本的に上と同じ

field2.value.updateOn(field2.keyUp)

field1.value.value foreach {_ => println("field1 changed")}
field2.value.value foreach {_ => println("field2 changed")}

field2.change.eventStream.foreach {_ => println("field2 change event")}

CheckboxInput

CheckboxInput は、<input type="checkbox"/> HTML 要素を表します。現在、イベント dblClickkeyUp、および change が存在し、プロパティには checked があります。

現在は 2 つのファクトリが存在します。1 つは、単純に Var[Boolean] だけを取り、もう 1 つは、Boolean として初期値を 1 つと、Boolean=>Unit としてコールバックを 1 つ、計 2 つのパラメータリストを取ります。

val checked = Var(false)
val check1 = CheckboxInput(checked)
val check2 = CheckboxInput(checked.now)(v => checked ()= v) // 基本的に上と同じ

check1.checked.value foreach {_ => println("check1 changed")}

check2.change.eventStream.foreach {_ => println("check2 change event")}

Button

Button トレイトは、<button> タグを表します。def buttonType をオーバーライドして、ButtonType.ButtonButtonType.Submit、または ButtonType.Reset のいずれかにします (またはそれぞれのファクトリに渡します)。

現在、Button では 1 つのイベント click が定義されています。

/**
 * Button に対し、ボタンがクリックされた回数を保持する
 * Signal を作成します.
 */
def buttonClickCount(button: Button): Signal[Int] =
  button.click.foldLeft(0){case (n, _) => n+1}.hold(0)

Button のインスタンス化

Button には現在 4 つファクトリがあります。4 つのファクトリはすべて、Button with Cell を返します。以下に単純なものから複雑なものまで順に例を示します。

Select

Select は、HTML の <select> 要素のタイプセーフな表現です。現在サポートされているのは、単一の項目の選択だけです。SeqSignal[T] に含まれる項目を表示します。ここで、TSelect への型引数です。型 T=>String のレンダリング関数を指定することも、デフォルトの _.toString を使用することもできます。size の値 (行数での高さ) を指定しなかった場合のデフォルトは 1 で、ドロップダウンになります。

Select は現在、change イベントと、(Option[Int] として表現される) selectedIndex プロパティをサポートしています。また、val selectedItem: Signal[Option[T]] が定義されており、これによって、あらかじめ用意した型付きの項目の中で、選択された項目に (選択された項目があれば) アクセスすることができます。また、def selectItem(item: Option[T]) を呼び出して選択内容を変更することができます。

以下に単純なものから複雑なものまで順にファクトリの例を示します。

Div

Div トレイトは、<div> 要素を表します。現在、dblClickkeyUp の 2 つのイベントをサポートしています。

def logKeyUp(div: Div)(implicit o: Observing) {
  for(KeyUp(code, Modifiers(alt, ctrl, shift, meta)) <- div.keyUp.eventStream)
    println(
      "Key code: %s; Alt: %s; Ctrl: %s; Shift: %s; Meta: %s".format(
        code, alt, ctrl, shift, meta
      )
    )

現在は Repeater をベースとする 2 つのファクトリがあります。「Fundamentals」[リンク切れ]で説明しているように、Repeater は、一連の要素のうち更新の必要がある部分だけを更新する汎用の強力なメカニズムを提供しています。プログラマはグラフィカル表現をバッキングリストにマップします。データが変更されると、表示も変更されます。

最初のファクトリは、実際の Div (実際には Div with Repeater) を返します。ファクトリに SeqSignal[RElem] を渡すと (RElem(elem) のように RElem ファクトリを使えば、任意の ElemRElem にラップできることを思い出してください)、ある時点で Signal に含まれている RElem の集合からコンテンツが構成される Div が返されます。

def peopleDiv(peopleSignal: SeqSignal[Person]) = Div {
  for(people <- peopleSignal) yield
    for(person <- people) yield
      RElem(person.fullName)
}

2 つ目のファクトリは、CSS セレクタバインディングで使いやすいファクトリです。ファクトリには SeqSignal[NodeSeq=>NodeSeq] を渡します。ここで、SeqSignal[NodeSeq=>NodeSeq] の各要素は、テンプレートの該当部分を対象とするバインディング関数です。すると、バインディング関数の対象となるテンプレートの部分が渡される NodeSeq=>NodeSeq が返されます。指定したバインディング関数はこの NodeSeq に適用され、これらの関数が返す NodeSeqDiv 内にレンダリングされます。SeqSignal[NodeSeq=>NodeSeq] は、実際には特別な Signal[Seq[NodeSeq=>NodeSeq]] であることに注意してください。すなわち、全体のシグナルの各値は、多くの要素を持つ Seq です。

テンプレート:

<p>
  Last: <span class="last"/>,
  First: <span class="first"/>
</p>

スニペット:

var peopleSignal = BufferSignal[Person]()
def render = "*" #> Div {
  for(people <- peopleSignal) yield
    for(person <- people) yield
      ".first" #> person.first &
        ".last" #> person.last
}

Span

Span トレイトは、<span> 要素を表します。現在、Span のファクトリは、Cell をベースとするものが 2 つあります。「Fundamentals」[リンク切れ]で説明しているように、Cell は、ある要素のコンテンツを Signal[NodeSeq] によって定義するための汎用のメカニズムを提供しています。

最初のファクトリは、単純に Signal[NodeSeq] を取り、Span with Cell を返します。

val span = Span {
  for(t <- clock) yield
    Text("The time is now " + t)
}

2 つ目のファクトリは、バインディング関数 Signal (Signal[NodeSeq=>NodeSeq]) を取ります。このファクトリは、バインディング関数 (NodeSeq=>NodeSeq) を返します。このバインディング関数は、呼び出されると Span with Cell を返し、Span with Cell のコンテンツは、返された関数への入力を Signal の値に適用することによって定義されます。文章で説明するより、実際に例を見た方がわかりやすいでしょう。

テンプレート:

<div id="div">The time is <span id="span"/></div>

スニペット:

def innerBindFunc(t: Long): NodeSeq=>NodeSeq =
  "#span" #> t.toString
val innerBindSignal = clock map innerBindFunc
val outerBindFunc = "#div *" #> Span(innerBindSignal)

outerBindFunc の実行時、Span のファクトリは、Signal[NodeSeq=>NodeSeq] である innerBindSignal が指定されて呼び出されます。ファクトリはバインディング関数を返します。Lift はこれを div のコンテンツを指定して呼び出します。このとき、バインディング関数は、innerBindSignal.map(bindFunc => bindFunc(divContents)) を呼び出して Signal[NodeSeq] を作成します。別の言い方をすれば、Lift は Span によって返されるバインディング関数に NodeSeq を渡し、この NodeSeq に対してプログラマが (Signal の中で) 指定したバインディング関数を適用した結果が Span にラップされたものが得られます。