How to add a DOM node in HTML DSL?

#1

Using kotlinx.html DSL on the browser side, how do you add a DOM node to the tree that you’re building? Is there something available on which I can call appendNode(domNode) or something like that? So that domNode becomes a child of myDiv?

val myDiv = document.create.div {
    style = "color:red"
    div { /* nested stuff */ }
    +"Add text like this"
    val domNode: HTMLElement = someComponent.makeDom()
    // How to add `domNode` here?
}
#2

No it is not possible in this way. What we are doing in our project is this:

val myDiv = document.create.div {
    style = "color:red"
    div { /* nested stuff */ }
    +"Add text like this"
    someComponent.render()
}
interface Component {
  fun TagConsumer<*>.render()
}
class CustomFooComponent : Component {
    fun TagConsumer<*>.render() {
        div {
            h4 { 
                +"foo" 
            }
            button { 
                + "Bar"
            }
        }
    }
}
#3

So the someComponent has to rebuild its entire DOM then, even if its state hasn’t changed and it could return an existing DOM node, previously rendered? If there’s no way around that, that would be bad in my situation.

On the bright side though it seems pretty easy to wrap document.createElement, etc., and create your own helper library in the same style as these HTML builders. I may end up doing that.

#4

You would like to cache the results of executing kotlinx-html? I am not sure whether that’s possible. Of course, you could do that yourself. It might be hard to make it feel good though.

#5

Yes, I would like to cache the results. I’m imagining a tree of “UI Component” objects, and each component creates and manages its DOM, and may have children components. If a component needs to re-render it’s part of the DOM, it would ask its child components for their DOMs again. If a child component has no state changes it might just return its existing/cached DOM node.

#6

It is possible to create your own nodes by just following the examples in kotlinx.html. But injecting DOM nodes is not “directly” possible. If you look at https://github.com/Kotlin/kotlinx.html/blob/master/js/src/main/kotlin/dom-js.kt for the JSDomBuilder class you will find that the path property is private, but that would be the property needed to know the current location in the tree. A second problem would be that you need a custom tag class that will hold on to the node until it is inserted. As others implied, kotlinx.html builders first create a parallel structure and then, that gets transformed to html in one shot.

While on the JVM and JS side DOM is used they are not fully compatible (kotlinx.html predates kotlin multiplatform and has some design consequences of that). For the caching thing, why not let the components create dom as well as the container. Instead of the components being injected as part of a single build phase, why not store the elements you attach to and then do a dom injection.

#7

I will try that. I’m a little worried about performance if I have something like a big list or table, but I’m not there yet. I’ll see how it goes. The code looks something like this this now…

typealias RMap = HashMap<Element, Element>
// need something more general than 'DIV' here...
fun DIV.replacementMarker(m: RMap, elt: Element) {
    val mark = consumer.div {  }
    m.put(mark as Element, elt)
}
fun replaceNodes(map: RMap) {
    for ((k, v) in map) {
        k.replaceWith(v)
    }
}
...
val replacements = RMap()
val dom = document.create.div {
    [ ... html blah blah ... ]
    replacementMarker(replacements, subComponent.dom)        
}
replaceNodes(replacements)
return dom
#8

I would go for something like (sorry for any errors):

val dom = document.create.div {
  div { id="loginContainer" }
  // .... some more code
  div { id="contentContainer" }
  // ... etc
}

val loginContainer = dom.getElementById("loginContainer")
val contentContainer = dom.getElementById("contentContainer")

loginContainer.replaceChildrenIfNotNull(newLoginContainerOrNull())
// etc.

You only build the outside once and then replace different bits on the inside. Unfortunately I don’t know of a 100% foolproof way of making the ids automatic or specified only once. (You could try something with objects or otherwise)