メインコンテンツまでスキップ

Laminarでテキスト入力処理

· 約4分

投稿の内容

本投稿では、Scala.jsのUIフレームワークであるLaminarを使ってテキスト入力処理について調べてみました。

Laminarのイベント処理はObserverObservableと使って処理をしますが、複数のイベントを組み合わせたい場合についてご紹介します。

実現した機能としては、テキストフィールドに入力した文字が隣にあるボタンを押すと、その下に表示されるという簡単なものです。

2種類のイベント

これを実現するためには、以下の2種類のイベントを処理する必要があります。

  • inputイベント
  • clickイベント

inputイベント

inputイベントは、テキストフィールドに文字が入力される度に発火されるイベントです。このイベントをObserverに入力します。

onInput.map(e =>
Command.Input(
e.target.asInstanceOf[HTMLInputElement].value
)
) --> event.writer

ここで、上記のコードに登場するCommandeventは以下のように定義しました。

enum Command:
case Input(value: String)
case Click
val event = EventBus[Command]()

ちなみに、このイベントを出力先に入れると、文字を入力する度に、出力先が更新されます。

div(child.text <-- event.events)

今回はボタンを押せば出力先が更新されるようにしたいので、これでは目的は達成されません。

clickイベント

clickイベントは、ボタンをクリックした時に発火されるイベントです。今回はこのタイミングで出力先に入力内容を表示します。

なのでclickイベントが発火した時のテキストフィールドの入力内容が必要です。そこで入力内容をどこかに保存しておき、clickイベントが発火時に、保存していた入力内容を出力先に渡すようにしたいと思います。

まずは、clickイベントをObserverに渡します。

onClick.mapTo(Command.Click) --> event.writer

次にfoldLeftを使用して入力内容を保存すうりょうにします。foldLeftの初期値は空文字列のタプルです。タプルの第一要素は、入力内容の現在値を保存しておく用で、第二要素はclick時の入力内容を表しています。イベントがCommand.Inputの場合は、その値をタプルの第一要素に入れます。イベントがCommand.Clickの場合は、最新の入力値(=タプルの第一要素)を第二要素に入れます。こうすることで、タプルの第二要素が常にclick時の入力内容になります。

val signal = event.events
.foldLeft(("", "")) {
case ((a, b), Command.Click) => (a, a)
case ((a, b), Command.Input(text)) => (text, b)
}
.map(_._2)

最後にclick時の入力内容を出力先のdivに入れます。

div(child.text <-- signal)

全コード

import org.scalajs.dom
import com.raquo.laminar.api.L.{*, given}
import org.scalajs.dom.HTMLInputElement

enum Command:
case Input(value: String)
case Click

@main def main =
documentEvents.onDomContentLoaded.foreach { _ =>
val appContainer: dom.Element = dom.document.body

val event = EventBus[Command]()

val signal = event.events
.foldLeft(("", "")) {
case ((a, b), Command.Click) => (a, a)
case ((a, b), Command.Input(text)) => (text, b)
}
.map(_._2)

val appElement: Div = div(
display := "flex",
justifyContent := "center",
fontSize := "1.2rem",
fontWeight := "bold",
div(
div(
padding := "8px",
div("入力"),
input(
typ := "text",
onInput.map(e =>
Command.Input(
e.target.asInstanceOf[HTMLInputElement].value
)
) --> event.writer
),
button("OK", onClick.mapTo(Command.Click) --> event.writer)
),
div(
padding := "8px",
div("出力"),
div(child.text <-- signal)
)
)
)
render(appContainer, appElement)
}(unsafeWindowOwner)