karax
karax copied to clipboard
Add support for "refs"
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 withgetElementById(id)
to fetch the elements. Here is an example of this in my transition groups demo. This has a few drawbacks:- Unnecessary pollution of DOM id namespace.
- For a huge DOM, calling
getElementById
repeatedly is significantly slower than just storing a reference once. - List-like elements need to encode their index in the id.
-
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 generatedVNode
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-nilnode.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?
I don't think Karax needs to have weird refs because it currently has VNode.dom
which is much better and generates less boilerplate code.
See my changes which replace getElementById
usage by VNode.dom
:
https://github.com/pragmagic/karadock/commit/2dfd3466ee2f9a2affcbc1c51281c04417d4a184
Also, we have some closure traps related to Karax smart diff algorithm https://gist.github.com/pnuzhdin/ceb35356e88f4f0a865c829d9a05fc97 but I don't think that having VNode
in a closure will lead to an issue because new VNode
value means new render so new closure.
Also, why you think getElementById
depends on the DOM size? It seems it's hashtable so O(1) for a lookup. I've found the following benchmark which shows almost the same result for the manually created JS Object as a hashtable and getElementById
https://jsperf.com/dom-vs-hash-lookup/5 (see getting of textContent benchmark).
The performance is definitely not my biggest concern, but it's a factor of 3 slower on my machine, and that is with a hashmap of the same size as the DOM. If the DOM has thousands of elements, but you only need to store like 5 refs in variables (not a hashmap) the difference is fairly big. But yes, that may only really matter in situations like animations.
Maybe I wasn't clear enough why VNode.dom
alone does not solve this problem nicely. Typically views should not bother about which part of the model has changed, and which parts need to be updated. The main benefit of a vdom is to keep the view logic simple and simply regenerate the entire vdom from scratch on a redraw. Karax than does its job and determines which of these newly generated VNodes
will actually end up in the real DOM. What you do in your first example solves the refs problem but it only works with static elements and is against the vdom idea. What if the resizer
elements becomes model dependent? Would the view logic itself start to do diffing to determine whether it is necessary to create a new VNode
?
It is still possible to solve the refs problem without putting such logic in the view. I have pushed a full example of the registerAs
approach here. The implementation now has to account for the possibility that all the VNodes
generated in the view may or may not end up in the DOM -- that's why the vdom registry has to store a candidate and verified VNode
. This approach still has a few drawbacks:
- The
registerAs
syntax does not work inline, i.e., I have to pull out the elements I want to create in explicitslet element = buildElement().registerAs("myElement")
. - The retrieval logic is ugly.
- Still string / hashmap based.
To me it feels like the client code is doing something which is better solved within Karax.
What if the resizer elements becomes model dependent? Would the view logic itself start to do diffing to determine whether it is necessary to create a new VNode?
Not sure what do you mean by "model dependent" here but resizer
depends on the model, see https://github.com/pragmagic/karadock/commit/2dfd3466ee2f9a2affcbc1c51281c04417d4a184#diff-03227bc30b7f9c03ee14731d4f6b4eaaR493. I don't see an issue if it will be rendered differently e.g. with another style based on other model attributes.
Certainly an interesting possible feature for karax. I'm going to close this for now due to inactivity. Please feel free to re-open and comment if you want to discuss further!