umbrella
umbrella copied to clipboard
[umbrella/hdom] - innerHTML as attrib?
in React one can set the innerHTML of a component from a string through dangerouslySetInnerHTML.__html
on a component's props. Is there a way to do this in hdom?
Example:
["div", {__html: "abc"}]
renders => <div>abc</div>
So far this was purposely excluded, largely for these two reasons:
-
it can (most likely will) mess with the diff, which in hdom never tracks the real DOM and so will go out of sync if the element will be updated later, e.g. by another component
-
for security / XSS considerations (example)
What's your use case for this?
I'm exploring ways to decouple content structure from specific renderers or for static, html-transpilers, e.g. hdom/hiccup, jsx.
Here's an example dispatches items in a DAG on a defined type (this could also be a component name), like
{ id: 1, type: "nav", content: { brand: 2, items: 3 } }
It renders a default component for types that have no specific renderers and also shows how two different renderers, here hiccup
and preact/htm
can be used and share styles via a shared context (ctx
).
If you want to try it out, scroll down to Test Run
and follow the instructions: https://beta.observablehq.com/d/7a6a1e07b79f2365
I'll still have to study this example of yours more closely (a bit complex to follow), but maybe something like this wrapper component could work?
https://github.com/thi-ng/umbrella/blob/develop/examples/hdom-inner-html/src/index.ts
Live demo here: http://demo.thi.ng/umbrella/hdom-inner-html/
That should work. I’m actually doing something similar already, just without the “diffing”. Thanks!! On Tue, Jan 15, 2019 at 22:02 Karsten Schmidt [email protected] wrote:
I'll still have to study this example of yours more closely (a bit complex to follow), but maybe something like this wrapper component could work?
https://github.com/thi-ng/umbrella/blob/develop/examples/hdom-inner-html/src/index.ts
Live demo here: http://demo.thi.ng/umbrella/hdom-inner-html/
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/thi-ng/umbrella/issues/67#issuecomment-454634308, or mute the thread https://github.com/notifications/unsubscribe-auth/AEb7IvB4vEOCRWiBIuQO-9FUL9BCaa1aks5vDpZIgaJpZM4Z9aap .
Sorry, @den1k - didn't notice this in your code earlier, but same wavelength! :) I think with the manual cleanup of children in release()
, this should work and not lead to DOM corruption should the wrapper component be replaced in the DOM at a later time...
Ps. I also think this is a cleaner & more lightweight approach than adding support for innerHTML
in hdom directly (which would require lots of additional work)...
Had an idea in a similar vein. What if hdom inlined html elements in its hiccup data? e.g.
const div = document.createElement("div")
div.innerHTML = "hello"
// hiccup
[
"div",
div,
"world"
]
This way hiccup can be used to determine the position inlined elements, for example if one wants to wrap a html node, one would need to make another component with init method in hdom or use id
attrs.
The nodes could still be annotated with paths, but I assume diffing would not be possible. However, since the Elements are known, enough information is available for automatic render
& release
.
Further, since functions are allowed, this would enable parameterization of various other renderers, like d3, preact etc. Example:
const d3Viz = (ctx, {width, height}) => {
ctx.state.deref()...
return element // returns html element
}
// hiccup
[
"div",
["h1", "Rendering d3"],
[d3Viz, {width: 200, height: 200}]
]
On the next render, the node itself could be passed to the the function, maybe annotating ctx
or on this.element
.
Not sure this is in the scope of the project. But if it could be, it doesn't seem like it would be too complicated to add. Besides, I find thinking of hdom
as a general purpose rendering engine, rather than a vdom impl for hiccup very exciting!
@den1k after some reading & experimenting it seems we could use the .isEqualNode()
method of DOM nodes to check for equality, but I will have to go over the hdom diffElement
code again to see how & where this would have to be integrated.
a = document.createElement("div")
a.setAttribute("id", "foo")
b = document.createElement("div")
b.setAttribute("id", "foo")
a.isEqualNode(b)
// true
The other place this will have to be taken care of is in the createTree()
& hydrateTree()
functions of the hdom API. And for hydrateTree()
, my current thinking is that it won't be possible to support embedded DOM nodes, since it assumes the existing browser DOM already is a 99% reflection of the hiccup tree and hydrateTree()
is only adding missing event listeners, but performs no diffing nor any other DOM changes... so food for thought...
@postspectacular thanks for looking into it! Lot's of food for thought here, indeed!
So it looks like isEqualNode
would enable an immutable diffing approach where functions always return a new element
.
But how would this work with rendered nodes? Since libs like d3
save data on the node itself and have their own lifecycle methods that mutate the dom (in this case local to the node) on updates.
I think what could work here is a .isSameNode()
method. So the "diffing" would runisEqualNode
and isSameNode
replace the node when both are false. Besides, node diffing, hiccup would still be in charge of path/position/identity, for example it might need to move a node in the dom when it shifts to a different index due to other elements being added or removed. Some ideas:
const d3viz = (ctx, props) => {
// some way to get the element if already rendered or create a new one.
const el = ctx.element || document.createElement('div')
<d3 magic...>
return el
}
// hiccup
[
"div",
// possibly dynamic (variable length). diffing will need to preserve d3viz's path/dom node across updates
// I assume this already works?
...[other, comps],
dataReady ? [d3viz] : "Loading..."
]
const shouldRerender = (previousNode, node) =>
!node.isEqualNode(previousNode) && !node.isSameNode(previousNode)
-
Initial render
previousNode
-ctx.element
isnull
, sod3viz
will return a newnode
, in codeshouldRerender(null, node)
is true: render! -
Re-render
d3viz
is passednode
from (1.) and possibly mutates it (a) or returns a new node (b), in code a.shouldRerender(previousNode, node)
returns false because previousNode and node are equal: hdom does nothing. b.d3viz
returned a new node that is not the same as previousNode. hdom replaces previousNode with node.
Does this look sound to you?
One thing I'm confused about is how hdom would manage the position of the node in the tree hierarchy. Say sibling elements appear/disappear throughout renders, will hdom be able to keep track of the previously returned node of d3viz
and pass it back to it?
Agreed, it doesn't make sense for hydrateTree()
to support embedded dom nodes.
Node position tracking in hdom is indirectly handled via the key
attribs injected in each hiccup node in normalizeTree()
. Reconciliation of equivalent, but moved elements is done via the linear edit log produced by the diffArray
and extractEquivElements
functions (both called from diffTree
) . Off the top of my head I can't think right now of a good solution how to do this with already existing DOM nodes (apart from adding an additional check for DOM nodes in normalizeTree
and setting the key
attrib there already, but then further checks for the special values are needed in diffTree
(in addition to the other places mentioned previously).
Another thing to consider as alternative solution: Use a standard hiccup wrapper node w/ __impl
control attribute to provide a custom branch-local partial implementation of the hiccup API to delegate dealing with these special cases, e.g.:
import { HDOMImplementation, DEFAULT_IMPL } from "@thi.ng/hdom";
const MyCustomHdom: HDOMImplementation = {
... DEFAULT_IMPL,
diffTree: () => ...,
normalizeTree: () => ...
};
// wrapper component w/ branch-local custom hdom implementation
const embed = (child: Element) =>
["div",
{
__impl: MyCustomHdom,
__normalize: false
},
child];
const fooviz = document.createElement("div");
fooviz.innerHTML = ...yadayada...
// then usage in the hiccup tree
["div", ["h1", "visualize!"], [embed, fooviz]]
Yes, this would require a wrapper like this, but OTOH, it would isolate all the extra stuff needed in these wrapped branches. Altogether, I think this would be my overall preferred solution, be non-intrusive, would allow distribution & maintenance of this feature as separate add-on/support package and is also the mechanism used by @thi.ng/hdom-canvas & @thi.ng/hdom-mock. In fact, I abstracted and added IOC support to hdom's API for exactly such special case behaviors & custom operations:
hdom-canvas: https://github.com/thi-ng/umbrella/blob/master/packages/hdom-canvas/src/index.ts#L71
hdom-mock: https://github.com/thi-ng/umbrella/tree/master/packages/hdom-mock
(...and soon another package for webgl, currently still very, very alpha...)
This all is what I meant with "needs a lot of additional work" in one of my earlier replies... And, to be honest, I won't have the bandwidth to deal with this anytime soon myself - there're just a lot more pressing things for me to work on right now (e.g. stuff from #23, getting the refactored vector, matrix, geom, color and memory pool packages ready for initial release, write/update docs, doc system, work on website, finish long overdue blog posts etc.)...
So if you feel like taking this on, please do! I'd recommend creating a new example project, copy the hdom sources in that project, create a custom hdom API implementation and start by overriding some of the default methods. then create a MVP of these changes. Doesn't need to use d3 to start with. But also, please always consider that this single use case should not cause a massive overall performance drop (due to all these additional checks/special case behaviors needed). In addition to updating hdom itself, there's also compatibility with thi.ng/hiccup string serialization to consider. Thus far, hdom trees have been fully compatible with hiccup and I'd really like to keep it this way. So how would embedded DOM trees be handled? Just skip these nodes when serializing to string? Or use their innerHTML body?
This constantly changing tool-of-the-month behavior of the JS community makes me feel like a complete beginner again.
😂😂😂build tooling in JS...
Having a custom pluggable impl is a great option to have, especially when it negatively impacts performance. Not sure I have the chops to impl this but I'll give it a few hours now. Options around serialization should probably ship with sensible defaults that can be overwritten by the user.
thanks @postspectacular
@postspectacular unfortunately I didn't get very far.
Getting RangeError: Maximum call stack size exceeded
when providing the hdom.DEFAULT_IMPL
as __impl
https://beta.observablehq.com/d/9bd5f0527f26fe79
w00t! will have to take a look at this later tonight & will report back! thanks for letting me know...