Try   HackMD

Tag Variable Hosting (round 3)

So While trying to implement tag variable hoisting, we ran into some conceptual issues. What happens in these cases?

  1. <my-input-range>

    ​​​​<div>${value}</div>
    
    ​​​​<my-input-range/value/>
    
  2. <dims>

    ​​​​<div>
    ​​​​  <dims/{ x, y }/>
    ​​​​</div>
    
    ​​​​<span>
    ​​​​  The div is ${x}px wide ${y}px tall
    ​​​​</span>
    
  3. <carousel> with paddles

    ​​​​<Paddles/>
    
    ​​​​<div>
    ​​​​    <carousel/Paddles>
    ​​​​</div>
    
    ​​​​<Paddles>
    ​​​​    <carousel/Paddles>
    ​​​​</Paddles>
    
    ​​​​<my-dialog focusEl=el>
    ​​​​    <button/el/>
    ​​​​    <log=el/>
    ​​​​    <script>
    ​​​​        el;
    ​​​​    </script>
    ​​​​</>
    
    ​​​​<const/{ focusEl, content }=addDefaults(input)>
    ​​​​<${content}/>
    

Overview

Hoisting becomes a problem when we can create cycles. Marko has the additional challenge that due to the nature of our reactive system (compiled & push-based), we can have cases (<${dynamic}> being the big one) where a hoisted value shouldn't need to be circular but we compile such that input to a tag is grouped together and it is circular.

So we have 4 classes of usage of variables

  1. No hoist
    ​​​ <let/value/>
    ​​​ <my-tag foo=value/>
    
  2. Non-circular hoist
    The value is used outside the scope it is introduced in, or before it in the source order, but its usage does not intersect with the definition in any way.
    ​​​ <if=show>
    ​​​   <let/value/>
    ​​​ </if>
    ​​​ <my-tag foo=value/>
    
    ​​​ <my-tag foo=value/>
    ​​​ <let/value/>
    
  3. Component-circular hoist
    These could be dependency circular, but they could also not be. Depends on the implementation of <my-tag>.
    ​​​ <my-tag foo=value>
    ​​​   <let/value/>
    ​​​ </my-tag>
    
    ​​​ <my-tag/value foo=value>
    
  4. Dependency-circular hoist
    ​​​ <const/foo=value/>
    ​​​ <let/value=foo/>
    

In the case of a circular hoist (definitely 4, probably 3 unless we make some significant changes to the reactive system - switch to pull-based), we have to have some rules in place to prevent the cycle.

There is also the question of where we apply these rule.

  • Only to circular (3/4)
  • or to all hoists (2/3/4)?

Option 0: No Circular Hoisting

A hoisted variable (or values derived from it) cannot be passed to a tag that wraps the variable.

Works:

<my-dialog focus-el=el/>

<some-component>
  <button/el/>
</some-component>

Not Allowed:

<my-dialog focus-el=el>
  <some-component>
    <button/el/>
  </some-component>
</>

Pros:

Cons:

Option 1: Functions only

Restrictions

  • If you return a value and render content, the returned value must be a function
  • A tag variable that's hoisted
    • Must be a function
    • Can't be called/invoked during render phase

If a .marko template renders any DOM nodes, it is only allowed to return functions. That means that <my-input-range> with a return is not possible, and instead the way to accomplish the same goal is with something like

<let/val=0.5/>

<div>${value}</div>

<my-input-range:=val/>

Pros:

  • We can allow double renders at some point in the future

Cons:

  • We need element refs to be functions
  • The <dims> example can't ever be reactive

Option 2: Objects via Proxies

Restrictions

  • If you return a value and render content, the returned value must be an object
  • A tag variable that's hoisted
    • Must be an object
    • Can't be destructured or have properties accessed during render phase
    • Can't be called/invoked during render phase
  • Properties of native element refs can't be read during the render phase

Pros

  • We don't need element refs to be functions?
  • We can allow double renders at some point in the future

Cons

  • Proxies can't be passed to DOM apis, which is a problem for native element refs
  • Devs will probably see proxies all over the place where they don't expect them
  • Proxies are slow

Option 3: Double Render

The hoisted value is initially undefined (but would be defined before the effect phase?). It then re-executes with the actual value.

Pros:

  • No usage limitations
  • Michael's excited about it so he might do it

Cons:

  • How do we double render on the server? Ideally won't send additional code to the browser if
  • Requires null checks on all hoisted values
  • Extra work being done
  • Requires a rethink of the server runtime (probably)
  • Do we ever have to triple render? Quadruple?
  • If a double render happens to redefine the property it will cause an infinite loop. This may be impossible to avoid in places where expressions are grouped together like the dynamic tag.

Addon 1: Explicit Hoisting

Many solutions have differing behavior of hoisted values. Having explicitly hoisted values may make it more clear where this unique behavior is present.

Exposgt a global $hoist function that must wrap any hoisted value.

<some-child>
  <const/value=123/>
</some-child>

<script>
  $hoist(value) // 123
</script>

Addon 2: Explicit Iteration

We've discussed making hoisted, non-primitive values iterable so if a variable is rendered multiple times you can access all values.

However this doesn't support primitive values and may benefit from explicit syntax.

Expose a global $hoistAll function that finds all tag variables with a given name and returns a list

<for|i| until=5>
  <const/value=i/>
</for>

<script>
  $hoistAll(value) // [0, 1, 2, 3, 4]
</script>