# 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>` ```marko <div>${value}</div> <my-input-range/value/> ``` 2. `<dims>` ```marko <div> <dims/{ x, y }/> </div> <span> The div is ${x}px wide ${y}px tall </span> ``` 3. `<carousel>` with paddles ```marko <Paddles/> <div> <carousel/Paddles> </div> ``` ```marko <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 ```marko <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. ```marko <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 ```marko <for|i| until=5> <const/value=i/> </for> <script> $hoistAll(value) // [0, 1, 2, 3, 4] </script> ```