HapticX / happyx

Macro-oriented asynchronous web-framework written in Nim with ♥
https://hapticx.github.io/happyx/
MIT License
494 stars 17 forks source link

implement proper thunks that construct their part of the vdom after the parent vdom has been created. #313

Closed quimt closed 1 month ago

quimt commented 1 month ago

Is your feature request related to a problem? Please describe. 🤔 There has been some struggle with loops and deferred TagRef generation. In #303 I said I'd look back at how Karax handles thunks,. Recall everyone that the general definition of thunk is "a delayed computation", and in Scheme, it means more specifically the functional programming way of doing this by passing around a proc (): whatever ` Nim takes inspiration from multiple sources, but part of the interest in macros comes from Lisp, of which Scheme is one dialect.

Describe the solution you'd like 💡

The implementation in Karax seems solid. The core logic is to delay the rendering of a thunk by attaching a placeholder TagRef to the vdom, then when rendering, calling the thunk and modifying the vdom. In proc toDom:

  elif n.kind == VNodeKind.vthunk:
    let x = callThunk(vcomponents[n.text], n)
    result = toDom(x, useAttachedNode, kxi)
    #n.key = result.key
    attach n
    return result

Components are handled in a similar way since that's the normal way of thinking about how html tags themselves are rendered to the dom. First the parent element is placed then the children are attached. The process is recursive and top-down.


  elif n.kind == VNodeKind.component:
    let x = VComponent(n)
    if x.onAttachImpl != nil: x.onAttachImpl(x)
    assert x.renderImpl != nil
    if x.expanded == nil:
      x.expanded = x.renderImpl(x)
      #  x.updatedImpl(x, nil)
    assert x.expanded != nil
    result = toDom(x.expanded, useAttachedNode, kxi)
    attach n
    return result

Not that the definition of VNode as a variant object helps with handling these different kinds of rendering behavior. This strategy would also allow diffreent types of TagRef in Happyx to have different data. I would conceptualize a HappyX thunk as a kind of TagRef (so that it can attach to the vdom) that carries a pure function thunk as a field. In other words, something like


type TagRef = ref object
   case kind: TagRefKind
     ##....
     of TagRefKind.thunk:
        thunkProc: proc (): TagRef
    ##....

Describe alternatives you've considered 💁‍♀️ I think there are some drawbacks to the approach in the most recent patch #303. Global variables tend to break easily and I think some preexisting functionality is already broken. I'm also somewhat concerned that continually tagging deeply nested components with the ID's of their parents will be non-performant in lengthy html documents.

Other less-important context

Karax implements two types of thunk . vthunks delay the computation until the vdom is being rendered to the dom. dthunksput a placeholder in the DOM that calls a proc, thus delaying computation until the dom is being rendered in the browser. Vthunks are not inmplemented in a particularly friendly way for the front end due to how the vthunk proc is parameterized, but that's a superficial issue and the core logic should be fine. dthunks work fine but are annoying to code on the front end since they need to render to nodes in the DOM. Of the two I think thatvthunks are the more important kind of thing to implement. We want to mark some things in the vdom so that they start rendering after the rest of the vdom has been rendered. This would allow things like loops to make many different branches first in a natural way, then start renderding their bodies to the vdom afterward.

Ethosa commented 1 month ago

How about this?

appRoutes "app":
  "/":
    tDiv:
      lazy:  # lazy converts all children into proc(): TagRef
        "Hello, world!"
Ethosa commented 1 month ago

lazy statement produces simple comment node.

Before rendering in HTML prerenderLazyProcs will called. It calls lazyFunc recursively and removes lazy from HTML (comment node)

So, if you print TagRef with lazy statement:

var html = buildHtml:
  "Hello, world!"
  lazy:
    "Bye, world!"

Than output will be:

<div>Hello, world!<!----></div>

But after rendering it will be

<div>
Hello, world!
<div>Bye world!</div>
</div>
quimt commented 1 month ago

That strategy seems fine.

Nomenclature differs from karax' nomenclature and formal computer science, but is more user-friendly. thunkHtml should then map to lazyHtml