Compare commits
31 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85c81d8c0e |
||
|
|
6d51d7d08c |
||
|
|
93d9b73867 |
||
|
|
ac210fb634 |
||
|
|
df828e145d |
||
|
|
cedd39a65f |
||
|
|
12b98b2338 |
||
|
|
8a7f05ac2a |
||
|
|
0fca6e961d |
||
|
|
32de202845 |
||
|
|
6cff2d8b05 |
||
|
|
4c06cb3204 | ||
|
|
9a627985d0 | ||
|
|
24a037ad19 |
||
|
|
40daae64e3 |
||
|
|
d3fc084c42 |
||
|
|
bcab551f05 |
||
|
|
04779f8502 |
||
|
|
bd18fc980c |
||
|
|
5a2ea1eef9 | ||
|
|
d2f88b4648 |
||
|
|
d8fe79fa40 |
||
|
|
f0dd16ebfe |
||
|
|
0813a7adf9 |
||
|
|
7a36b19729 |
||
|
|
98a2d1ce50 |
||
|
|
22e2e226cb |
||
|
|
f1cd9e606d |
||
|
|
80f500874b |
||
|
|
5f21dcdc56 | ||
|
|
02038d40c1 |
22 changed files with 784 additions and 475 deletions
31
.github/workflows/ci.yml
vendored
Normal file
31
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: CI
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the master branch
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
container: nimlang/nim
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
# Runs a single command using the runners shell
|
||||
- name: Tests
|
||||
run: |
|
||||
nimble develop -y
|
||||
nim c -r tests/tester.nim
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -3,3 +3,5 @@ nimcache/
|
|||
karax/tools/karun
|
||||
*.code-workspace
|
||||
*.exe
|
||||
app.js
|
||||
app.html
|
||||
|
|
|
|||
20
.travis.yml
20
.travis.yml
|
|
@ -1,20 +0,0 @@
|
|||
sudo: false
|
||||
language: c
|
||||
os: linux
|
||||
install:
|
||||
- git clone https://github.com/nim-lang/nim
|
||||
- cd nim
|
||||
- git clone --depth 1 https://github.com/nim-lang/csources.git
|
||||
- cd csources
|
||||
- sh build.sh
|
||||
- cd ../
|
||||
- bin/nim c koch
|
||||
- ./koch boot -d:release
|
||||
- ./koch tools
|
||||
- cd ..
|
||||
before_script:
|
||||
- set -e
|
||||
- export PATH=$(pwd)/nim/bin:$(pwd):$PATH
|
||||
script:
|
||||
- nimble develop
|
||||
- nim c -r tests/tester.nim
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
include karax / prelude
|
||||
|
||||
var lines: seq[kstring] = @[]
|
||||
|
|
|
|||
17
examples/hellostyle.nim
Normal file
17
examples/hellostyle.nim
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
include karax / prelude
|
||||
import karax / vstyles
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
tdiv(style = style(StyleAttr.color, "red".cstring)):
|
||||
text "red"
|
||||
tdiv(style = style(StyleAttr.color, "blue")):
|
||||
text "blue"
|
||||
# explicit `kstring` required for varargs overload
|
||||
tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))):
|
||||
text "italic orange"
|
||||
# can use a string directly
|
||||
tdiv(style = "font-style: oblique; color: pink".toCss):
|
||||
text "oblique pink"
|
||||
|
||||
setRenderer createDom
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
|
||||
include karax / prelude
|
||||
import random
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
if random(100) <= 50:
|
||||
if rand(100) <= 50:
|
||||
text "Hello World!"
|
||||
else:
|
||||
text "Hello Universe"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
include karax / prelude
|
||||
from sugar import `=>`
|
||||
import karax / errors
|
||||
|
|
@ -18,7 +17,7 @@ const
|
|||
proc validateNotEmpty(field: kstring): proc () =
|
||||
result = proc () =
|
||||
let x = getVNodeById(field)
|
||||
if x.text.isNil or x.text == "":
|
||||
if x.text == "":
|
||||
errors.setError(field, field & " must not be empty")
|
||||
else:
|
||||
errors.setError(field, "")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
include login
|
||||
|
||||
const
|
||||
|
|
@ -12,7 +11,7 @@ var allMessages: seq[TextMessage] = @[]
|
|||
|
||||
proc doSendMessage() =
|
||||
let inputField = getVNodeById(message)
|
||||
allMessages.add(TextMessage(name: "you", content: inputField.text))
|
||||
allMessages.add(TextMessage(name: "you", content: inputField.getInputText))
|
||||
inputField.setInputText ""
|
||||
|
||||
proc main(): VNode =
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
# Package
|
||||
|
||||
version = "1.1.1"
|
||||
version = "1.2.1"
|
||||
author = "Andreas Rumpf"
|
||||
description = "Karax is a framework for developing single page applications in Nim."
|
||||
license = "MIT"
|
||||
|
||||
# Dependencies
|
||||
|
||||
requires "nim >= 0.16.1"
|
||||
|
||||
requires "nim >= 0.18.0"
|
||||
requires "ws"
|
||||
requires "dotenv"
|
||||
skipDirs = @["examples", "experiments", "tests"]
|
||||
|
||||
bin = @["karax/tools/karun"]
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ proc newname*(n: NimNode): NimNode =
|
|||
n[1] = newname(n[1])
|
||||
result = n
|
||||
elif n.kind == nnkSym:
|
||||
result = ident($n.symbol)
|
||||
result = ident(n.strVal)
|
||||
else:
|
||||
result = n
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ export kdom.Event, kdom.Blob
|
|||
when defined(nimNoNil):
|
||||
{.experimental: "notnil".}
|
||||
|
||||
proc kout*[T](x: T) {.importc: "console.log", varargs, deprecated.}
|
||||
## the preferred way of debugging karax applications. Now deprecated,
|
||||
## you can now use ``system.echo`` instead.
|
||||
proc kout*[T](x: T) {.importc: "console.log", varargs.}
|
||||
## The preferred way of debugging karax applications.
|
||||
|
||||
type
|
||||
PatchKind = enum
|
||||
|
|
@ -38,6 +37,7 @@ type
|
|||
toFocus: Node
|
||||
toFocusV: VNode
|
||||
renderId: int
|
||||
rendering: bool
|
||||
patches: seq[Patch] # we reuse this to save allocations
|
||||
patchLen: int
|
||||
patchesV: seq[PatchV]
|
||||
|
|
@ -154,7 +154,9 @@ proc getVNodeById*(id: cstring; kxi: KaraxInstance = kxi): VNode =
|
|||
|
||||
proc toDom*(n: VNode; useAttachedNode: bool; kxi: KaraxInstance = nil): Node =
|
||||
if useAttachedNode:
|
||||
if n.dom != nil: return n.dom
|
||||
if n.dom != nil:
|
||||
if n.id != nil: kxi.byId[n.id] = n
|
||||
return n.dom
|
||||
if n.kind == VNodeKind.text:
|
||||
result = document.createTextNode(n.text)
|
||||
attach n
|
||||
|
|
@ -383,6 +385,8 @@ proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
|
|||
proc moveDom(dest, src: VNode) =
|
||||
dest.dom = src.dom
|
||||
src.dom = nil
|
||||
if dest.id != nil:
|
||||
kxi.byId[dest.id] = dest
|
||||
assert dest.len == src.len
|
||||
for i in 0..<dest.len:
|
||||
moveDom(dest[i], src[i])
|
||||
|
|
@ -626,8 +630,21 @@ proc avoidDomDiffing*(kxi: KaraxInstance = kxi) =
|
|||
## This is an experimental API.
|
||||
kxi.currentTree = nil
|
||||
|
||||
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
|
||||
when false:
|
||||
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
|
||||
|
||||
proc dodraw(kxi: KaraxInstance) =
|
||||
if kxi.renderer.isNil: return
|
||||
kxi.renderId = 0
|
||||
|
||||
if kxi.rendering:
|
||||
# there is a render already running, delay 1 frame
|
||||
kxi.renderId = reqFrame(proc () = kxi.dodraw)
|
||||
return
|
||||
|
||||
kxi.rendering = true
|
||||
|
||||
let rdata = RouterData(hashPart: hashPart)
|
||||
let newtree = kxi.renderer(rdata)
|
||||
inc kxi.runCount
|
||||
|
|
@ -658,16 +675,12 @@ proc dodraw(kxi: KaraxInstance) =
|
|||
# now that it's part of the DOM, give it the focus:
|
||||
if kxi.toFocus != nil:
|
||||
kxi.toFocus.focus()
|
||||
kxi.renderId = 0
|
||||
kxi.rendering = false
|
||||
when defined(stats):
|
||||
kxi.recursion = 0
|
||||
var total = 0
|
||||
echo "depth ", depth(kxi.currentTree, total), " total ", total
|
||||
|
||||
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
|
||||
when false:
|
||||
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
|
||||
|
||||
proc redraw*(kxi: KaraxInstance = kxi) =
|
||||
# we buffer redraw requests:
|
||||
when false:
|
||||
|
|
@ -787,11 +800,10 @@ proc prepend(parent, kid: Element) =
|
|||
parent.insertBefore(kid, parent.firstChild)
|
||||
|
||||
proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) =
|
||||
let body = document.getElementById("body")
|
||||
let s = document.createElement("script")
|
||||
s.setAttr "type", "text/javascript"
|
||||
s.setAttr "src", jsfilename
|
||||
body.prepend(s)
|
||||
document.body.prepend(s)
|
||||
redraw(kxi)
|
||||
|
||||
proc runLater*(action: proc(); later = 400): Timeout {.discardable.} =
|
||||
|
|
@ -805,11 +817,18 @@ proc setInputText*(n: VNode; s: cstring) =
|
|||
n.text = s
|
||||
if n.dom != nil: n.dom.value = s
|
||||
|
||||
proc getInputText*(n: VNode): cstring =
|
||||
if n.dom != nil:
|
||||
result = n.dom.value
|
||||
|
||||
proc toChecked*(checked: bool): cstring =
|
||||
(if checked: cstring"checked" else: cstring(nil))
|
||||
|
||||
proc toDisabled*(disabled: bool): cstring =
|
||||
(if disabled: cstring"disabled" else: cstring(nil))
|
||||
|
||||
proc toSelected*(selected: bool): cstring =
|
||||
(if selected: cstring"selected" else: cstring(nil))
|
||||
|
||||
proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
|
||||
result = toDom(n, useAttachedNode = false, kxi)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,16 @@
|
|||
## Simple tool to quickly run Karax applications. Generates the HTML
|
||||
## required to run a Karax app and opens it in a browser.
|
||||
|
||||
import os,
|
||||
strutils,
|
||||
parseopt,
|
||||
browsers,
|
||||
times,
|
||||
tables
|
||||
|
||||
|
||||
import os, strutils, browsers,times, tables
|
||||
import parseopt
|
||||
import threadpool
|
||||
import static_server
|
||||
|
||||
const
|
||||
css = """
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
"""
|
||||
html = """
|
||||
|
||||
const html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
|
@ -23,33 +18,81 @@ const
|
|||
<title>$1</title>
|
||||
$2
|
||||
</head>
|
||||
<body id="body">
|
||||
<div id="ROOT" />
|
||||
<script type="text/javascript" src="$1.js"></script>
|
||||
<body id="body" class="site">
|
||||
<div id="ROOT"></div>
|
||||
<script type="text/javascript" src="/app.js"></script>
|
||||
$3
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
const websocket = """
|
||||
<script type="text/javascript">
|
||||
var ws = new WebSocket("ws://localhost:8080/ws");
|
||||
|
||||
ws.onopen = function(evt) {
|
||||
console.log("Connection open ...");
|
||||
ws.send("Hello WebSockets!");
|
||||
};
|
||||
|
||||
ws.onmessage = function(evt) {
|
||||
console.log( "Received Message: " + evt.data);
|
||||
if(evt.data == "refresh"){
|
||||
window.location.href = window.location.href
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function(evt) {
|
||||
console.log("Connection closed.");
|
||||
};
|
||||
</script>
|
||||
"""
|
||||
|
||||
proc exec(cmd: string) =
|
||||
if os.execShellCmd(cmd) != 0:
|
||||
quit "External command failed: " & cmd
|
||||
|
||||
proc build(name: string, rest: string, selectedCss: string, run: bool) =
|
||||
proc build(rest: string, selectedCss: string, run: bool, watch: bool) =
|
||||
echo("Building...")
|
||||
exec("nim js --out:" & name & ".js " & rest)
|
||||
let dest = name & ".html"
|
||||
writeFile(dest, html % [name, selectedCss])
|
||||
if run: openDefaultBrowser(dest)
|
||||
let cmd = "nim js --out:" & "app" & ".js " & rest
|
||||
if watch:
|
||||
discard os.execShellCmd(cmd)
|
||||
else:
|
||||
exec cmd
|
||||
let dest = "app" & ".html"
|
||||
let script = if watch: websocket else: ""
|
||||
writeFile(dest, html % ["app", selectedCss, script])
|
||||
if run: openDefaultBrowser("http://localhost:8080")
|
||||
|
||||
proc watchBuild(filePath: string, selectedCss: string, rest: string) {.thread.} =
|
||||
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
|
||||
while true:
|
||||
sleep(300)
|
||||
for path in walkDirRec("."):
|
||||
if ".git" in path:
|
||||
continue
|
||||
var (_, _, ext) = splitFile(path)
|
||||
if ext in [".scss",".sass",".less",".styl",".pcss",".postcss"]:
|
||||
continue
|
||||
if files.hasKey(path):
|
||||
if files[path] != getLastModificationTime(path):
|
||||
echo("File changed: " & path)
|
||||
build(rest,selectedCss, false, true)
|
||||
files[path] = getLastModificationTime(path)
|
||||
else:
|
||||
if absolutePath(path) in [absolutePath("app" & ".js"),absolutePath("app" & ".html")]:
|
||||
continue
|
||||
files[path] = getLastModificationTime(path)
|
||||
|
||||
proc serve(){.thread.} =
|
||||
serveStatic()
|
||||
|
||||
proc main =
|
||||
var op = initOptParser()
|
||||
var rest = op.cmdLineRest
|
||||
var file = ""
|
||||
var run = false
|
||||
var selectedCss = ""
|
||||
var watch = false
|
||||
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
|
||||
|
||||
var selectedCss = ""
|
||||
while true:
|
||||
op.next()
|
||||
case op.kind
|
||||
|
|
@ -59,8 +102,11 @@ proc main =
|
|||
run = true
|
||||
rest = rest.replace("--run ")
|
||||
of "css":
|
||||
if op.val != "":
|
||||
selectedCss = readFile(op.val)
|
||||
else:
|
||||
selectedCss = css
|
||||
rest = rest.replace("--css ")
|
||||
rest = rest.substr(rest.find(" "))
|
||||
else: discard
|
||||
of cmdShortOption:
|
||||
if op.key == "r":
|
||||
|
|
@ -73,25 +119,11 @@ proc main =
|
|||
of cmdEnd: break
|
||||
|
||||
if file.len == 0: quit "filename expected"
|
||||
let name = file.splitFile.name
|
||||
build(name, rest, selectedCss, run)
|
||||
if run:
|
||||
spawn serve()
|
||||
if watch:
|
||||
# TODO: launch http server
|
||||
while true:
|
||||
sleep(300)
|
||||
for path in walkDirRec("."):
|
||||
if ".git" in path:
|
||||
continue
|
||||
if files.hasKey(path):
|
||||
if files[path] != getLastModificationTime(path):
|
||||
echo("File changed: " & path)
|
||||
build(name, rest, selectedCss, run)
|
||||
files[path] = getLastModificationTime(path)
|
||||
else:
|
||||
files[path] = getLastModificationTime(path)
|
||||
spawn watchBuild(file, selectedCss, rest)
|
||||
build(rest,selectedCss, run, watch)
|
||||
sync()
|
||||
|
||||
main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
1
karax/tools/karun.nims
Normal file
1
karax/tools/karun.nims
Normal file
|
|
@ -0,0 +1 @@
|
|||
switch("threads", "on")
|
||||
141
karax/tools/static_server.nim
Normal file
141
karax/tools/static_server.nim
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import
|
||||
std/[net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5,
|
||||
logging, httpcore, asyncfile, asynchttpserver, tables, times]
|
||||
from cgi import decodeUrl
|
||||
import ws, dotenv
|
||||
|
||||
var logger = newConsoleLogger()
|
||||
addHandler(logger)
|
||||
|
||||
when defined(release):
|
||||
setLogFilter(lvlError)
|
||||
|
||||
type
|
||||
RawHeaders* = seq[tuple[key, val: string]]
|
||||
|
||||
proc toStr(headers: RawHeaders): string =
|
||||
$newHttpHeaders(headers)
|
||||
|
||||
proc send(request: Request, code: HttpCode, headers: RawHeaders,
|
||||
body: string): Future[void] =
|
||||
return request.respond(code, body, newHttpHeaders(headers))
|
||||
|
||||
proc statusContent(request: Request, status: HttpCode, content: string,
|
||||
headers: RawHeaders): Future[void] =
|
||||
try:
|
||||
result = send(request, status, headers, content)
|
||||
debug(" ", status, " ", toStr(headers))
|
||||
except:
|
||||
error("Could not send response: ", osErrorMsg(osLastError()))
|
||||
|
||||
proc sendStaticIfExists(req: Request, paths: seq[string]): Future[HttpCode] {.async.} =
|
||||
result = Http200
|
||||
let mimes = newMimetypes()
|
||||
for p in paths:
|
||||
if fileExists(p):
|
||||
if fpOthersRead notin getFilePermissions(p):
|
||||
return Http403
|
||||
let fileSize = getFileSize(p)
|
||||
let extPos = searchExtPos(p)
|
||||
let mimetype = mimes.getMimetype(
|
||||
if extPos >= 0: p.substr(extPos + 1)
|
||||
else: "")
|
||||
if fileSize < 10_000_000: # 10 mb
|
||||
var file = readFile(p)
|
||||
var hashed = getMD5(file)
|
||||
# If the user has a cached version of this file and it matches our
|
||||
# version, let them use it
|
||||
if req.headers.getOrDefault("If-None-Match") == hashed:
|
||||
await req.statusContent(Http304, "", default(RawHeaders))
|
||||
else:
|
||||
await req.statusContent(Http200, file, @{
|
||||
"Content-Type": mimetype,
|
||||
"ETag": hashed
|
||||
})
|
||||
else:
|
||||
let headers = @{
|
||||
"Content-Type": mimetype,
|
||||
"Content-Length": $fileSize
|
||||
}
|
||||
await req.statusContent(Http200, "", headers)
|
||||
var fileStream = newFutureStream[string]("sendStaticIfExists")
|
||||
var file = openAsync(p, fmRead)
|
||||
# Let `readToStream` write file data into fileStream in the
|
||||
# background.
|
||||
asyncCheck file.readToStream(fileStream)
|
||||
# The `writeFromStream` proc will complete once all the data in the
|
||||
# `bodyStream` has been written to the file.
|
||||
while true:
|
||||
let (hasValue, value) = await fileStream.read()
|
||||
if hasValue:
|
||||
await req.client.send(value)
|
||||
else:
|
||||
break
|
||||
file.close()
|
||||
return
|
||||
# If we get to here then no match could be found.
|
||||
return Http404
|
||||
|
||||
proc handleFileRequest(req: Request): Future[HttpCode] {.async.} =
|
||||
# Find static file.
|
||||
var reqPath = cgi.decodeUrl(req.url.path)
|
||||
var staticDir = getEnv("staticDir") # it's assumed a relative dir
|
||||
var status = Http400
|
||||
var path = staticDir / reqPath
|
||||
normalizePathEnd(path, false)
|
||||
if dirExists(path):
|
||||
status = await sendStaticIfExists(req, @[path / "index.html", path / "index.htm"])
|
||||
else:
|
||||
status = await sendStaticIfExists(req, @[path])
|
||||
return status
|
||||
|
||||
proc handleWs(req: Request) {.async.} =
|
||||
var ws = await newWebSocket(req)
|
||||
await ws.send("Welcome to simple echo server")
|
||||
|
||||
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
|
||||
let watchedFiles = [absolutePath "app.js", absolutePath "app.html"]
|
||||
for path in watchedFiles:
|
||||
files[path] = getLastModificationTime(path)
|
||||
|
||||
while ws.readyState == Open:
|
||||
await sleepAsync(500)
|
||||
var changed = false
|
||||
for path in watchedFiles:
|
||||
if files[path] != getLastModificationTime(path):
|
||||
changed = true
|
||||
files[path] = getLastModificationTime(path)
|
||||
if changed:
|
||||
await ws.send("refresh")
|
||||
changed = false
|
||||
|
||||
proc serveStatic*() =
|
||||
if fileExists("static.env"):
|
||||
var env: DotEnv
|
||||
env = initDotEnv(getCurrentDir(), "static.env")
|
||||
env.overload()
|
||||
else:
|
||||
putEnv("staticDir", "assets/")
|
||||
|
||||
var server = newAsyncHttpServer()
|
||||
proc cb(req: Request) {.gcsafe, async.} =
|
||||
if req.url.path == "/ws":
|
||||
await handleWs(req)
|
||||
if req.url.path == "/":
|
||||
await req.respond(Http200, readFile "app.html")
|
||||
elif req.url.path == "/app.js":
|
||||
let file = absolutePath("app" & ".js")
|
||||
if not file.fileExists:
|
||||
error(file, " does not exist!")
|
||||
if fpUserRead notin os.getFilePermissions(file):
|
||||
error("Could not read ", file, "!")
|
||||
await req.respond(Http200, readFile(file))
|
||||
else:
|
||||
let status = await handleFileRequest(req)
|
||||
if status != Http200:
|
||||
await req.respond(status, "")
|
||||
|
||||
waitFor server.serve(Port(8080), cb)
|
||||
|
||||
when isMainModule:
|
||||
serveStatic()
|
||||
|
|
@ -35,7 +35,19 @@ type
|
|||
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
|
||||
ins, del, img, iframe, embed, `object` = "object",
|
||||
param, video, audio, source, track, canvas, map,
|
||||
area, svg, math, path, circle
|
||||
area, math,
|
||||
|
||||
# SVG elements, see: https://www.w3.org/TR/SVG2/eltindex.html
|
||||
animate, animateMotion, animateTransform, circle, clipPath, defs, desc,
|
||||
`discard` = "discard", ellipse, feBlend, feColorMatrix, feComponentTransfer,
|
||||
feComposite, feConvolveMatrix, feDiffuseLighting, feDisplacementMap,
|
||||
feDistantLight, feDropShadow, feFlood, feFuncA, feFuncB, feFuncG, feFuncR,
|
||||
feGaussianBlur, feImage, feMerge, feMergeNode, feMorphology, feOffset,
|
||||
fePointLight, feSpecularLighting, feSpotLight, feTile, feTurbulence,
|
||||
filter, foreignObject, g, image, line, linearGradient, marker, mask,
|
||||
metadata, mpath, path, pattern, polygon, polyline, radialGradient, rect,
|
||||
`set` = "set", stop, svg, switch, symbol, txt = "text", textPath, tspan,
|
||||
unknown, use, view,
|
||||
|
||||
table, caption, colgroup, col, tbody, thead,
|
||||
tfoot, tr, td, th,
|
||||
|
|
@ -97,6 +109,13 @@ type
|
|||
## passed (useful for on-the-fly text completions)
|
||||
onload, # img
|
||||
|
||||
ontransitioncancel,
|
||||
ontransitionend,
|
||||
ontransitionrun,
|
||||
ontransitionstart,
|
||||
|
||||
onwheel # fires when the user rotates a wheel button on a pointing device.
|
||||
|
||||
macro buildLookupTables(): untyped =
|
||||
var a = newTree(nnkBracket)
|
||||
for i in low(VNodeKind)..high(VNodeKind):
|
||||
|
|
@ -298,6 +317,12 @@ proc sameAttrs*(a, b: VNode): bool =
|
|||
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
|
||||
n.events.add((event, handler, nil))
|
||||
|
||||
when kstring is cstring:
|
||||
proc len(a: kstring): int =
|
||||
# xxx: maybe move where kstring is defined
|
||||
# without this, `n.field.len` fails on js (non web) platform
|
||||
if a == nil: 0 else: a.len
|
||||
|
||||
template toStringAttr(field) =
|
||||
if n.field.len > 0:
|
||||
result.add " " & astToStr(field) & " = " & $n.field
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
##[
|
||||
see examples/hellostyle.nim
|
||||
]##
|
||||
|
||||
import macros, kbase
|
||||
import std/[macros, strutils]
|
||||
import kbase
|
||||
|
||||
when defined(js):
|
||||
import kdom, jdict
|
||||
|
|
@ -234,7 +238,10 @@ proc eq*(a, b: VStyle): bool =
|
|||
if a[i] != b[i]: return false
|
||||
return true
|
||||
|
||||
proc setAttr(s: VStyle; a, value: kstring) {.noSideEffect.} =
|
||||
proc setAttr*(s: VStyle; a, value: kstring) {.noSideEffect.} =
|
||||
## inserts (a, value) in sorted order of key `a`
|
||||
# worst case quadratic complexity (if given styles in reverse order), hopefully
|
||||
# not a concern assuming small cardinal
|
||||
var i = 0
|
||||
while i < s.len:
|
||||
if s[i] == a:
|
||||
|
|
@ -243,8 +250,8 @@ proc setAttr(s: VStyle; a, value: kstring) {.noSideEffect.} =
|
|||
elif s[i] > a:
|
||||
s.add ""
|
||||
s.add ""
|
||||
# insertion point here:
|
||||
for j in countdown(s.len-1, i, 2):
|
||||
# insertion point here, shift all remaining pairs by 2 indexes
|
||||
for j in countdown(s.len-1, i+3, 2):
|
||||
s[j] = s[j-2]
|
||||
s[j-1] = s[j-3]
|
||||
s[i] = a
|
||||
|
|
@ -289,6 +296,24 @@ proc style*(a: StyleAttr; val: kstring): VStyle {.noSideEffect.} =
|
|||
result[] = @[]
|
||||
result.setAttr a, val
|
||||
|
||||
proc toCss*(a: string): VStyle =
|
||||
##[
|
||||
See example in hellostyle.nim
|
||||
Allows passing a css string directly, eg:
|
||||
tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): discard
|
||||
tdiv(style = "font-style: oblique; color: pink".toCss): discard
|
||||
]##
|
||||
when defined(js):
|
||||
result = newJSeq[cstring]()
|
||||
else:
|
||||
new(result)
|
||||
result[] = @[]
|
||||
for ai in a.split(";"):
|
||||
var ai = ai.strip
|
||||
if ai.len == 0: continue
|
||||
let aj = ai.strip.split(":", maxsplit=1)
|
||||
result.setAttr(aj[0], aj[1])
|
||||
|
||||
when defined(js):
|
||||
proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
|
||||
|
||||
|
|
|
|||
394
readme.md
Normal file
394
readme.md
Normal file
|
|
@ -0,0 +1,394 @@
|
|||

|
||||
|
||||
    
|
||||
|
||||
# Karax
|
||||
Karax is a framework for developing single page applications in Nim.
|
||||
|
||||
## Install
|
||||
|
||||
To use Karax you must have nim installed. You can follow the instructions [here](https://nim-lang.org/install.html).
|
||||
|
||||
Then you can install karax through nimble:
|
||||
``nimble install karax``
|
||||
|
||||
## Try Karax
|
||||
To try it out, run:
|
||||
|
||||
``cd ~/projects # Insert your favourite directory for projects``
|
||||
|
||||
``nimble develop karax # This will clone Karax and create a link to it in ~/.nimble``
|
||||
|
||||
``cd karax``
|
||||
|
||||
``cd examples/todoapp``
|
||||
|
||||
``nim js todoapp.nim``
|
||||
|
||||
``open todoapp.html``
|
||||
|
||||
``cd ../..``
|
||||
|
||||
``cd examples/mediaplayer``
|
||||
|
||||
``nim js playerapp.nim``
|
||||
|
||||
``open playerapp.html``
|
||||
|
||||
It uses a virtual DOM like React, but is much smaller than the existing
|
||||
frameworks plus of course it's written in Nim for Nim. No external
|
||||
dependencies! And thanks to Nim's whole program optimization only what
|
||||
is used ends up in the generated JavaScript code.
|
||||
|
||||
|
||||
## Goals
|
||||
|
||||
|
||||
- Leverage Nim's macro system to produce a framework that allows
|
||||
for the development of applications that are boilerplate free.
|
||||
- Keep it small, keep it fast, keep it flexible.
|
||||
|
||||
|
||||
|
||||
## Hello World
|
||||
|
||||
|
||||
The simplest Karax program looks like this:
|
||||
|
||||
```nim
|
||||
|
||||
include karax / prelude
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
text "Hello World!"
|
||||
|
||||
setRenderer createDom
|
||||
```
|
||||
|
||||
Since ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead
|
||||
here. ``tdiv`` produces a ``<div>`` virtual DOM node.
|
||||
|
||||
As you can see, karax comes with its own ``buildHtml`` DSL for convenient
|
||||
construction of (virtual) DOM trees (of type ``VNode``). Karax provides
|
||||
a tiny build tool called ``karun`` that generates the HTML boilerplate code that
|
||||
embeds and invokes the generated JavaScript code::
|
||||
|
||||
``nim c karax/tools/karun``
|
||||
``karax/tools/karun -r helloworld.nim``
|
||||
|
||||
Via ``-d:debugKaraxDsl`` we can have a look at the produced Nim code by
|
||||
``buildHtml``:
|
||||
|
||||
```nim
|
||||
|
||||
let tmp1 = tree(VNodeKind.tdiv)
|
||||
add(tmp1, text "Hello World!")
|
||||
tmp1
|
||||
```
|
||||
(I shortened the IDs for better readability.)
|
||||
|
||||
Ok, so ``buildHtml`` introduces temporaries and calls ``add`` for the tree
|
||||
construction so that it composes with all of Nim's control flow constructs:
|
||||
|
||||
|
||||
```nim
|
||||
|
||||
include karax / prelude
|
||||
import random
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
if rand(100) <= 50:
|
||||
text "Hello World!"
|
||||
else:
|
||||
text "Hello Universe"
|
||||
|
||||
randomize()
|
||||
setRenderer createDom
|
||||
|
||||
```
|
||||
Produces:
|
||||
|
||||
```nim
|
||||
|
||||
let tmp1 = tree(VNodeKind.tdiv)
|
||||
if rand(100) <= 50:
|
||||
add(tmp1, text "Hello World!")
|
||||
else:
|
||||
add(tmp1, text "Hello Universe")
|
||||
tmp1
|
||||
```
|
||||
|
||||
## Event model
|
||||
|
||||
Karax does not change the DOM's event model much, here is a program
|
||||
that writes "Hello simulated universe" on a button click:
|
||||
|
||||
```nim
|
||||
|
||||
include karax / prelude
|
||||
# alternatively: import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
|
||||
|
||||
var lines: seq[kstring] = @[]
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
button:
|
||||
text "Say hello!"
|
||||
proc onclick(ev: Event; n: VNode) =
|
||||
lines.add "Hello simulated universe"
|
||||
for x in lines:
|
||||
tdiv:
|
||||
text x
|
||||
|
||||
setRenderer createDom
|
||||
```
|
||||
|
||||
``kstring`` is Karax's alias for ``cstring`` (which stands for "compatible
|
||||
string"; for the JS target that is an immutable JavaScript string) which
|
||||
is preferred for efficiency on the JS target. However, on the native targets
|
||||
``kstring`` is mapped to ``string`` for efficiency. The DSL for HTML
|
||||
construction is also avaible for the native targets (!) and the ``kstring``
|
||||
abstraction helps to deal with these conflicting requirements.
|
||||
|
||||
Karax's DSL is quite flexible when it comes to event handlers, so the
|
||||
following syntax is also supported:
|
||||
|
||||
```nim
|
||||
|
||||
include karax / prelude
|
||||
from sugar import `=>`
|
||||
|
||||
var lines: seq[kstring] = @[]
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
button(onclick = () => lines.add "Hello simulated universe"):
|
||||
text "Say hello!"
|
||||
for x in lines:
|
||||
tdiv:
|
||||
text x
|
||||
|
||||
setRenderer createDom
|
||||
```
|
||||
|
||||
The ``buildHtml`` macro produces this code for us:
|
||||
|
||||
```nim
|
||||
|
||||
let tmp2 = tree(VNodeKind.tdiv)
|
||||
let tmp3 = tree(VNodeKind.button)
|
||||
addEventHandler(tmp3, EventKind.onclick,
|
||||
() => lines.add "Hello simulated universe", kxi)
|
||||
add(tmp3, text "Say hello!")
|
||||
add(tmp2, tmp3)
|
||||
for x in lines:
|
||||
let tmp4 = tree(VNodeKind.tdiv)
|
||||
add(tmp4, text x)
|
||||
add(tmp2, tmp4)
|
||||
tmp2
|
||||
```
|
||||
As the examples grow larger it becomes more and more visible of what
|
||||
a DSL that composes with the builtin Nim control flow constructs buys us.
|
||||
Once you have tasted this power there is no going back and languages
|
||||
without AST based macro system simply don't cut it anymore.
|
||||
|
||||
|
||||
## Attaching data to an event handler
|
||||
|
||||
|
||||
Since the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any
|
||||
additional data that should be passed to the event handler needs to be
|
||||
done via Nim's closures. In general this means a pattern like this:
|
||||
|
||||
```nim
|
||||
|
||||
proc menuAction(menuEntry: kstring): proc() =
|
||||
result = proc() =
|
||||
echo "clicked ", menuEntry
|
||||
|
||||
proc buildMenu(menu: seq[kstring]): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
for m in menu:
|
||||
nav(class="navbar is-primary"):
|
||||
tdiv(class="navbar-brand"):
|
||||
a(class="navbar-item", onclick = menuAction(m)):
|
||||
```
|
||||
|
||||
## DOM diffing
|
||||
|
||||
Ok, so now we have seen DOM creation and event handlers. But how does
|
||||
Karax actually keep the DOM up to date? The trick is that every event
|
||||
handler is wrapped in a helper proc that triggers a *redraw* operation
|
||||
that calls the *renderer* that you initially passed to ``setRenderer``.
|
||||
So a new virtual DOM is created and compared against the previous
|
||||
virtual DOM. This comparison produces a patch set that is then applied
|
||||
to the real DOM the browser uses internally. This process is called
|
||||
"virtual DOM diffing" and other frameworks, most notably Facebook's
|
||||
*React*, do quite similar things. The virtual DOM is faster to create
|
||||
and manipulate than the real DOM so this approach is quite efficient.
|
||||
|
||||
|
||||
## Form validation
|
||||
Most applications these days have some "login"
|
||||
mechanism consisting of ``username`` and ``password`` and
|
||||
a ``login`` button. The login button should only be clickable
|
||||
if ``username`` and ``password`` are not empty. An error
|
||||
message should be shown as long as one input field is empty.
|
||||
|
||||
To create new UI elements we write a ``loginField`` proc that
|
||||
returns a ``VNode``:
|
||||
|
||||
```nim
|
||||
|
||||
proc loginField(desc, field, class: kstring;
|
||||
validator: proc (field: kstring): proc ()): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
label(`for` = field):
|
||||
text desc
|
||||
input(class = class, id = field, onchange = validator(field))
|
||||
```
|
||||
|
||||
We use the ``karax / errors`` module to help with this error
|
||||
logic. The ``errors`` module is mostly a mapping from strings to
|
||||
strings but it turned out that the logic is tricky enough to warrant
|
||||
a library solution. ``validateNotEmpty`` returns a closure that
|
||||
captures the ``field`` parameter:
|
||||
|
||||
```nim
|
||||
|
||||
proc validateNotEmpty(field: kstring): proc () =
|
||||
result = proc () =
|
||||
let x = getVNodeById(field).getInputText
|
||||
if x.isNil or x == "":
|
||||
errors.setError(field, field & " must not be empty")
|
||||
else:
|
||||
errors.setError(field, "")
|
||||
```
|
||||
|
||||
This indirection is required because
|
||||
event handlers in Karax need to have the type ``proc ()``
|
||||
or ``proc (ev: Event; n: VNode)``. The errors module also
|
||||
gives us a handy ``disableOnError`` helper. It returns
|
||||
``"disabled"`` if there are errors. Now we have all the
|
||||
pieces together to write our login dialog:
|
||||
|
||||
|
||||
```nim
|
||||
|
||||
# some consts in order to prevent typos:
|
||||
const
|
||||
username = kstring"username"
|
||||
password = kstring"password"
|
||||
|
||||
var loggedIn: bool
|
||||
|
||||
proc loginDialog(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
if not loggedIn:
|
||||
loginField("Name :", username, "input", validateNotEmpty)
|
||||
loginField("Password: ", password, "password", validateNotEmpty)
|
||||
button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
|
||||
text "Login"
|
||||
p:
|
||||
text errors.getError(username)
|
||||
p:
|
||||
text errors.getError(password)
|
||||
else:
|
||||
p:
|
||||
text "You are now logged in."
|
||||
|
||||
setRenderer loginDialog
|
||||
```
|
||||
|
||||
(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/login.nim).)
|
||||
|
||||
This code still has a bug though, when you run it, the ``login`` button is not
|
||||
disabled until some input fields are validated! This is easily fixed,
|
||||
at initialization we have to do:
|
||||
|
||||
```nim
|
||||
|
||||
setError username, username & " must not be empty"
|
||||
setError password, password & " must not be empty"
|
||||
```
|
||||
There are likely more elegant solutions to this problem.
|
||||
|
||||
## Routing
|
||||
|
||||
|
||||
For routing ``setRenderer`` can be called with a callback that takes a parameter of
|
||||
type ``RouterData``. Here is the relevant excerpt from the famous "Todo App" example:
|
||||
|
||||
```nim
|
||||
|
||||
proc createDom(data: RouterData): VNode =
|
||||
if data.hashPart == "#/": filter = all
|
||||
elif data.hashPart == "#/completed": filter = completed
|
||||
elif data.hashPart == "#/active": filter = active
|
||||
result = buildHtml(tdiv(class="todomvc-wrapper")):
|
||||
section(class = "todoapp"):
|
||||
...
|
||||
|
||||
setRenderer createDom
|
||||
```
|
||||
(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/todoapp/todoapp.nim).)
|
||||
|
||||
## Server Side HTML Rendering
|
||||
|
||||
Karax can also be used to render HTML on the server. Only a subset of
|
||||
modules can be used since there is no JS interpreter.
|
||||
|
||||
```nim
|
||||
|
||||
import karax / [karaxdsl, vdom]
|
||||
|
||||
const places = @["boston", "cleveland", "los angeles", "new orleans"]
|
||||
|
||||
proc render*(): string =
|
||||
let vnode = buildHtml(tdiv(class = "mt-3")):
|
||||
h1: text "My Web Page"
|
||||
p: text "Hello world"
|
||||
ul:
|
||||
for place in places:
|
||||
li: text place
|
||||
dl:
|
||||
dt: text "Can I use Karax for client side single page apps?"
|
||||
dd: text "Yes"
|
||||
|
||||
dt: text "Can I use Karax for server side HTML rendering?"
|
||||
dd: text "Yes"
|
||||
result = $vnode
|
||||
|
||||
echo render()
|
||||
```
|
||||
## Generate HTML with event handlers
|
||||
|
||||
If you are writing a static site generator or do server-side HTML rendering
|
||||
via ``nim c``, you may want to override ``addEventHandler`` when using event
|
||||
handlers to avoid compiler complaints.
|
||||
|
||||
Here's an example of auto submit a dropdown when a value is selected:
|
||||
|
||||
```nim
|
||||
|
||||
template kxi(): int = 0
|
||||
template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =
|
||||
n.setAttr($k, action)
|
||||
|
||||
let
|
||||
names = @["nim", "c", "python"]
|
||||
selected_name = request.params.getOrDefault("name")
|
||||
hello = buildHtml(html):
|
||||
form(`method` = "get"):
|
||||
select(name="name", onchange="this.form.submit()"):
|
||||
for name in names:
|
||||
if name == selected_name:
|
||||
option(selected = ""): text name
|
||||
else:
|
||||
option: text name
|
||||
```
|
||||
|
||||
## License
|
||||
MIT License. See [here](https://github.com/karaxnim/karax/blob/master/LICENSE.txt).
|
||||
355
readme.rst
355
readme.rst
|
|
@ -1,355 +0,0 @@
|
|||
Karax – Single page applications in Nim |travis|
|
||||
================================================
|
||||
|
||||
Karax is a framework for developing single page applications in Nim.
|
||||
|
||||
To try it out, run::
|
||||
|
||||
cd ~/projects # Insert your favourite directory for projects
|
||||
|
||||
nimble develop karax # This will clone Karax and create a link to it in ~/.nimble
|
||||
|
||||
cd karax
|
||||
|
||||
cd examples/todoapp
|
||||
nim js todoapp.nim
|
||||
open todoapp.html
|
||||
cd ../..
|
||||
|
||||
cd examples/mediaplayer
|
||||
nim js playerapp.nim
|
||||
open playerapp.html
|
||||
|
||||
It uses a virtual DOM like React, but is much smaller than the existing
|
||||
frameworks plus of course it's written in Nim for Nim. No external
|
||||
dependencies! And thanks to Nim's whole program optimization only what
|
||||
is used ends up in the generated JavaScript code.
|
||||
|
||||
|
||||
Goals
|
||||
=====
|
||||
|
||||
- Leverage Nim's macro system to produce a framework that allows
|
||||
for the development of applications that are boilerplate free.
|
||||
- Keep it small, keep it fast, keep it flexible.
|
||||
|
||||
.. |travis| image:: https://travis-ci.org/pragmagic/karax.svg?branch=master
|
||||
:target: https://travis-ci.org/pragmagic/karax
|
||||
|
||||
|
||||
Hello World
|
||||
===========
|
||||
|
||||
The simplest Karax program looks like this:
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
include karax / prelude
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
text "Hello World!"
|
||||
|
||||
setRenderer createDom
|
||||
|
||||
|
||||
Since ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead
|
||||
here. ``tdiv`` produces a ``<div>`` virtual DOM node.
|
||||
|
||||
As you can see, karax comes with its own ``buildHtml`` DSL for convenient
|
||||
construction of (virtual) DOM trees (of type ``VNode``). Karax provides
|
||||
a tiny build tool called ``karun`` that generates the HTML boilerplate code that
|
||||
embeds and invokes the generated JavaScript code::
|
||||
|
||||
nim c karax/tools/karun
|
||||
karax/tools/karun -r helloworld.nim
|
||||
|
||||
Via ``-d:debugKaraxDsl`` we can have a look at the produced Nim code by
|
||||
``buildHtml``:
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
let tmp1 = tree(VNodeKind.tdiv)
|
||||
add(tmp1, text "Hello World!")
|
||||
tmp1
|
||||
|
||||
(I shortened the IDs for better readability.)
|
||||
|
||||
Ok, so ``buildHtml`` introduces temporaries and calls ``add`` for the tree
|
||||
construction so that it composes with all of Nim's control flow constructs:
|
||||
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
include karax / prelude
|
||||
import random
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
if random(100) <= 50:
|
||||
text "Hello World!"
|
||||
else:
|
||||
text "Hello Universe"
|
||||
|
||||
randomize()
|
||||
setRenderer createDom
|
||||
|
||||
|
||||
Produces:
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
let tmp1 = tree(VNodeKind.tdiv)
|
||||
if random(100) <= 50:
|
||||
add(tmp1, text "Hello World!")
|
||||
else:
|
||||
add(tmp1, text "Hello Universe")
|
||||
tmp1
|
||||
|
||||
|
||||
Event model
|
||||
===========
|
||||
|
||||
Karax does not change the DOM's event model much, here is a program
|
||||
that writes "Hello simulated universe" on a button click:
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
include karax / prelude
|
||||
# alternatively: import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
|
||||
|
||||
var lines: seq[kstring] = @[]
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
button:
|
||||
text "Say hello!"
|
||||
proc onclick(ev: Event; n: VNode) =
|
||||
lines.add "Hello simulated universe"
|
||||
for x in lines:
|
||||
tdiv:
|
||||
text x
|
||||
|
||||
setRenderer createDom
|
||||
|
||||
|
||||
``kstring`` is Karax's alias for ``cstring`` (which stands for "compatible
|
||||
string"; for the JS target that is an immutable JavaScript string) which
|
||||
is preferred for efficiency on the JS target. However, on the native targets
|
||||
``kstring`` is mapped to ``string`` for efficiency. The DSL for HTML
|
||||
construction is also avaible for the native targets (!) and the ``kstring``
|
||||
abstraction helps to deal with these conflicting requirements.
|
||||
|
||||
Karax's DSL is quite flexible when it comes to event handlers, so the
|
||||
following syntax is also supported:
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
include karax / prelude
|
||||
from sugar import `=>`
|
||||
|
||||
var lines: seq[kstring] = @[]
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
button(onclick = () => lines.add "Hello simulated universe"):
|
||||
text "Say hello!"
|
||||
for x in lines:
|
||||
tdiv:
|
||||
text x
|
||||
|
||||
setRenderer createDom
|
||||
|
||||
|
||||
The ``buildHtml`` macro produces this code for us:
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
let tmp2 = tree(VNodeKind.tdiv)
|
||||
let tmp3 = tree(VNodeKind.button)
|
||||
addEventHandler(tmp3, EventKind.onclick,
|
||||
() => lines.add "Hello simulated universe", kxi)
|
||||
add(tmp3, text "Say hello!")
|
||||
add(tmp2, tmp3)
|
||||
for x in lines:
|
||||
let tmp4 = tree(VNodeKind.tdiv)
|
||||
add(tmp4, text x)
|
||||
add(tmp2, tmp4)
|
||||
tmp2
|
||||
|
||||
As the examples grow larger it becomes more and more visible of what
|
||||
a DSL that composes with the builtin Nim control flow constructs buys us.
|
||||
Once you have tasted this power there is no going back and languages
|
||||
without AST based macro system simply don't cut it anymore.
|
||||
|
||||
|
||||
Attaching data to an event handler
|
||||
==================================
|
||||
|
||||
Since the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any
|
||||
additional data that should be passed to the event handler needs to be
|
||||
done via Nim's closures. In general this means a pattern like this:
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
proc menuAction(menuEntry: kstring): proc() =
|
||||
result = proc() =
|
||||
echo "clicked ", menuEntry
|
||||
|
||||
proc buildMenu(menu: seq[kstring]): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
for m in menu:
|
||||
nav(class="navbar is-primary"):
|
||||
tdiv(class="navbar-brand"):
|
||||
a(class="navbar-item", onclick = menuAction(m)):
|
||||
|
||||
|
||||
DOM diffing
|
||||
===========
|
||||
|
||||
Ok, so now we have seen DOM creation and event handlers. But how does
|
||||
Karax actually keep the DOM up to date? The trick is that every event
|
||||
handler is wrapped in a helper proc that triggers a *redraw* operation
|
||||
that calls the *renderer* that you initially passed to ``setRenderer``.
|
||||
So a new virtual DOM is created and compared against the previous
|
||||
virtual DOM. This comparison produces a patch set that is then applied
|
||||
to the real DOM the browser uses internally. This process is called
|
||||
"virtual DOM diffing" and other frameworks, most notably Facebook's
|
||||
*React*, do quite similar things. The virtual DOM is faster to create
|
||||
and manipulate than the real DOM so this approach is quite efficient.
|
||||
|
||||
|
||||
Form validation
|
||||
===============
|
||||
|
||||
Most applications these days have some "login"
|
||||
mechanism consisting of ``username`` and ``password`` and
|
||||
a ``login`` button. The login button should only be clickable
|
||||
if ``username`` and ``password`` are not empty. An error
|
||||
message should be shown as long as one input field is empty.
|
||||
|
||||
To create new UI elements we write a ``loginField`` proc that
|
||||
returns a ``VNode``:
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
proc loginField(desc, field, class: kstring;
|
||||
validator: proc (field: kstring): proc ()): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
label(`for` = field):
|
||||
text desc
|
||||
input(class = class, id = field, onchange = validator(field))
|
||||
|
||||
We use the ``karax / errors`` module to help with this error
|
||||
logic. The ``errors`` module is mostly a mapping from strings to
|
||||
strings but it turned out that the logic is tricky enough to warrant
|
||||
a library solution. ``validateNotEmpty`` returns a closure that
|
||||
captures the ``field`` parameter:
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
proc validateNotEmpty(field: kstring): proc () =
|
||||
result = proc () =
|
||||
let x = getVNodeById(field)
|
||||
if x.text.isNil or x.text == "":
|
||||
errors.setError(field, field & " must not be empty")
|
||||
else:
|
||||
errors.setError(field, "")
|
||||
|
||||
This indirection is required because
|
||||
event handlers in Karax need to have the type ``proc ()``
|
||||
or ``proc (ev: Event; n: VNode)``. The errors module also
|
||||
gives us a handy ``disableOnError`` helper. It returns
|
||||
``"disabled"`` if there are errors. Now we have all the
|
||||
pieces together to write our login dialog:
|
||||
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
# some consts in order to prevent typos:
|
||||
const
|
||||
username = kstring"username"
|
||||
password = kstring"password"
|
||||
|
||||
var loggedIn: bool
|
||||
|
||||
proc loginDialog(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
if not loggedIn:
|
||||
loginField("Name :", username, "input", validateNotEmpty)
|
||||
loginField("Password: ", password, "password", validateNotEmpty)
|
||||
button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
|
||||
text "Login"
|
||||
p:
|
||||
text errors.getError(username)
|
||||
p:
|
||||
text errors.getError(password)
|
||||
else:
|
||||
p:
|
||||
text "You are now logged in."
|
||||
|
||||
setRenderer loginDialog
|
||||
|
||||
(Full example `here <https://github.com/pragmagic/karax/blob/master/examples/login.nim>`_.)
|
||||
|
||||
This code still has a bug though, when you run it, the ``login`` button is not
|
||||
disabled until some input fields are validated! This is easily fixed,
|
||||
at initialization we have to do:
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
setError username, username & " must not be empty"
|
||||
setError password, password & " must not be empty"
|
||||
|
||||
There are likely more elegant solutions to this problem.
|
||||
|
||||
|
||||
Routing
|
||||
=======
|
||||
|
||||
For routing ``setRenderer`` can be called with a callback that takes a parameter of
|
||||
type ``RouterData``. Here is the relevant excerpt from the famous "Todo App" example:
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
proc createDom(data: RouterData): VNode =
|
||||
if data.hashPart == "#/": filter = all
|
||||
elif data.hashPart == "#/completed": filter = completed
|
||||
elif data.hashPart == "#/active": filter = active
|
||||
result = buildHtml(tdiv(class="todomvc-wrapper")):
|
||||
section(class = "todoapp"):
|
||||
...
|
||||
|
||||
setRenderer createDom
|
||||
|
||||
(Full example `here <https://github.com/pragmagic/karax/blob/master/examples/todoapp/todoapp.nim>`_.)
|
||||
|
||||
|
||||
Server Side HTML Rendering
|
||||
==========================
|
||||
|
||||
Karax can also be used to render HTML on the server. Only a subset of
|
||||
modules can be used since there is no JS interpreter.
|
||||
|
||||
.. code-block:: nim
|
||||
|
||||
import karax / [karaxdsl, vdom]
|
||||
|
||||
const places = @["boston", "cleveland", "los angeles", "new orleans"]
|
||||
|
||||
proc render*(): string =
|
||||
let vnode = buildHtml(tdiv(class = "mt-3")):
|
||||
h1: text "My Web Page"
|
||||
p: text "Hello world"
|
||||
ul:
|
||||
for place in places:
|
||||
li: text place
|
||||
dl:
|
||||
dt: text "Can I use Karax for client side single page apps?"
|
||||
dd: text "Yes"
|
||||
|
||||
dt: text "Can I use Karax for server side HTML rendering?"
|
||||
dd: text "Yes"
|
||||
result = $vnode
|
||||
|
||||
echo render()
|
||||
Loading…
Add table
Add a link
Reference in a new issue