karaxnim / karax Goto Github PK
View Code? Open in Web Editor NEWKarax. Single page applications for Nim.
License: MIT License
Karax. Single page applications for Nim.
License: MIT License
Consider the following code:
include karax/prelude
const maxMessageLength = 10
var buttonEnabled = true
proc renderTweetBox*(): VNode =
buildHtml(tdiv):
textarea():
proc onkeyup(event: Event, n: VNode) =
buttonEnabled =
if n.value.len < maxMessageLength: true
else: false
if buttonEnabled:
button():
text "Tweet"
else:
button(disabled = "disabled"):
text "Tweet"
setRenderer renderTweetBox
It works fine (although nimsuggest raises a warning).
If I rename n
to this
, it stops working.
Compilation should not depend on variable names.
The readme refers to div as a keyword in nim. Is this still the case. Wouldn't using div be better?
Nimble says Karax is MIT licensed, but actually the repo does not contain a license, which probably means that both private and commercial use are not permitted ;).
I wanted to give karax a try on my MacBook Pro with Nim 0.16.0 installed, starting with the very first example on the readme, but in iTerm I got:
$ cd examples/todoapp
$ nim js todoapp.nim
Hint: used config file '/usr/local/Cellar/nim/0.16.0/nim/config/nim.cfg' [Conf]
Hint: system [Processing]
Hint: todoapp [Processing]
todoapp.nim(2, 8) Error: cannot open 'vdom'
This case should be handled, see here https://stackoverflow.com/questions/36416389/why-does-my-xmlhttprequest-have-readystate-4-but-status-0
I have a problem in a larger karax application, which was hard to reproduce, but I think this artificial example should be sufficient:
import vdom, karax, karaxdsl
type
User = object
name: cstring
var users = @[@[User(name: cstring"a")], @[User(name: cstring"b")]]
const MAX = 250
for z in 0..<MAX:
users.add(@[])
for y in 0..<20:
users[^1].add(User(name: cstring("c" & $z)))
var specialLine = -1
proc render: VNode =
result = buildHtml(tdiv):
for i, line in users:
var special = ""
if specialLine == i:
special = "focus"
tdiv(class="line " & special):
for m in line:
span:
text(m.name)
proc ok: VNode =
# render()
result = buildHtml(tdiv):
tdiv:
tdiv:
render()
proc top: VNode =
result = buildHtml(tdiv):
section(id="main"):
ok()
setRenderer(top)
I start this with karun --run example.nim
and stats enabled and the only modifications I have are that instead of if kxi.recursion > 100:stuff
I have if kxi.recursion mod 1000 == 0:kout kxi.recursion
,to more easily monitor the value itself
When I call redraw
, normally it logs ~5000 kxi.recursion which is almost instant.
When I change specialLine in the console, (e.g. to 2), the first redraw after that logs ~1200000 kxi.recursion.
If we test with changing ok: if you just have render()
there, you log ~40000 kxi.recursion, but for each nested tdiv in which render VNode goes, it logs 3 * more recursion.
As a whole, even the 5000 value is weird, because there is if kxi.recursion > 100:
debug log, so I assume that may be karax is not supposed to go even to that number: I guess it has also something to do with the huge number of tags (but 250 * ~20 shoudn't be that much)
There is a small issue with case statements in karaxdsl. For example
include karaxprelude
type
TestEnum {.pure.} = enum
ValA, ValB
var e = TestEnum.ValA
proc renderDom(): VNode =
result = buildHtml():
tdiv():
case e
of TestEnum.ValA:
tdiv: text "A"
of TestEnum.ValB:
tdiv: text "B"
does not compile, because the macro generates an add
call for the case identifier:
let tmp123076 = tree(VNodeKind.tdiv)
case add(tmp123076, e)
of TestEnum.ValA:
...
Will try to push a fix in a moment...
/cc @Araq as reported on gitter:
nim js echartstest.nim
open echartstest.html
shows
Customers
Products
hyperlinks but nothing happens when I click them (and no errors are shown).
If I edit echartstest.nim
with
currentView = Customers
=>
currentView = Products
then a chart is shown (but clicking on the links doesn't work either)
There is a chart instance already initialized on the dom
see https://translate.google.fr/translate?hl=en&sl=zh-CN&u=https://zhidao.baidu.com/question/813945019601723452.html&prev=search for details on that
hello
i want to pass fixed arguments to dom event callback.
i hope it would like this
for option in self.product.options:
button(onclick=onDelClick("A3333", ev, node)):
"A3333" is fixed.
do you have any suggestion?
CSS white-space https://developer.mozilla.org/en/docs/Web/CSS/white-space property is required at least to disable wrapping of inline elements and also to truncate texts with ellipsis https://css-tricks.com/snippets/css/truncate-string-with-ellipsis/.
<div>
<div style="display: inline-block;">ACCOUNTS</div>
<div style="display: inline-block;">NEW ROW</div>
<div style="display: inline-block;">INFO</div>
</div>
I thought 0.16.0 was the latest version...
Am I missing something? :)
Tried it anyway, lowered the version requirement, but obviously nothing's compiling
I'm currently dealing with issues which come down to the following behavior. Small example with dynamic deletes from a closure event handler:
include karaxprelude
var elements = @[cstring"A", cstring"B", cstring"C", cstring"D", cstring"E"]
proc renderElement(text: cstring, index: int): VNode =
kout(cstring"re-building closure for", text, index)
proc remove(ev: Event, n: VNode) =
kout(cstring"Deleting element from index: ", index)
elements.delete(index)
result = buildHtml():
tdiv(onClick=remove):
text text
proc buildDom(): VNode =
result = buildHtml():
tdiv:
for index, text in elements.pairs:
renderElement(text, index)
setRenderer buildDom
The initial render shows elements A B C D E. Clicking on C correctly says Deleting element from index: 2 and the sequence becomes A B D E. From the log output I can see that the VNodes + closures should have been updated correctly. However, clicking e.g. again on the item on index 2 (now the D) produces the log message Deleting element from index: 3, resulting in the wrong element to be deleted. Since the index 3 is D's old index, it looks like the event handler hasn't been updated.
I assume the json mod is nim js friendly.
Reproduce using the following as source after compiling to js in the default html template:
include karaxprelude
import json
var jString = "{}"
var jNode = parseJson(jString)
The textarea character "\n" is being ignored when passed through the text() procedure.
It can be inserted by another procedure by using javascript calls but not by setting the variable and having it loaded during the renderDom process.
In short, everything comes out as a single continuous line.
This is very easily tested, but I can generate examples if needed.
It's very easy to call kajax every render and accidentally DoS the server.
# TODO: Surely there is a better way to handle this.
let rankSelect = buildHtml():
if isAdmin:
select(class="form-select", value = $profile.user.rank):
for r in Rank:
option(text $r)
else:
select(class="form-select", value = $profile.user.rank, disabled=""):
for r in Rank:
option(text $r)
There should be an easy way to avoid this repetition.
Currently the ID is hard-coded to "ROOT", which can clash with existing IDs. Ideally the ID should be configurable.
Also, if the ID is not found, the error message could be more helpful (currently fails with a cryptic "x_84928 is null" error).
The graph showing performance comparison should really have some kind of explanation with it. Specifically the colours aren't documented anywhere and makes the whole thing rather confusing..
according to https://developer.mozilla.org/en-US/docs/Web/API/WebSocket , protocol or key is optional,
key = "" or key =nil errors at runtime.
Using &
is a strange choice and is confusing
Karax looks like a cool project and I would like to use it, but there are barely any docs and no getting started guide.
Hi there,
I wanted again to try Karax with Nim 0.17.0 and a "nimble install" at the project root gives me
$ nimble install *[master]
Info Hint: used config file '/etc/nim.cfg' [Conf]
Error: Could not read package info file in /home/vince/bacasable/karax/karax.nimble;
... Reading as ini file failed with:
... Invalid section: .
... Evaluating as NimScript file failed with:
... Error: system module needs 'string'.
If it is not necessary to do that (as discussed in #2), then I have the same errors as discussed here #6 (comment)
meaning with nim js todoapp.nim
:
/home/vince/bacasable/karax/src/components.nim(82, 13) Error: type mismatch: got (NimNode, proc (bl: typed, op: string): NimNode{.noSideEffect.}, void)
but expected one of:
proc add(x: var cstring; y: cstring)
proc add[T](x: var seq[T]; y: openArray[T])
proc add(x: var string; y: cstring)
proc add[T](x: var seq[T]; y: T)
proc add(x: var string; y: string)
proc add(x: var string; y: char)
proc add(parent, kid: VNode)
proc add(c: ClassList; class: cstring)
proc add[A, B](t: var OrderedTable[A, B]; key: A; val: B)
proc add(father, child: NimNode): NimNode
proc add[A, B](t: TableRef[A, B]; key: A; val: B)
proc add(father: NimNode; children: varargs[NimNode]): NimNode
proc add[A, B](t: var Table[A, B]; key: A; val: B)
proc add[A, B](t: OrderedTableRef[A, B]; key: A; val: B)
It happens when you are using a copy of an object with a recursive type in the deep even if the real value is nil
.
For example:
import karax, kajax, vdom, components, karaxdsl
type
Column = object
t: ref Column
Test = object
columns: seq[Column]
var test1 = Test(columns: @[Column(t: nil), Column(t: nil)])
let test2 = test1
kout toJSON(test1)
kout toJSON(test2)
{"columns":[{"t":null},{"t":null}]}
test.js:402 Uncaught TypeError: Converting circular structure to JSON
at JSON.stringify (<anonymous>)
at test.js:402
Of course, it will not work anyway in a case t
will have real value. But user might have an ability to prepare data before marshaling e.g. set all known refs to nil
.
One possible solution is to delete m_type
field in toJson e.g.:
proc toJson*[T](data: T): cstring
{.importc: """
(function some(obj) {
function clear(obj) {
delete obj["m_type"];
for (var i in obj) {
if (obj[i] !== null && typeof(obj[i])=="object") {
clear(obj[i]);
}
}
}
clear(obj);
return JSON.stringify(obj);
})
""".}
I used
function setCookie(cname, cvalue, exdays) {
var exdate = new Date();
exdate.setDate(exdate.getDate() + exdays);
document.cookie = cname + "=" + escape(cvalue) + ((exdays == null) ? "" : ";expires=" + exdate.toGMTString());
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
in the html and import as
proc setCookie(cname:kstring, cvalue:kstring, exdays:int) {.importc.}
proc getCookie(cname:kstring):kstring {.importc.}
or
proc setCookie(cname:kstring, cvalue:kstring, exdays:int) =
if cname == kstring"":
document.cookie = kstring""
else:
document.cookie = cname & "=" & cvalue & ";expires={exdays}"
proc getCookie(cname:kstring):cstring = document.cookie
I can confirm that the event handler update problem (#35) is solved now for me. However it seems that this can trigger problems in the diffing. The problem is not visible in the small example attached to the other issue, but with a slightly more complex VDOM I'm now seeing update times >10 sec and "unresponsive scripts" warnings. The old diffing which did not update the event handlers was very quick.
I had some difficulties to reproduce the problem in smaller examples, so I'm attaching a more complete version. The problem should be reproducible via karun
and clicking on one of the "x" elements.
include karaxprelude
import sequtils, future
type
Model = ref object
entries: seq[cstring]
proc postRenderCallback() =
discard
proc onClick(ev: Event, n: VNode) =
discard
proc onDragStart(ev: Event, n: VNode) =
discard
proc dummy(): VNode =
result = buildHtml():
tdiv()
proc panel(title: string, body: VNode): VNode =
result = buildHtml():
tdiv(class="panel panel-default"):
tdiv(class="panel-heading"):
text title
tdiv(class="panel-collapse"):
tdiv(class="panel-body"):
body
proc threePanels(viewL, viewC, viewR: VNode): VNode =
result = buildHtml():
tdiv(class="row"):
tdiv(class="col-xs-2 sidebar sidebar-left"):
viewL
tdiv(class="col-xs-8 col-xs-offset-2"):
viewC
tdiv(class="col-xs-2 sidebar sidebar-right"):
viewR
proc renderEntry(model: Model, entries: seq[cstring], elementIndex: int): VNode =
proc onRemove(ev: Event, n: VNode) =
model.entries.delete(elementIndex)
result = buildHtml():
span(id=entries[elementIndex], class="some-classes", draggable="true", ondragstart=onDragStart):
span(class="label-float-left"):
text entries[elementIndex]
a(type="button", class="close label-float-right", onclick=onRemove):
text "×"
proc dropzone(model: Model, entries: seq[cstring]): VNode =
result = buildHtml():
tdiv(class="dropzone"):
for elementIndex, entry in entries.pairs():
renderEntry(model, entries, elementIndex)
proc rowElement(model: Model, title: string, entries: seq[cstring], dropzoneId: cstring): VNode =
result = buildHtml():
tdiv(class="row vertical-align"):
tdiv(class="col-xs-2"):
text title
tdiv(class="col-xs-10"):
dropzone(model, entries)
proc viewCenter(model: Model): VNode =
panel("Panel Header"):
flatHtml():
tdiv():
rowElement(model, "Entries", model.entries, "rows".cstring)
tdiv(class="form-group text-right"):
tdiv(class="checkbox"):
label():
input(type="checkbox")
text "Automatic update"
button(type="button", class="btn btn-primary btn-sm", onclick=onClick):
text "Update"
proc view(model: Model): VNode =
result = buildHtml():
tdiv():
tdiv():
text "Navbar here"
tdiv(class="container-fluid below-navbar"):
threePanels(
dummy(),
viewCenter(model),
dummy(),
)
proc runMain() =
var model = Model(
entries: toSeq(1..20).map(x => cstring($x))
)
proc renderer(): VNode =
view(model)
setRenderer(renderer, root="ROOT", clientPostRenderCallback=postRenderCallback)
runMain()
There is a placeholder which changes each time input gets focus. I expect input to be not re-rendered or even not to lose focus because placeholder and input are siblings but it actually has been re-rendered and lost focus.
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
type TextInput* = ref object of VComponent
value: cstring
placeholder: cstring
proc render(x: VComponent): VNode =
let self = TextInput(x)
let style = style(
(StyleAttr.position, cstring"relative"),
(StyleAttr.paddingLeft, cstring"10px"),
(StyleAttr.paddingRight, cstring"5px"),
(StyleAttr.height, cstring"30px"),
(StyleAttr.lineHeight, cstring"30px"),
(StyleAttr.border, cstring"solid 1px black"),
(StyleAttr.fontSize, cstring"12px"),
(StyleAttr.fontWeight, cstring"600")
).merge(self.style)
let inputStyle = style.merge(style(
(StyleAttr.color, cstring"inherit"),
(StyleAttr.fontSize, cstring"inherit"),
(StyleAttr.fontWeight, cstring"inherit"),
(StyleAttr.fontFamily, cstring"inherit"),
(StyleAttr.position, cstring"absolute"),
(StyleAttr.top, cstring"0"),
(StyleAttr.left, cstring"0"),
(StyleAttr.height, cstring"100%"),
(StyleAttr.width, cstring"100%"),
(StyleAttr.border, cstring"none"),
(StyleAttr.backgroundColor, cstring"transparent"),
))
let placeholderStyle = style(
(StyleAttr.opacity, cstring"0.35"),
(StyleAttr.fontStyle, cstring"italic"),
(StyleAttr.overflow, cstring"hidden")
)
proc onfocus(ev: Event; n: VNode) =
self.placeholder = self.placeholder & cstring"1"
kout cstring"focus"
markDirty(self)
proc onblur(ev: Event; n: VNode) =
kout cstring"blur"
result = buildHtml(tdiv(style=style)):
if self.value == nil or len(self.value) == 0:
tdiv(style=placeholderStyle):
text self.placeholder
input(style=inputStyle, value=self.value, onblur=onblur, onfocus=onfocus)
proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""; placeholder: cstring = cstring""): TextInput =
result = newComponent(TextInput, render)
result.style = style
result.value = value
result.placeholder = placeholder
proc createDom(): VNode =
result = buildHtml(tdiv):
newTextInput(placeholder=cstring"test")
setRenderer createDom
Hi:
import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage]
type
Filter = enum
all, active, completed
var
selectedEntry = -1
filter: Filter
entriesLen: int
const
contentSuffix = cstring"content"
completedSuffix = cstring"completed"
lenSuffix = cstring"entriesLen"
proc getEntryContent(pos: int): cstring =
result = getItem(&pos & contentSuffix)
if result == cstring"null":
result = nil
proc isCompleted(pos: int): bool =
var value = getItem(&pos & completedSuffix)
result = value == cstring"true"
proc setEntryContent(pos: int, content: cstring) =
setItem(&pos & contentSuffix, content)
proc markAsCompleted(pos: int, completed: bool) =
setItem(&pos & completedSuffix, &completed)
proc addEntry(content: cstring, completed: bool) =
setEntryContent(entriesLen, content)
markAsCompleted(entriesLen, completed)
inc entriesLen
setItem(lenSuffix, &entriesLen)
proc updateEntry(pos: int, content: cstring, completed: bool) =
setEntryContent(pos, content)
markAsCompleted(pos, completed)
proc onTodoEnter(ev: Event; n: VNode) =
addEntry(n.value, false)
n.value = ""
proc removeHandler(ev: Event; n: VNode) =
updateEntry(n.index, cstring(nil), false)
proc editHandler(ev: Event; n: VNode) =
selectedEntry = n.index
proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
proc editEntry(ev: Event; n: VNode) =
setEntryContent(n.index, n.value)
selectedEntry = -1
proc toggleEntry(ev: Event; n: VNode) =
let id = n.index
markAsCompleted(id, not isCompleted(id))
proc onAllDone(ev: Event; n: VNode) =
clear()
selectedEntry = -1
proc clearCompleted(ev: Event, n: VNode) =
for i in 0..<entriesLen:
if isCompleted(i): setEntryContent(i, nil)
proc toClass(completed: bool): cstring =
(if completed: cstring"completed" else: cstring(nil))
proc selected(v: Filter): cstring =
(if filter == v: cstring"selected" else: cstring(nil))
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compact.} =
result = buildHtml(tr):
li(class=toClass(completed)):
if not selected:
tdiv(class = "view"):
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
onclick=toggleEntry, index=id)
label(onDblClick=editHandler, index=id):
text d
button(class = "destroy", index=id, onclick=removeHandler)
else:
input(class = "edit", name = "title", index=id,
onblur = focusLost,
onkeyupenter = editEntry, value = d, setFocus=true)
proc makeFooter(entriesCount, completedCount: int): VNode {.compact.} =
result = buildHtml(footer(class = "footer")):
span(class = "todo-count"):
strong:
text(&entriesCount)
text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
ul(class = "filters"):
li:
a(class = selected(all), href = "#/"):
text "All"
li:
a(class = selected(active), href = "#/active"):
text "Active"
li:
a(class = selected(completed), href = "#/completed"):
text "Completed"
button(class = "clear-completed", onclick = clearCompleted):
text "Clear completed (" & &completedCount & ")"
proc makeHeader(): VNode {.compact.} =
result = buildHtml(header(class = "header")):
h1:
text "todos"
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
onkeyupenter = onTodoEnter, setFocus)
proc createDom(): VNode =
result = buildHtml(tdiv(class="todomvc-wrapper")):
section(class = "todoapp"):
makeHeader()
section(class = "main"):
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle-all", onclick = onAllDone):
text "Mark all as complete"
text "Testing latin character \243"
var entriesCount = 0
var completedCount = 0
ul(class = "todo-list"):
#for i, d in pairs(entries):
for i in 0..entriesLen-1:
var d0 = getEntryContent(i)
var d1 = isCompleted(i)
if d0 != nil:
let b = case filter
of all: true
of active: not d1
of completed: d1
if b:
createEntry(i, d0, d1, i == selectedEntry)
inc completedCount, ord(d1)
inc entriesCount
makeFooter(entriesCount, completedCount)
setOnHashChange(proc(hash: cstring) =
if hash == "#/": filter = all
elif hash == "#/completed": filter = completed
elif hash == "#/active": filter = active
)
if hasItem(lenSuffix):
entriesLen = parseInt getItem(lenSuffix)
else:
entriesLen = 0
setRenderer createDom
This returns an error on this statement:
text "Testing latin character \243"
URIError: malformed URI sequence
toJSStr
https://github.com/pragmagic/karax/blob/master/experiments/echartstest.nim uses import:
import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
but https://github.com/pragmagic/karax/blob/master/readme.rst uses include:
include karax / prelude
import is cleaner IMO; include should only be used when import doesn't work.
nim js scrollapp.nim
scrollapp.nim(20, 12) Error: undeclared field: 'key'
There seems to be an issue with using the $
macro on enum strings containing unicode values in karax.
You can use karun --run
to run the following code, and there should be a javascript error thrown, which can be seen in the browser developer tools.
include karax / prelude
type
NavType = enum
Home = "Home",
Review = "[漢字 Review]"
var
currentNav = Review
doAssert(currentNav == Review, "Check 1 failed") # OK
doAssert($Review == "[漢字 Review]", "Check 2 failed") # OK
doAssert($currentNav == $Review, "Check 3 failed") # Fails here
proc createDom(): VNode =
result = buildHtml(tdiv()):
text "Current page: " & $currentNav
setRenderer createDom
The assertion failure message is the following, which indicates the unicode characters in the string are in their escaped form, which is why the final assertion fails.
Uncaught Error: Error: unhandled exception: $currentNav == "[\xE6\xBC\xA2\xE5\xAD\x97 Review]" Check 3 failed [AssertionError]
This same test (without the karax content) works fine as a nim c -r
test.
I'm running into some issues using Karax in a project. I have embedded the Monaco editor within my Karax app, and redraw operations fail. The same
proc returns false when it detects the outside changes in the DOM. Is there an existing workaround to this issue? If not, I'm happy to add the functionality myself and submit a PR. All I need is some guidance on the best way to solve this problem. I can also provide more details if needed!
type
clickHandler* = proc():VNode
var view* :clickHandler
proc click*(x : clickHandler): proc() =
result = proc() = view = x
proc echartSection():VNode =
buildHtml tdiv(id = "echartSection", style = style((width, kstring"600px"), (height, kstring"400px")))
proc createDom(data: RouterData): VNode =
let hash = data.hashPart
if hash == cstring"#/Products": currentView = Products
else: currentView = Customers
result = buildHtml(tdiv):
ul(class = "tabs"):
for v in low(Views)..high(Views):
li:
if v == Products:
a(href = "#/" & $v, onclick = click echartSection):
text kstring($v)
else:
a(href = "#/" & $v):
text kstring($v)
tdiv:
text "other section"
setRenderer createDom, "ROOT", postRender
setForeignNodeId "echartSection"
got
Uncaught Error: Initialize failed: invalid dom.
at Object.init (echarts.js:1778)
at Object.post_render_256045 [as postRenderCallback] (echartstest.js:3454)
at dodraw_249631 (echartstest.js:2870)
at HEX3Aanonymous_251001 (echartstest.js:2913)
Consider the following component code (likebutton.nim):
import future
include karax/prelude
var liked = false
proc render*(): VNode =
buildHtml:
if liked:
text "You liked this"
else:
button(onclick = () => (liked = true)):
text "Like"
and the root component (index.nim):
include karax/prelude
from likebutton import nil
proc render(): VNode =
buildHtml(tdiv):
h1:
text "Hello Karax!"
likebutton.render()
when isMainModule:
setRenderer render
karun -r index.nim
fails with index.nim(10, 15) Error: Expected a node of kind nnkIdent, got nnkDotExpr
I'm considering to add a "refs" system to Karax at some point, similar to solutions in React/Vue. It's probably best to write down the idea for now, to see if this fits the general design.
The problem arises when an event handler needs to access arbitrary DOM elements, e.g., you click a button and the onclick
needs to fetch the position of a bunch of divs. Currently I see two ways to implement this:
Use DOM id
tags in combination with getElementById(id)
to fetch the elements. Here is an example of this in my transition groups demo. This has a few drawbacks:
getElementById
repeatedly is significantly slower than just storing a reference once.I have been experimenting with adding a "refs" system on user side:
var vnodeMap = newJDict[cstring, (VNode, VNode)]()
proc registerAs(n: VNode, name: cstring): VNode =
if name in vnodeMap:
# store new candidate node
vnodeMap[name] = (vnodeMap[name][0], n)
else:
vnodeMap[name] = (n, n)
result = n
# Usage in the `buildHtml` vdom generation:
buildsImportantDiv().registerAs("importantDiv")
Storing a tuple of VNode
is necessary, because not every generated VNode
ends up being used. Thus, the tuple stores (old-vnode, candidate-vnode) and on retrieval there is additional logic to move the candidate into the old-vnode position if it has ended up with a non-nil node.dom
. Overall, solving the refs problem on client side is a bit complicated and it would be nice to have some solution in Karax.
I haven't really thought it through yet how to implement it in Karax. Some ideas:
A syntactically naive approach would be:
var
refDivA: Ref = nil # maybe just use VNode?
refDivB: Ref = nil
proc view(): VNode =
result = buildHtml():
tdiv(ref=refDivA): text "A"
tdiv(ref=refDivB): text "B"
Karax would have update the refs on rendering accordingly. But on first glance this approach becomes weird when dealing with a list of elements/references: The view would have to adjust the length of a seq[Ref]
so that for i in 0..<N: tdiv(ref=refs[i])
would not lead to out-of-bound errors when Karax wants to update a reference. So probably that's too error prone and not the kind of responsibility for a view.
An alternative would be to use a callback for ref
with signature Ref -> void
(or VNode -> void
if there is no need for an explicit Ref
type). This would be similar to how it is solved in React. The usage with lists could look like:
var
refs: newJDict[int, Ref]()
proc view(): VNode =
result = buildHtml():
tdiv:
for i in 0..<N:
tdiv(ref = (r: Ref) => refs[i] = r)
This on the other hand raises the question if the callback should rather have both a register and an unregister call, either by having a second argument in the callback or by having two optional callbacks refAdd
and refRemove
. This would enable other things that would fall into the "component lifecycle" of React/Vue, but I'm not sure if Karax needs them.
What do you guys think is best?
Try to click in the input. Observe that border of the input has blinked to red but return back to the black because input has been replaced by markDirty which caused blur event right after the focus.
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
type TextInput* = ref object of VComponent
value: cstring
isActive: bool
proc render(x: VComponent): VNode =
let self = TextInput(x)
let style = style(
(StyleAttr.position, cstring"relative"),
(StyleAttr.paddingLeft, cstring"10px"),
(StyleAttr.paddingRight, cstring"5px"),
(StyleAttr.height, cstring"30px"),
(StyleAttr.lineHeight, cstring"30px"),
(StyleAttr.border, cstring"solid 1px " & (if self.isActive: cstring"red" else: cstring"black")),
(StyleAttr.fontSize, cstring"12px"),
(StyleAttr.fontWeight, cstring"600")
).merge(self.style)
let inputStyle = style.merge(style(
(StyleAttr.color, cstring"inherit"),
(StyleAttr.fontSize, cstring"inherit"),
(StyleAttr.fontWeight, cstring"inherit"),
(StyleAttr.fontFamily, cstring"inherit"),
(StyleAttr.position, cstring"absolute"),
(StyleAttr.top, cstring"0"),
(StyleAttr.left, cstring"0"),
(StyleAttr.height, cstring"100%"),
(StyleAttr.width, cstring"100%"),
(StyleAttr.border, cstring"none"),
(StyleAttr.backgroundColor, cstring"transparent"),
))
proc onfocus(ev: Event; n: VNode) =
if not self.isActive:
kout cstring"focus"
self.isActive = true
markDirty(self)
proc onblur(ev: Event; n: VNode) =
if self.isActive:
kout cstring"blur"
self.isActive = false
markDirty(self)
result = buildHtml(tdiv(style=style)):
input(style=inputStyle, value=self.value, onblur=onblur, onfocus=onfocus)
proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""): TextInput =
result = newComponent(TextInput, render)
result.style = style
result.value = value
proc createDom(): VNode =
result = buildHtml(tdiv):
newTextInput(value=cstring"test")
setRenderer createDom
This example might explain better
include karax / prelude
import future
var page = 1
proc createDom(): VNode = buildHtml(tdiv):
h1:
text "Testing"
if page == 3 or page == 4:
tdiv: text "Some text"
input(`type`="text")
button(onclick=() => (page = 1)): text "1"
button(onclick=() => (page = 2)): text "2"
button(onclick=() => (page = 3)): text "3"
button(onclick=() => (page = 4)): text "4"
if page == 2 or page == 4:
tdiv: text "Some text"
setRenderer createDom
Going from page 1 to page 2 or page 1 to page 3 works as expected. But page 1 to page 4 causes the input and buttons 1, 2 and 3 to be re-rendered.
onchange
is superseeded by oninput
. onchange
is pretty much useless.
I tried new karax with existing code, and it seems setRenderer f leads to errors, as wrapPostRender tries to call nil.
Hi I want to propose following color helpers:
type
RGB = tuple
R: int
G: int
B: int
proc `&`*(x: RGB): cstring =
&x.R & cstring"," & &x.G & cstring"," & &x.B
proc rgb*(rgb: RGB): cstring =
cstring"rgb(" & &rgb & cstring")"
proc rgba*(rgb: RGB, alfa: float): cstring =
cstring"rgba(" & &rgb & cstring"," & &alfa & cstring")"
Usage:
const WhiteColor* = (R: 255, G: 255, B: 255)
tdiv(style=style(StyleAttr.color, rgba(WhiteColor, 0.5)):
text "Test"
This would be a useful feature for my current project.
Seems like a lot of changes would be required to support it though? For a start setRenderer would need to take an element rather than a string for the root dom and the KaraxInstance would also need to contain the rootElement.
I'm not sure what other changes would also need to be made but hopefully it can be done without breaking backwards compatibility.
This really is a neat little project. After looking at some of the examples, the TODO one seems to be the best one, but I'm having a little difficulty of trying to make sense of how this framework... well... works. I understand that this is still in early development and in an experimental phase, but something would be nice.
This code works:
include karax/prelude
const maxMessageLength = 10
var buttonEnabled = true
proc renderTweetBox*(): VNode =
buildHtml(tdiv):
textarea():
proc onkeyup(event: Event, n: VNode) =
buttonEnabled =
if n.value.len < maxMessageLength: true
else: false
if buttonEnabled:
button():
text "Tweet"
else:
button(disabled = "disabled"):
text "Tweet"
setRenderer renderTweetBox
but line 11 gets underlined by nimsuggest in VSCode with the following error:
type mismatch: got <VNode, EventKind, proc (event: Event, n: VNode){.gcsafe, locks: 0.}, KaraxInstance>
but expected one of:
proc addEventHandler(n: VNode; k: EventKind; action: EventHandler;
kxi: KaraxInstance = kxi)
first type mismatch at position: 3
required type: EventHandler
but expression 'proc (event: Event; n: VNode) = buttonEnabled = if len(value(n)) < 10: true else: false' is of type: proc (event: Event, n: VNode){.gcsafe, locks: 0.}
proc addEventHandler(n: VNode; k: EventKind; action: proc (); kxi: KaraxInstance = kxi)
first type mismatch at position: 3
required type: proc (){.closure.}
but expression 'proc (event: Event; n: VNode) = buttonEnabled = if len(value(n)) < 10: true else: false' is of type: proc (event: Event, n: VNode){.gcsafe, locks: 0.}
expression: addEventHandler(tmp179021, onkeyup, proc (event: Event; n: VNode) = buttonEnabled = if len(
value(n)) < 10: true else: false, kxi)
There is no such problem with handlers without arguments.
I would like to propose some enhancements/refactoring to the kdom (mostly event-centric). Ideally these should also be considered for addition to the Nim JS dom as well (in fact, some of these changes are already in the Nim JS dom). The reason I am suggesting considering these enhancements for karax is because karax provides a convenient framework for testing and proving these changes, but I am fine with moving this over to Nim instead if requested.
The need for these changes initially arose from the lack of drag event functionality in the kdom, and noticing the divergence from the standard JavaScript event model, though I understand this was a deliberate design decision and can respect the desire to keep it that way.
NOTE: Some of these changes are breaking changes, in particular removing certain fields from Event that belong to child events (x, y, keyCode, etc.). As far as karax is concerned, this would require one small change: the enterWrapper() proc in karax.nim will need to cast the Event to KeyboardEvent in order to retrieve the keyCode property.
The changes I am proposing are as follows:
Have you considered publishing the jdict and jstrutils modules as separate nimble packages? I think they are very universal and I would like to import them in other projects.
Not sure if this is by design, but having multiple top level elements in buildHtml
does not seem to work:
proc createDom(): VNode =
result = buildHtml():
tdiv():
text "test"
tdiv():
text "test"
Leads to: Error: expression 'tmp94011' is of type 'VNode' and has to be discarded
Wrapping them into one top level element solve the problem.
I made a VComponent which has a child VComponent
when child vcomponent calls
markDirty(self)
redraw()
it doesn't redraw the child component.
I can see the change when the parent component gets update.
Thank you.
Uncaught TypeError: Cannot set property 'renderId' of null
at init_215046
Uncaught TypeError: Cannot read property 'renderer' of null
at dodraw_213631 (todoapp.js:2780)
at HEX3Aanonymous_215049 (todoapp.js:3252)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.