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