Vize.vapor_split!/1 returns statics in document order but slots in a different order, so PhoenixVapor.Renderer fills each hole with the wrong value. Anything past a trivial template renders wrong markup.
Reproduced on vize 0.10.0, 0.11.1 and 0.14.1, with phoenix_vapor 0.3.0 and with its inertia-inspired-foundations branch. Elixir 1.20.0-rc.6, OTP 28, Linux x86_64.
Repro
render = fn template, assigns ->
template
|> PhoenixVapor.render(assigns)
|> Phoenix.HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
end
render.(~s(<div :class="outer"><i :class="inner"></i></div>), %{outer: "OUTER", inner: "INNER"})
# got: <div class="INNER"><i class="OUTER"></i></div>
# expected: <div class="OUTER"><i class="INNER"></i></div>
render.(~s(<div><b v-if="on">Y</b><span>{{ label }}</span></div>), %{on: true, label: "L"})
# got: <div><span>L</span><b>Y</b></div>
# expected: <div><b>Y</b><span>L</span></div>
render.(~s(<div><section><b v-if="on">Y</b></section><span>{{ label }}</span></div>), %{on: true, label: "L"})
# got: <div><section>L</section><span><b>Y</b></span></div>
# expected: <div><section><b>Y</b></section><span>L</span></div>
The last one changes the tree, not just the values.
Where the order comes from
vapor_ir!/1 is correct — its effects come in document order and carry the element id and the attribute name:
Vize.vapor_ir!(~s(<a :class="l1"><b :class="l2"><c :class="l3"></c></b></a>))
# effects, in order: {"a", element: 1, "l1"}, {"b", element: 0, "l2"}, {"c", element: 2, "l3"}
Vize.vapor_split!(~s(<a :class="l1"><b :class="l2"><c :class="l3"></c></b></a>)).slots
# [["l2"], ["l1"], ["l3"]]
The slots come out sorted by element id, and the ids are not assigned in document order. statics stay in document order, so the two lists disagree whenever the ids do not happen to ascend with the document.
v-if and v-for holes are appended after the attribute holes of the same parent, which is what moves the elements in the second and third examples.
What still works
- A template with a single dynamic hole.
- Interpolations only, in sibling subtrees.
v-if / v-for when every interpolation in the same parent precedes them.
Impact
vapor_split!/1 backs the ~VUE sigil, .vue server-only, :reactive and :hybrid in phoenix_vapor. Only runtime: :full avoids it. That leaves no usable path for writing a real screen as a Vue SFC.
Vize.vapor_split!/1returnsstaticsin document order butslotsin a different order, soPhoenixVapor.Rendererfills each hole with the wrong value. Anything past a trivial template renders wrong markup.Reproduced on vize 0.10.0, 0.11.1 and 0.14.1, with phoenix_vapor 0.3.0 and with its
inertia-inspired-foundationsbranch. Elixir 1.20.0-rc.6, OTP 28, Linux x86_64.Repro
The last one changes the tree, not just the values.
Where the order comes from
vapor_ir!/1is correct — its effects come in document order and carry the element id and the attribute name:The slots come out sorted by element id, and the ids are not assigned in document order.
staticsstay in document order, so the two lists disagree whenever the ids do not happen to ascend with the document.v-ifandv-forholes are appended after the attribute holes of the same parent, which is what moves the elements in the second and third examples.What still works
v-if/v-forwhen every interpolation in the same parent precedes them.Impact
vapor_split!/1backs the~VUEsigil,.vueserver-only,:reactiveand:hybridin phoenix_vapor. Onlyruntime: :fullavoids it. That leaves no usable path for writing a real screen as a Vue SFC.