KHCの

KHC( https://twitter.com/we_can_panic )が140字を超えるツイートをするところ

Karaxプラクティス集

https://github.com/karaxnim/karax

↑のライブラリを触っています。Nimで動的Webページが作成できるらしい。

基本

↓のHTMLでJSにトランスパイルされたNimコードを呼び出し。
Karaxは div[id="ROOT"] の要素を見つけ、そこから下に要素を生成していく。

<html>
  <head>
    <title>Sample page</title>
  </head>
</html>

<body id="body">
  <div id="ROOT">
    <script type="text/javascript" src="index.js"></script>
  </div>
</body>

Nimコードの例。↓をnim js index.nimなどしてindex.jsにトランスパイルし、HTMLに読み込ませる。

import strformat, sugar, colors
import karax / [kdom, vdom, karax, karaxdsl, vstyles]

type
  # Kanbanオブジェクト
  # Nim 2.0.0からtypeの宣言時にデフォルト値を設定できるようになった
  Kanban = ref object
    posx, posy: int
    headtxt = ""
    bodytxt = "write description here"
    headerColor = colAqua
    bodycolor = colAquamarine

# Kanbanの管理
var
  kanbanlist: seq[Kanban] = @[]

# event系の関数。引数はevent, nodeで固定。もしくは何も書かない。
proc addKanban(ev: Event, n: VNode) =
  var
    mev = (MouseEvent)ev
    num = kanbanlist.len+1
    title = case num:
      of 1: "1'st Kanban"
      of 2: "2'nd Kanban"
      of 3: "3'rd Kanban"
      else: fmt"{num}'th Kanban"

  kanbanlist.add(Kanban(
    posx: mev.pageX,
    posY: mev.pageY,
    headtxt: title
  ))

# Kanbanのデータに対応したHTMLコード生成。
proc makeKanban(k: Kanban): VNode =
  let
    kanbanStyle = fmt"position: absolute; top: {k.posy}px; left: {k.posx}px;"
    headerStyle = fmt"background-color: {k.headerColor}"
    bodyStyle = fmt"background-color: {k.bodycolor}; height: 3em"

  buildHtml tdiv(style=kanbanStyle.toCss, onclick = (ev: Event, n: VNode) => ev.stopPropagation):
    tdiv(class="kanban-header", style=headerStyle.toCss):
      input(placeholder=k.headtxt)
    tdiv(class="kanban-body", style=bodyStyle.toCss):
      textarea(placeholder=k.bodytxt, style="width: 100%; height: 100%".toCss)


# 全体の構造。これがアクションのたびに再計算されて動的なページを構成している
proc createDom(): VNode =
  buildHTML tdiv:
    # ヘッダ
    tdiv(class="header"):
      h1: text "KAN-BAN"

    # メイン。kanbanlistのぶんだけmakeKanbanして、HTMLオブジェクトを作成。
    tdiv(class="parette", onclick=addKanban, style="width: 600px; height: 400px; border: 1px solid black".toCss):
      for kanban in kanbanlist:
        tdiv(class="node"):
          kanban.makeKanban()

    button:
      text "refresh"
      proc onclick() =
        kanbanlist = @[]

    # フッタ
    tdiv(class="footer"):
      text "@we_can_panic"

# 任意のイベントが起こった際、createDom関数が仮想DOMをレンダリングし、差分があれば更新
setRenderer createDom

クリックしたところにKanbanが生成される。

チートシート

クリックイベント

2つの方法がある

一つは、関数を定義して任意の要素に渡す方法

# 引数は固定、もしくは何もつけない
proc greet(ev: Event, n: VNode) = echo "Hello world!"

proc main(): VNode =
  buildHtml tdiv:
    # ここで関数を渡す
    input(`type`="button", onclick=greet)


無名関数を渡すこともできる

import sugar # 無名関数の作成に必要

proc main(): VNode =
  buildHtml tdiv:
    # ここで関数を作成して渡す
    input(`type`="button", onclick=(ev: Event, n: VNode) => echo "Hello world!")


もう一つは、proc onclick()を任意の要素内で宣言する方法

proc main(): VNode =
  buildHtml tdiv:
    input(`type`="button"):
      # ここでevent名に合わせた関数を宣言
      # 関数の名前は固定なので、無名関数を渡すことはできない
      proc onclick(ev: Event, n: VNode) =
        echo "Hello world!"


DOM生成の都合上、ループで別々の関数を渡したいときにはコツが必要

var fruits = @["Apple", "Banana", "Pineapple"]

proc main(): VNode =
  buildHtml tdiv:
    for fruit in fruits:
      input(`type`="button"):
        proc onclick(ev: Event, n: VNode) =
          echo "Hello " & fruit & "!"

上記のように書いてしまうと、"Hello Pineapple!"を出力するボタンが3つ生成されるようになってしまう

この場合、inputの生成のたびに関数を生成する必要がある
ただしonclickの関数は引数が固定されているので、onclickの関数を生成する関数を作ってループの回数分実行してやる必要がある

var fruits = @["Apple", "Banana", "Pineapple"]

# onclickの関数を生成する関数
proc generateGreeter(word: string): proc (ev: Event, n: VNode) =
  greet(ev: Event, n: VNode) =
    echo "Hello " & word & "!"

proc main(): VNode =
  buildHtml tdiv:
    for fruit in fruits:
      # ループ分、関数を生成
      input(`type`="button" onclick=generateGreeter(word))

要素を動かす

const
  maxX = 400
  maxY = 600

var
  posX = 200
  posY = 300
  deltaX = 10
  deltaY = 10

# 0.1秒ごとに、posX, posYが更新され、ボールが動く
proc moveBall() =
  posX = posX + deltaX
  if posX > maxX:
    posX = 0
  posY = posY + deltaY
  if posY > maxY:
    posY = 0
  redraw()

discard setInterval(moveBall, 100)

proc main (): VNode =
  buildHtml tdiv:
    # 枠
    tdiv(style=fmt"width: {maxY}; height: {maxX}; border: 1px solid black;".toCss):
      # ボール
      tdiv(style=fmt"position: absolute; top: {posX}px; left: {posY}px; background-color: blue; width: 10px; height: 10px; border-radius: 50%".toCss)

setRenderer main

APIコール

基礎は以下

import std/jsfetch, asyncjs, httpcore
import karax / [kdom, vdom, karax, karaxdsl, vstyles]

proc get(url: string): Future[Response] {.async.} =
  result = await fetch(url.cstring)

proc post(url, body: string): Future[Response] {.async.} =
  let opt = newFetchOptions(
    metod = HttpPost,
    body = body
  )
  result = await fetch(url.cstring, opt)

関数の型が固定されている都合上、async関数はonclick下では使えないので、fetch部分は別の関数に切り分ける

var
  responseStatus: string
  responseBody: string

# fetch部分
proc displayGetResult(url: string) {.async discardable.} =
  let res = await get(url)
  responseStatus = $res.status
  responseBody = block:
    let txt = $(await res.text)
    try:
      parseJson(txt).pretty
    except:
      txt

proc displayPostResult(url, body: string) {.async discardable.} =
  let res = await post(url, body)
  responseStatus = $((await res).status)
  responseBody = block:
    let txt = $(await (await res).text)
    try:
      parseJson(txt).pretty
    except:
      txt

onclick下の処理では関数のキックのみを行う

proc main(): VNode =
  buildHtml tdiv:
    tdiv:
      input(id="url", style="width: 300px".toCss, value="https://sample.com/api/...")
    tdiv:
      textarea(id="body", style="width: 300px; height: 50px".toCss, value="{\n  \"\": \"\"\n}")
    tdiv:
      button:
        text "GET"
        proc onclick() =
          let url = $getElementById("url").value
          # キック
          displayGet(url)

      button:
        text "POST"
        proc onclick() =
          let
            url = $getElementById("url").value
            body = $getElementById("body").value
          displayPost(url, body)

    tdiv:
      text "status: "&responseStatus
      br()
      text "body: "&responseBody

setRenderer main