月曜日, 5月 5, 2025
ホームニューステックニュースMoonbit の宣言的UIライブラリ Rabbit-TEA を試してみる

Moonbit の宣言的UIライブラリ Rabbit-TEA を試してみる



Rabbit-TEA は Moonbit で動く Elm モデルの宣言的 UI ライブラリ。

TEA は The Ele Architecture で、要は React+Redux みたいなやつ。

https://github.com/moonbit-community/rabbit-tea

このテンプレートで vite + Tailwind 上で動かせる。

https://github.com/Yoorkin/rabbit-tea-tailwind

今の Moonbit は自分が集中的に触った 1 年前と比べて非同期や例外処理などと比べて、色々な機能が増えている。それを、このライブラリを使うことで確認していく。

https://zenn.dev/mizchi/articles/moonbit-live-resources

https://zenn.dev/mizchi/scraps/c2c8b63d7a18ad

簡単なカウンターの例

fnalias @html.(div, text, button)
enum Message {
  Increment
}

struct Model {
  count : Int
}

fn update(msg : Message, model : Model) -> (@tea.Command[Message], Model) {
  match msg {
    Increment => (@tea.none(), { count: model.count + 1 })
  }
}

fn view(model : Model) -> @html.Html[Message] {
  div(class="flex", [
    button(class="p-4 bg-pink-200 rounded-lg select-none text-center", click=Increment, [
      text("\{model.count}"),
    ]),
  ])
}

///| NOTE: This program is only available in the js backend,
fn main {
  let model = { count: 0 }
  @tea.startup(model~, update~, view~)
}

Elm パターン, 要は Redux のネタ元なので、 enum で代数的データ型を定義して、 Model を更新する。

model~ はキーワード引数 model=model の省略形。

div や button の VDOM 構築は @html.div() のような関数になっている。Moonbit は Rust 風の構文だが、キーワード引数があるので、props 相当の部分がここで補完可能なインターフェースが実現できている。

余談だが、 Rust Wasm の同等のライブラリの dioxus や yew は、可変長な同等の構文を proc-macro で実装しているが、補完までは実現できておらずあんまり使い勝手がよくない。

button() の型定義を見てみる

ライブラリの実態である .mooncakes/Yoorkin/rabbit-tea/src/html/html.mbt で定義を見る。


pub fn button[M](
  style~ : Array[String] = [],
  id? : String,
  class? : String,
  click? : M,
  childrens : Array[Html[M]]
) -> Html[M] {
  let attrs = []
  if click is Some(click) {
    attrs.push(on_click(fn(_) { click }))
  }
  common_node(style~, class?, id?, "button", attrs, childrens)
}

現状、 HTML の attributes が全部定義できているわけではない。

このままだと disabledtype=button が書けない。

じゃあどうしたらいいか?とドキュメント読んでると、 node 属性を自分で定義すればいいらしい。

## Advanced Usage

The wrapper functions and properties provided here may not cover all possible use cases. If you encounter missing functionality, feel free to file an issue or use the `node()` function as a workaround. The `node()` function allows you to manually specify the tag name, attributes, and children for your HTML element, offering flexibility for advanced or uncommon scenarios.

```mbt
let html = node(
  "div",
  [style("key","value"), property("id","key")],
  [child1, child2],
)
```

これはドキュメント的に少し古くて、 value 部分は @variant.Variant 型を取るらしい。


pub(all) enum Variant {
  Boolean(Bool)
  Integer(Int)
  Floating(Double)
  String(String)
} derive(Show, Eq, Compare, Hash)

disabled は確かにこれでしか表現できないので、納得感ある。

variant はそのまま export されてないので、 src/main/moon.pkg.json を追記

{
  "is-main": true,
  "import": [
    {
      "path": "Yoorkin/rabbit-tea",
      "alias": "tea"
    },
    {
      "path": "Yoorkin/rabbit-tea/variant",
      "alias": "variant"
    },
    "Yoorkin/rabbit-tea/html"
  ]
}

最終的にこうなった。

fn view(model : Model) -> @html.Html[Message] {
  div(class="flex", [
    button(
      class="p-4 bg-pink-200 rounded-lg select-none text-center",
      click=Increment,
      [text("\{model.count}")],
    ),
    @html.node(
      "button",
      [
        @html.property("disabled", @variant.Boolean(true)),
        @html.property("type", @variant.String("button")),
      ],
      [text("Disabled")],
    ),
  ])
}

IDE のコードジャンプで書けるのがすごい。

非同期処理

.mooncakes/Yoorkin/rabbit-tea/src/example/async/main/main.mbt

https://zenn.dev/mizchi/scraps/c2c8b63d7a18ad


fnalias @html.(div, text, button)


pub async fn suspend[T](f : ((T) -> Unit) -> Unit) -> T = "%async.suspend"


extern "js" fn set_timeout(f : () -> Unit, ms : Int) = "(f,ms) => setTimeout(f, ms)"


enum Message {
  Increment
  Display(String)
  LazyLog
}


struct Model {
  count : Int
  message : String
}


fn update(msg : Message, model : Model) -> (@tea.Command[Message], Model) {
  match msg {
    Increment =>
      (@tea.none(), { count: model.count + 1, message: model.message })
    Display(text) => (@tea.none(), { count: model.count, message: text })
    LazyLog => {
      let f = async fn() {
        suspend!(fn(resolve) { set_timeout(fn() { resolve(()) }, 200) })
        "Resolved"
      }
      println("LazyLog")
      (@tea.perform(Message::Display, f), model)
    }
  }
}


fn view(model : Model) -> @html.Html[Message] {
  div([
    button(class="p-4 bg-pink-200", click=Increment, [text("\{model.count}")]),
    @html.hr(),
    @html.div(class="p-4 bg-amber-50", [
      @html.p([text(model.message)]),
      button(class="p-4", click=LazyLog, [text("show text after 5s")]),
    ]),
    @html.hr(),
    @html.node(
      "button",
      [
        @html.property("disabled", @variant.Boolean(true)),
        @html.property("type", @variant.String("button")),
      ],
      [text("Disabled")],
    ),
  ])
}


fn main {
  let model = { count: 0, message: "" }
  @tea.startup(model~, update~, view~)
}

extern "js" fn set_timeout で setTimeount の FFI を定義して、それを suspend!() で呼び出している。

extern "js" fn set_timeout(f : () -> Unit, ms : Int) = "(f,ms) => setTimeout(f, ms)"

    LazyLog => {
      let f = async fn() {
        suspend!(fn(resolve) { set_timeout(fn() { resolve(()) }, 200) })
        "Resolved"
      }
      println("LazyLog")
      (@tea.perform(Message::Display, f), model)
    }

@tea.startup() は何してる?

pub fn startup[Model, Message](
  model~ : Model,
  update~ : (Message, Model) -> (Cmd[Message], Model),
  view~ : (Model) -> @html.Html[Message],
  mount~ : String = "app"
) -> Unit {
  @dom.document()
  .get_element_by_id(mount)
  .get_exn()
  .set_inner_html("")
  let mut sandbox = None
  let mut curr_dom = @vdom.node("div", [], [])
  fn after_update(html : @html.Html[Message]) {
    guard sandbox is Some(sandbox)
    let new_dom = html.to_virtual_dom()
    new_dom.patch(curr_dom, sandbox, mount~)
    curr_dom = new_dom
  }

  sandbox = Some(@browser.Sandbox::new(model, update, view, after_update~))
  sandbox.unwrap().refersh()
}

requestAnimationFrame でメインループを回す

TODO: あとで書く。

途中まで書いたけど、イベントを介さずコマンドを dispatch する方法がわからない。

感想

とにかく IDE 補助が小気味よく、するする書ける。
とはいえライブラリの import はドキュメント足りない。

TS で書きづらい複雑なドメインロジックは、これで書いてみてもいいかも。

フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link

Views: 2

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -

Most Popular