Skip to content

vapor_split!/1 emits dynamic holes out of document order #3

Description

@luis1112

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions