From 02038d40c1679c8286f145e72643651524319b89 Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 12 Apr 2020 23:41:24 +0200 Subject: [PATCH 01/31] fixes #141 --- karax/karax.nim | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/karax/karax.nim b/karax/karax.nim index 84ab6f5..5c0b41a 100644 --- a/karax/karax.nim +++ b/karax/karax.nim @@ -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.. Date: Sun, 12 Apr 2020 23:41:55 +0200 Subject: [PATCH 02/31] 1.1.2 --- karax.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/karax.nimble b/karax.nimble index c6259f6..bc157ab 100644 --- a/karax.nimble +++ b/karax.nimble @@ -1,6 +1,6 @@ # Package -version = "1.1.1" +version = "1.1.2" author = "Andreas Rumpf" description = "Karax is a framework for developing single page applications in Nim." license = "MIT" From 80f500874b71aff061739b2d1e2eadc7b2e68624 Mon Sep 17 00:00:00 2001 From: Miran Date: Mon, 27 Apr 2020 10:59:54 +0200 Subject: [PATCH 03/31] fix deprecated stuff (#144) --- examples/hellouniverse.nim | 2 +- karax/compact.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hellouniverse.nim b/examples/hellouniverse.nim index 9d6ebc6..c07435b 100644 --- a/examples/hellouniverse.nim +++ b/examples/hellouniverse.nim @@ -4,7 +4,7 @@ import random proc createDom(): VNode = result = buildHtml(tdiv): - if random(100) <= 50: + if rand(100) <= 50: text "Hello World!" else: text "Hello Universe" diff --git a/karax/compact.nim b/karax/compact.nim index 3363802..4e5dd6d 100644 --- a/karax/compact.nim +++ b/karax/compact.nim @@ -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 From f1cd9e606d117bf387a1ddf9e71cafd02d10a50f Mon Sep 17 00:00:00 2001 From: dawkot <97dawkot@gmail.com> Date: Sat, 2 May 2020 13:42:53 +0200 Subject: [PATCH 04/31] Added `toSelected` (#145) Co-authored-by: dawkot --- karax/karax.nim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/karax/karax.nim b/karax/karax.nim index 5c0b41a..c32e969 100644 --- a/karax/karax.nim +++ b/karax/karax.nim @@ -815,5 +815,8 @@ proc toChecked*(checked: bool): cstring = 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) From 22e2e226cb82ca4c8495d10a41545a1df0bbb3d9 Mon Sep 17 00:00:00 2001 From: tarunama Date: Thu, 2 Jul 2020 17:36:20 +0900 Subject: [PATCH 05/31] Refactor examples html, nim files. (#147) * refactor: remove no need first new line * arrange: add indent in HTML files Co-authored-by: Takuya Yamamoto --- examples/button.nim | 1 - examples/carousel/carousel.html | 14 +++++++------- examples/hellouniverse.nim | 1 - examples/login.nim | 1 - examples/mediaplayer/playerapp.html | 14 +++++++------- examples/scrollapp/scrollapp.html | 14 +++++++------- examples/todoapp/todoapp.html | 16 ++++++++-------- examples/toychat.nim | 1 - 8 files changed, 29 insertions(+), 33 deletions(-) diff --git a/examples/button.nim b/examples/button.nim index bd49dc3..d86fa82 100644 --- a/examples/button.nim +++ b/examples/button.nim @@ -1,4 +1,3 @@ - include karax / prelude var lines: seq[kstring] = @[] diff --git a/examples/carousel/carousel.html b/examples/carousel/carousel.html index 464f960..500e73a 100644 --- a/examples/carousel/carousel.html +++ b/examples/carousel/carousel.html @@ -1,15 +1,15 @@ - - Carousel app - + + Carousel app + - + -
+
- + - + diff --git a/examples/hellouniverse.nim b/examples/hellouniverse.nim index c07435b..8343d07 100644 --- a/examples/hellouniverse.nim +++ b/examples/hellouniverse.nim @@ -1,4 +1,3 @@ - include karax / prelude import random diff --git a/examples/login.nim b/examples/login.nim index 642f573..f7b9062 100644 --- a/examples/login.nim +++ b/examples/login.nim @@ -1,4 +1,3 @@ - include karax / prelude from sugar import `=>` import karax / errors diff --git a/examples/mediaplayer/playerapp.html b/examples/mediaplayer/playerapp.html index 7800d32..a45a2e8 100644 --- a/examples/mediaplayer/playerapp.html +++ b/examples/mediaplayer/playerapp.html @@ -1,15 +1,15 @@ - - Player app - + + Player app + - + -
+
- + - + diff --git a/examples/scrollapp/scrollapp.html b/examples/scrollapp/scrollapp.html index f7ca085..44d657e 100644 --- a/examples/scrollapp/scrollapp.html +++ b/examples/scrollapp/scrollapp.html @@ -1,15 +1,15 @@ - - Todo app - + + Todo app + - + -
+
- + - + diff --git a/examples/todoapp/todoapp.html b/examples/todoapp/todoapp.html index bec9d65..ab3daa7 100644 --- a/examples/todoapp/todoapp.html +++ b/examples/todoapp/todoapp.html @@ -1,16 +1,16 @@ - - Todo app - - + + Todo app + + - + -
+
- + - + diff --git a/examples/toychat.nim b/examples/toychat.nim index 66c4cd0..600998c 100644 --- a/examples/toychat.nim +++ b/examples/toychat.nim @@ -1,4 +1,3 @@ - include login const From 98a2d1ce5061b7cc28096800a1e81a79ea7d9951 Mon Sep 17 00:00:00 2001 From: Bung Date: Thu, 23 Jul 2020 16:40:04 +0800 Subject: [PATCH 06/31] add static server,websocket refresh watching mode (#149) * add static server,websocket refresh watching mode * watch optional * we dont quit when building error in watch mode * staticDir,publicUrl configure through dot env * remove unused import * serve optional --- .gitignore | 2 + karax.nimble | 3 +- karax/tools/karun.nim | 116 ++++++++++++++--------- karax/tools/karun.nims | 1 + karax/tools/static_server.nim | 170 ++++++++++++++++++++++++++++++++++ 5 files changed, 248 insertions(+), 44 deletions(-) create mode 100644 karax/tools/karun.nims create mode 100644 karax/tools/static_server.nim diff --git a/.gitignore b/.gitignore index acb4ec1..842e140 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ nimcache/ karax/tools/karun *.code-workspace *.exe +app.js +app.html diff --git a/karax.nimble b/karax.nimble index bc157ab..a5b269c 100644 --- a/karax.nimble +++ b/karax.nimble @@ -8,7 +8,8 @@ license = "MIT" # Dependencies requires "nim >= 0.16.1" - +requires "ws" +requires "dotenv" skipDirs = @["examples", "experiments", "tests"] bin = @["karax/tools/karun"] diff --git a/karax/tools/karun.nim b/karax/tools/karun.nim index 2b948b4..41410ca 100644 --- a/karax/tools/karun.nim +++ b/karax/tools/karun.nim @@ -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 = """ """ - html = """ + +const html = """ @@ -23,33 +18,79 @@ const $1 $2 - -
- + +
+ +$3 """ +const websocket = """ + +""" 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 run and 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 + + 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 +100,11 @@ proc main = run = true rest = rest.replace("--run ") of "css": - selectedCss = css - rest = rest.replace("--css ") + if op.val != "": + selectedCss = readFile(op.val) + else: + selectedCss = css + rest = rest.substr(rest.find(" ")) else: discard of cmdShortOption: if op.key == "r": @@ -73,25 +117,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() - - - - diff --git a/karax/tools/karun.nims b/karax/tools/karun.nims new file mode 100644 index 0000000..8983496 --- /dev/null +++ b/karax/tools/karun.nims @@ -0,0 +1 @@ +switch("threads", "on") \ No newline at end of file diff --git a/karax/tools/static_server.nim b/karax/tools/static_server.nim new file mode 100644 index 0000000..40375a9 --- /dev/null +++ b/karax/tools/static_server.nim @@ -0,0 +1,170 @@ +import net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5, + logging, httpcore, asyncfile, options +import asynchttpserver +from cgi import decodeUrl +import ws,tables,times +import dotenv + +type + RawHeaders* = seq[tuple[key, val: string]] + +proc toStr(headers: Option[RawHeaders]): string = + return $newHttpHeaders(headers.get(@({:}))) + +proc send( + request: Request, code: HttpCode, headers: Option[RawHeaders], body: string +): Future[void] = + return request.respond( + code, body, newHttpHeaders(headers.get(@({:}))) + ) + +proc statusContent(request: Request, status: HttpCode, content: string, + headers: Option[RawHeaders]): Future[void] = + try: + result = send(request, status, headers, content) + when not defined(release): + logging.debug(" $1 $2" % [$status, toStr(headers)]) + except: + logging.error("Could not send response: $1" % osErrorMsg(osLastError())) + +proc sendStaticIfExists( + req: Request, paths: seq[string] +): Future[HttpCode] {.async.} = + result = Http200 + let mimes = newMimetypes() + for p in paths: + if existsFile(p): + + var fp = getFilePermissions(p) + if not fp.contains(fpOthersRead): + return Http403 + + let fileSize = getFileSize(p) + let ext = p.splitFile.ext + let mimetype = mimes.getMimetype( + if ext.len > 0: ext[1 .. ^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.hasKey("If-None-Match") and req.headers["If-None-Match"] == hashed: + await req.statusContent(Http304, "", none[RawHeaders]()) + else: + await req.statusContent(Http200, file, some(@({ + "Content-Type": mimetype, + "ETag": hashed + }))) + else: + let headers = @({ + "Content-Type": mimetype, + "Content-Length": $fileSize + }) + await req.statusContent(Http200, "", some(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 publicUrl = getEnv("publicUrl") + var staticDir = getEnv("staticDir") + if not publicUrl.endsWith("/"): + publicUrl = publicUrl & "/" + reqPath = reqPath.substr(publicUrl.len) + + let path = normalizedPath( + staticDir / reqPath + ) + + # Verify that this isn't outside our static dir. + var status = Http400 + let pathDir = path.splitFile.dir / "" + + if pathDir.startsWith(publicUrl): + if existsDir(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: + loadEnvFromString(""" + staticDir="./src/assets/" + publicUrl="public" + """) + + 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: + logging.error("$1 not exists!" % file ) + if fpUserRead notin os.getFilePermissions(file): + logging.error("Could not read $1!" % 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() \ No newline at end of file From 7a36b19729624be0c542b903ed9da89ccf5984ed Mon Sep 17 00:00:00 2001 From: Bung Date: Fri, 31 Jul 2020 23:54:05 +0800 Subject: [PATCH 07/31] watch ignore precompile style lang (#150) --- karax/tools/karun.nim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/karax/tools/karun.nim b/karax/tools/karun.nim index 41410ca..b29cebe 100644 --- a/karax/tools/karun.nim +++ b/karax/tools/karun.nim @@ -70,7 +70,9 @@ proc watchBuild(filePath: string, selectedCss: string, rest: string) {.thread.} 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) From 0813a7adf9047814cc6a89c1975ad3f98e0b74a6 Mon Sep 17 00:00:00 2001 From: Bung Date: Thu, 27 Aug 2020 00:03:52 +0800 Subject: [PATCH 08/31] add transition events (#151) --- karax/vdom.nim | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/karax/vdom.nim b/karax/vdom.nim index 3f44673..d0ee5b5 100644 --- a/karax/vdom.nim +++ b/karax/vdom.nim @@ -97,6 +97,11 @@ type ## passed (useful for on-the-fly text completions) onload, # img + ontransitioncancel, + ontransitionend, + ontransitionrun, + ontransitionstart + macro buildLookupTables(): untyped = var a = newTree(nnkBracket) for i in low(VNodeKind)..high(VNodeKind): From f0dd16ebfe26e79e069f10ed02970b07a77f4081 Mon Sep 17 00:00:00 2001 From: OctoCat <62594565+lumison@users.noreply.github.com> Date: Mon, 28 Sep 2020 09:46:23 +0300 Subject: [PATCH 09/31] Fixed the error with password (#155) * Fixed the error with password * Changed to old style --- examples/login.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/login.nim b/examples/login.nim index f7b9062..0350e23 100644 --- a/examples/login.nim +++ b/examples/login.nim @@ -17,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, "") @@ -27,7 +27,7 @@ var loggedIn: bool proc loginDialog(): VNode = result = buildHtml(tdiv): if not loggedIn: - loginField("Name :", username, "input", validateNotEmpty) + loginField("Name: ", username, "input", validateNotEmpty) loginField("Password: ", password, "password", validateNotEmpty) button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()): text "Login" From d8fe79fa4041325da07892978336eb8a27e5ac6b Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Thu, 1 Oct 2020 00:28:42 -0700 Subject: [PATCH 10/31] make vstyles.setAttr kstring overload public (#156) --- karax/vstyles.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/karax/vstyles.nim b/karax/vstyles.nim index e2fca7d..13f9c3a 100644 --- a/karax/vstyles.nim +++ b/karax/vstyles.nim @@ -234,7 +234,7 @@ 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.} = var i = 0 while i < s.len: if s[i] == a: From d2f88b4648e8fbff7eb207f2974daebec74c52a9 Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Thu, 1 Oct 2020 10:35:25 -0700 Subject: [PATCH 11/31] close #6 (#157) --- karax.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/karax.nimble b/karax.nimble index a5b269c..ac15c06 100644 --- a/karax.nimble +++ b/karax.nimble @@ -7,7 +7,7 @@ license = "MIT" # Dependencies -requires "nim >= 0.16.1" +requires "nim >= 0.18.0" requires "ws" requires "dotenv" skipDirs = @["examples", "experiments", "tests"] From 5a2ea1eef9a11f6f08f7acaa8998a066176adbc8 Mon Sep 17 00:00:00 2001 From: Araq Date: Thu, 1 Oct 2020 19:56:30 +0200 Subject: [PATCH 12/31] 1.1.3 --- karax.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/karax.nimble b/karax.nimble index ac15c06..9e29495 100644 --- a/karax.nimble +++ b/karax.nimble @@ -1,6 +1,6 @@ # Package -version = "1.1.2" +version = "1.1.3" author = "Andreas Rumpf" description = "Karax is a framework for developing single page applications in Nim." license = "MIT" From bd18fc980ccfb7f44efb1f69a0939d2af924b6db Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Thu, 1 Oct 2020 13:13:54 -0700 Subject: [PATCH 13/31] fix #153 (#159) --- karax/vdom.nim | 6 ++++++ karax/vstyles.nim | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/karax/vdom.nim b/karax/vdom.nim index d0ee5b5..8a3dfaa 100644 --- a/karax/vdom.nim +++ b/karax/vdom.nim @@ -303,6 +303,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 diff --git a/karax/vstyles.nim b/karax/vstyles.nim index 13f9c3a..a5971a9 100644 --- a/karax/vstyles.nim +++ b/karax/vstyles.nim @@ -235,6 +235,9 @@ proc eq*(a, b: VStyle): bool = return true 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 +246,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 From 04779f85028c69574f79549387e4057af507dd5f Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Wed, 7 Oct 2020 03:23:16 -0700 Subject: [PATCH 14/31] add toCss: allows tdiv(style = "font-style: oblique; color: pink".toCss) (#158) * add toCss: allows tdiv(style = "font-style: oblique; color: pink".toCss) * fixup to work with nim c --- examples/hellostyle.nim | 17 +++++++++++++++++ karax/vstyles.nim | 24 +++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 examples/hellostyle.nim diff --git a/examples/hellostyle.nim b/examples/hellostyle.nim new file mode 100644 index 0000000..716c2f0 --- /dev/null +++ b/examples/hellostyle.nim @@ -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 diff --git a/karax/vstyles.nim b/karax/vstyles.nim index a5971a9..7097aee 100644 --- a/karax/vstyles.nim +++ b/karax/vstyles.nim @@ -1,5 +1,9 @@ +##[ +see examples/hellostyle.nim +]## -import macros, kbase +import std/[macros, strutils] +import kbase when defined(js): import kdom, jdict @@ -292,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.} From bcab551f05067a99a4c5adacd13a3531c9c6d78d Mon Sep 17 00:00:00 2001 From: Demian Florentin Date: Thu, 22 Oct 2020 06:38:22 -0300 Subject: [PATCH 15/31] onwheel event added (#161) --- karax/vdom.nim | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/karax/vdom.nim b/karax/vdom.nim index 8a3dfaa..6668d10 100644 --- a/karax/vdom.nim +++ b/karax/vdom.nim @@ -100,8 +100,10 @@ type ontransitioncancel, ontransitionend, ontransitionrun, - ontransitionstart - + 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): From d3fc084c42d02dbcdbb639c464c0da16223af4e2 Mon Sep 17 00:00:00 2001 From: Xie Yanbo Date: Fri, 30 Oct 2020 17:44:38 +0800 Subject: [PATCH 16/31] added documentation on rendering to string when using event handlers (#162) --- readme.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/readme.rst b/readme.rst index 4b8cec2..5d3e862 100644 --- a/readme.rst +++ b/readme.rst @@ -353,3 +353,30 @@ modules can be used since there is no JS interpreter. 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: + +.. code-block:: 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.getOrDefaul("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 From 40daae64e339da00b81ddd5972b16de9f8ef35aa Mon Sep 17 00:00:00 2001 From: Xie Yanbo Date: Mon, 2 Nov 2020 15:17:27 +0800 Subject: [PATCH 17/31] fix typo (#163) --- readme.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.rst b/readme.rst index 5d3e862..6099bd8 100644 --- a/readme.rst +++ b/readme.rst @@ -371,7 +371,7 @@ Here's an example of auto submit a dropdown when a value is selected: let names = @["nim", "c", "python"] - selected_name = request.params.getOrDefaul("name") + selected_name = request.params.getOrDefault("name") hello = buildHtml(html): form(`method` = "get"): select(name="name", onchange="this.form.submit()"): From 24a037ad1959adbc3f5d0fb9cc22ae84794d3a44 Mon Sep 17 00:00:00 2001 From: Constantine Molchanov Date: Wed, 25 Nov 2020 14:28:12 +0400 Subject: [PATCH 18/31] Get rid of annoying deprecation warnings (#164) * Get rid of annoying deprecation warnings Changed existsFile to fileExists and existsDir to dirExists. * Install packages during nimble develop --- .travis.yml | 2 +- karax/tools/static_server.nim | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6e03ea9..2f26a5a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,5 +16,5 @@ before_script: - set -e - export PATH=$(pwd)/nim/bin:$(pwd):$PATH script: - - nimble develop + - nimble develop -y - nim c -r tests/tester.nim diff --git a/karax/tools/static_server.nim b/karax/tools/static_server.nim index 40375a9..5838777 100644 --- a/karax/tools/static_server.nim +++ b/karax/tools/static_server.nim @@ -33,7 +33,7 @@ proc sendStaticIfExists( result = Http200 let mimes = newMimetypes() for p in paths: - if existsFile(p): + if fileExists(p): var fp = getFilePermissions(p) if not fp.contains(fpOthersRead): @@ -107,7 +107,7 @@ proc handleFileRequest( let pathDir = path.splitFile.dir / "" if pathDir.startsWith(publicUrl): - if existsDir(path): + if dirExists(path): status = await sendStaticIfExists( req, @[path / "index.html", path / "index.htm"] @@ -167,4 +167,4 @@ proc serveStatic*() = waitFor server.serve(Port(8080), cb) when isMainModule: - serveStatic() \ No newline at end of file + serveStatic() From 9a627985d0971d7e129dc0a791b42c4194d5ea0d Mon Sep 17 00:00:00 2001 From: Araq Date: Tue, 8 Dec 2020 18:02:59 +0100 Subject: [PATCH 19/31] added a getInputText API, cleaner and more robust than the .text attribute which is often updated too late in event handlers after a redraw --- examples/toychat.nim | 2 +- karax.nimble | 2 +- karax/karax.nim | 4 ++++ readme.rst | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/toychat.nim b/examples/toychat.nim index 600998c..11ec7a4 100644 --- a/examples/toychat.nim +++ b/examples/toychat.nim @@ -11,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 = diff --git a/karax.nimble b/karax.nimble index 9e29495..7d95304 100644 --- a/karax.nimble +++ b/karax.nimble @@ -1,6 +1,6 @@ # Package -version = "1.1.3" +version = "1.2.0" author = "Andreas Rumpf" description = "Karax is a framework for developing single page applications in Nim." license = "MIT" diff --git a/karax/karax.nim b/karax/karax.nim index c32e969..5ec4f5d 100644 --- a/karax/karax.nim +++ b/karax/karax.nim @@ -809,6 +809,10 @@ 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)) diff --git a/readme.rst b/readme.rst index 6099bd8..f515667 100644 --- a/readme.rst +++ b/readme.rst @@ -250,8 +250,8 @@ captures the ``field`` parameter: proc validateNotEmpty(field: kstring): proc () = result = proc () = - let x = getVNodeById(field) - if x.text.isNil or x.text == "": + let x = getVNodeById(field).getInputText + if x.isNil or x == "": errors.setError(field, field & " must not be empty") else: errors.setError(field, "") From 4c06cb3204bfc89dbbdd1b4d20ca888bd61aeaa1 Mon Sep 17 00:00:00 2001 From: Araq Date: Tue, 8 Dec 2020 18:03:14 +0100 Subject: [PATCH 20/31] 1.2.1 --- karax.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/karax.nimble b/karax.nimble index 7d95304..8712a6d 100644 --- a/karax.nimble +++ b/karax.nimble @@ -1,6 +1,6 @@ # Package -version = "1.2.0" +version = "1.2.1" author = "Andreas Rumpf" description = "Karax is a framework for developing single page applications in Nim." license = "MIT" From 6cff2d8b05793ed245964b3c233e1d39742fd3cb Mon Sep 17 00:00:00 2001 From: Antonis Geralis <43617260+planetis-m@users.noreply.github.com> Date: Fri, 8 Jan 2021 18:07:19 +0200 Subject: [PATCH 21/31] Refactor static_server (#169) --- karax/tools/static_server.nim | 131 +++++++++++++--------------------- 1 file changed, 51 insertions(+), 80 deletions(-) diff --git a/karax/tools/static_server.nim b/karax/tools/static_server.nim index 5838777..eeddf4f 100644 --- a/karax/tools/static_server.nim +++ b/karax/tools/static_server.nim @@ -1,71 +1,63 @@ -import net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5, - logging, httpcore, asyncfile, options -import asynchttpserver +import + std/[net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5, + logging, httpcore, asyncfile, asynchttpserver, tables, times] from cgi import decodeUrl -import ws,tables,times -import dotenv +import ws, dotenv -type +var logger = newConsoleLogger() +addHandler(logger) + +when defined(release): + setLogFilter(lvlError) + +type RawHeaders* = seq[tuple[key, val: string]] -proc toStr(headers: Option[RawHeaders]): string = - return $newHttpHeaders(headers.get(@({:}))) +proc toStr(headers: RawHeaders): string = + $newHttpHeaders(headers) -proc send( - request: Request, code: HttpCode, headers: Option[RawHeaders], body: string -): Future[void] = - return request.respond( - code, body, newHttpHeaders(headers.get(@({:}))) - ) +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: Option[RawHeaders]): Future[void] = + headers: RawHeaders): Future[void] = try: result = send(request, status, headers, content) - when not defined(release): - logging.debug(" $1 $2" % [$status, toStr(headers)]) + debug(" ", status, " ", toStr(headers)) except: - logging.error("Could not send response: $1" % osErrorMsg(osLastError())) + error("Could not send response: ", osErrorMsg(osLastError())) -proc sendStaticIfExists( - req: Request, paths: seq[string] -): Future[HttpCode] {.async.} = +proc sendStaticIfExists(req: Request, paths: seq[string]): Future[HttpCode] {.async.} = result = Http200 let mimes = newMimetypes() for p in paths: if fileExists(p): - - var fp = getFilePermissions(p) - if not fp.contains(fpOthersRead): + if fpOthersRead notin getFilePermissions(p): return Http403 - let fileSize = getFileSize(p) - let ext = p.splitFile.ext + let extPos = searchExtPos(p) let mimetype = mimes.getMimetype( - if ext.len > 0: ext[1 .. ^1] - else: "" - ) + 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.hasKey("If-None-Match") and req.headers["If-None-Match"] == hashed: - await req.statusContent(Http304, "", none[RawHeaders]()) + if req.headers.getOrDefault("If-None-Match") == hashed: + await req.statusContent(Http304, "", default(RawHeaders)) else: - await req.statusContent(Http200, file, some(@({ + await req.statusContent(Http200, file, @{ "Content-Type": mimetype, "ETag": hashed - }))) + }) else: - let headers = @({ + let headers = @{ "Content-Type": mimetype, "Content-Length": $fileSize - }) - await req.statusContent(Http200, "", some(headers)) - + } + await req.statusContent(Http200, "", headers) var fileStream = newFutureStream[string]("sendStaticIfExists") var file = openAsync(p, fmRead) # Let `readToStream` write file data into fileStream in the @@ -80,38 +72,19 @@ proc sendStaticIfExists( else: break file.close() - - return - + return # If we get to here then no match could be found. return Http404 - -proc handleFileRequest( - req: Request -): Future[HttpCode] {.async.} = +proc handleFileRequest(req: Request): Future[HttpCode] {.async.} = # Find static file. var reqPath = cgi.decodeUrl(req.url.path) - var publicUrl = getEnv("publicUrl") - var staticDir = getEnv("staticDir") - if not publicUrl.endsWith("/"): - publicUrl = publicUrl & "/" - reqPath = reqPath.substr(publicUrl.len) - - let path = normalizedPath( - staticDir / reqPath - ) - - # Verify that this isn't outside our static dir. + var staticDir = getEnv("staticDir") # it's assumed a relative dir var status = Http400 - let pathDir = path.splitFile.dir / "" - - if pathDir.startsWith(publicUrl): - if dirExists(path): - status = await sendStaticIfExists( - req, - @[path / "index.html", path / "index.htm"] - ) + 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 @@ -119,10 +92,12 @@ proc handleFileRequest( 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"] + 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 @@ -134,35 +109,31 @@ proc handleWs(req: Request) {.async.} = await ws.send("refresh") changed = false - -proc serveStatic*() = - if fileExists( "static.env" ): - var env:DotEnv +proc serveStatic*() = + if fileExists("static.env"): + var env: DotEnv env = initDotEnv(getCurrentDir(), "static.env") env.overload() else: - loadEnvFromString(""" - staticDir="./src/assets/" - publicUrl="public" - """) - + 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" ) + await req.respond(Http200, readFile "app.html") elif req.url.path == "/app.js": let file = absolutePath("app" & ".js") if not file.fileExists: - logging.error("$1 not exists!" % file ) + error(file, " does not exist!") if fpUserRead notin os.getFilePermissions(file): - logging.error("Could not read $1!" % file ) - await req.respond(Http200, readFile( 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,"") + await req.respond(status, "") waitFor server.serve(Port(8080), cb) From 32de202845762607a55332c2d138aca1327bf37b Mon Sep 17 00:00:00 2001 From: Double_oxygeN Date: Mon, 25 Jan 2021 15:51:24 +0900 Subject: [PATCH 22/31] add picture tag as a kind of virtual DOM (#174) --- karax/vdom.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/karax/vdom.nim b/karax/vdom.nim index 6668d10..2016ca3 100644 --- a/karax/vdom.nim +++ b/karax/vdom.nim @@ -34,7 +34,7 @@ type kdb, sub, sup, italic = "i", bold = "b", underlined = "u", mark, ruby, rt, rp, bdi, dbo, span, br, wbr, ins, del, img, iframe, embed, `object` = "object", - param, video, audio, source, track, canvas, map, + param, video, audio, picture, source, track, canvas, map, area, svg, math, path, circle table, caption, colgroup, col, tbody, thead, From 0fca6e961d69a7bb073baf10f2c27d31522abb59 Mon Sep 17 00:00:00 2001 From: Antonis Geralis <43617260+planetis-m@users.noreply.github.com> Date: Thu, 4 Feb 2021 20:24:19 +0200 Subject: [PATCH 23/31] add all svg elements acording to the standard (#178) --- karax/vdom.nim | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/karax/vdom.nim b/karax/vdom.nim index 2016ca3..8fd654e 100644 --- a/karax/vdom.nim +++ b/karax/vdom.nim @@ -34,8 +34,20 @@ type kdb, sub, sup, italic = "i", bold = "b", underlined = "u", mark, ruby, rt, rp, bdi, dbo, span, br, wbr, ins, del, img, iframe, embed, `object` = "object", - param, video, audio, picture, source, track, canvas, map, - area, svg, math, path, circle + param, video, audio, source, track, canvas, map, + 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, @@ -101,9 +113,9 @@ type 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): From 8a7f05ac2a953f0d390efdcf9f045b3068cf8d60 Mon Sep 17 00:00:00 2001 From: flywind <43030857+xflywind@users.noreply.github.com> Date: Tue, 9 Feb 2021 04:55:46 -0600 Subject: [PATCH 24/31] Update readme.rst (#184) --- readme.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.rst b/readme.rst index f515667..8e1651b 100644 --- a/readme.rst +++ b/readme.rst @@ -86,7 +86,7 @@ construction so that it composes with all of Nim's control flow constructs: proc createDom(): VNode = result = buildHtml(tdiv): - if random(100) <= 50: + if rand(100) <= 50: text "Hello World!" else: text "Hello Universe" @@ -100,7 +100,7 @@ Produces: .. code-block:: nim let tmp1 = tree(VNodeKind.tdiv) - if random(100) <= 50: + if rand(100) <= 50: add(tmp1, text "Hello World!") else: add(tmp1, text "Hello Universe") From 12b98b2338a5cfb11f89e4fdba7e6b7b65038549 Mon Sep 17 00:00:00 2001 From: Matt Haggard Date: Wed, 31 Mar 2021 05:43:43 -0400 Subject: [PATCH 25/31] Make 'karun -w -r ...' do watching for more than one iteration (#190) --- karax/tools/karun.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/karax/tools/karun.nim b/karax/tools/karun.nim index b29cebe..adf1ecb 100644 --- a/karax/tools/karun.nim +++ b/karax/tools/karun.nim @@ -59,7 +59,7 @@ proc build(rest: string, selectedCss: string, run: bool, watch: bool) = else: exec cmd let dest = "app" & ".html" - let script = if run and watch: websocket else: "" + let script = if watch: websocket else: "" writeFile(dest, html % ["app", selectedCss, script]) if run: openDefaultBrowser("http://localhost:8080") From cedd39a65fe3a03ef07474f00c55cd5171ace6ea Mon Sep 17 00:00:00 2001 From: Arham Jain <7073362+ajusa@users.noreply.github.com> Date: Mon, 3 May 2021 04:26:15 -0400 Subject: [PATCH 26/31] Remove kout deprecation (#193) --- karax/karax.nim | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/karax/karax.nim b/karax/karax.nim index 5ec4f5d..bae82d2 100644 --- a/karax/karax.nim +++ b/karax/karax.nim @@ -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 From df828e145da578a00be4b78e9689f51a5f8f92d4 Mon Sep 17 00:00:00 2001 From: Willyboar Date: Thu, 6 May 2021 18:57:08 +0300 Subject: [PATCH 27/31] beautify README (#194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * beautify README * Fix small issues * Revert "Fix small issues" This reverts commit 2a480addd7e2a55faaa95dd2145a6dc0aa4b95f7. * Small Fixes vol.2 * Fix licence link Co-authored-by: codewski Co-authored-by: ΝΙΚΟΣ --- readme.md | 394 +++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.rst | 382 --------------------------------------------------- 2 files changed, 394 insertions(+), 382 deletions(-) create mode 100644 readme.md delete mode 100644 readme.rst diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..affdd05 --- /dev/null +++ b/readme.md @@ -0,0 +1,394 @@ +![karax](https://user-images.githubusercontent.com/22755228/117183486-482b2a00-ade0-11eb-88e6-d8eeb28951ca.png) + +![Travis (.com) branch](https://img.shields.io/travis/com/karaxnim/karax/master?style=for-the-badge) ![GitHub issues](https://img.shields.io/github/issues-raw/karaxnim/karax?style=for-the-badge) ![GitHub](https://img.shields.io/github/license/karaxnim/karax?style=for-the-badge) ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/karaxnim/karax?sort=semver&style=for-the-badge) ![https://nim-lang.org](https://img.shields.io/badge/nim-powered-ffc200?style=for-the-badge) + +# 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 ``
`` 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 +``` + +## Licence +MIT Licence. See [here](https://github.com/karaxnim/karax/blob/master/LICENSE.txt). \ No newline at end of file diff --git a/readme.rst b/readme.rst deleted file mode 100644 index 8e1651b..0000000 --- a/readme.rst +++ /dev/null @@ -1,382 +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 ``
`` 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 rand(100) <= 50: - text "Hello World!" - else: - text "Hello Universe" - - randomize() - setRenderer createDom - - -Produces: - -.. code-block:: 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: - -.. 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).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: - - -.. 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 `_.) - -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 `_.) - - -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() - -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: - -.. code-block:: 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 From ac210fb634bfe006c52c70e8ab5999d8d6de4ee0 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 6 May 2021 17:57:48 +0200 Subject: [PATCH 28/31] Update readme.md --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index affdd05..f4f2fa1 100644 --- a/readme.md +++ b/readme.md @@ -390,5 +390,5 @@ let option: text name ``` -## Licence -MIT Licence. See [here](https://github.com/karaxnim/karax/blob/master/LICENSE.txt). \ No newline at end of file +## License +MIT License. See [here](https://github.com/karaxnim/karax/blob/master/LICENSE.txt). From 93d9b73867111ba9ea2e3a899e28c24c92a26d24 Mon Sep 17 00:00:00 2001 From: Arham Jain <7073362+ajusa@users.noreply.github.com> Date: Thu, 6 May 2021 16:56:55 -0400 Subject: [PATCH 29/31] Moved CI from Travis to Github Actions (#198) * Create ci.yml * removed travis build file * changed docker image * Update readme to include CI results --- .github/workflows/ci.yml | 31 +++++++++++++++++++++++++++++++ .travis.yml | 20 -------------------- readme.md | 2 +- 3 files changed, 32 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5e6c415 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2f26a5a..0000000 --- a/.travis.yml +++ /dev/null @@ -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 -y - - nim c -r tests/tester.nim diff --git a/readme.md b/readme.md index f4f2fa1..d8a3814 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ ![karax](https://user-images.githubusercontent.com/22755228/117183486-482b2a00-ade0-11eb-88e6-d8eeb28951ca.png) -![Travis (.com) branch](https://img.shields.io/travis/com/karaxnim/karax/master?style=for-the-badge) ![GitHub issues](https://img.shields.io/github/issues-raw/karaxnim/karax?style=for-the-badge) ![GitHub](https://img.shields.io/github/license/karaxnim/karax?style=for-the-badge) ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/karaxnim/karax?sort=semver&style=for-the-badge) ![https://nim-lang.org](https://img.shields.io/badge/nim-powered-ffc200?style=for-the-badge) +![Github Actions](https://img.shields.io/github/workflow/status/karaxnim/karax/CI?style=for-the-badge) ![GitHub issues](https://img.shields.io/github/issues-raw/karaxnim/karax?style=for-the-badge) ![GitHub](https://img.shields.io/github/license/karaxnim/karax?style=for-the-badge) ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/karaxnim/karax?sort=semver&style=for-the-badge) ![https://nim-lang.org](https://img.shields.io/badge/nim-powered-ffc200?style=for-the-badge) # Karax Karax is a framework for developing single page applications in Nim. From 6d51d7d08c4a4072ed1c14e28483d919f8182378 Mon Sep 17 00:00:00 2001 From: geotre Date: Sat, 15 May 2021 20:22:33 +0100 Subject: [PATCH 30/31] Fix issue #192 (#197) --- karax/karax.nim | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/karax/karax.nim b/karax/karax.nim index bae82d2..3b81262 100644 --- a/karax/karax.nim +++ b/karax/karax.nim @@ -37,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] @@ -629,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 @@ -661,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: From 85c81d8c0e54622293d8133ccee643e6056541e7 Mon Sep 17 00:00:00 2001 From: geotre Date: Sat, 15 May 2021 20:22:56 +0100 Subject: [PATCH 31/31] Fix issue #195 (#196) --- karax/karax.nim | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/karax/karax.nim b/karax/karax.nim index 3b81262..2246676 100644 --- a/karax/karax.nim +++ b/karax/karax.nim @@ -800,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.} =