Repository navigation
v0.6.0
Upgrading from 0.5
A hybrid server action no longer writes props. Declare the data it changes as a model, and write the model's .value:
-const props = defineProps(["contacts"])
+const contacts = defineModel("contacts")
function deleteContact(id) {
"use server"
- props.contacts = props.contacts.filter((c) => c.id !== id)
+ contacts.value = contacts.value.filter((c) => c.id !== id)
}Read the model as contacts.value in the script and as contacts in the template; the LiveView assigns it as contacts, as before. The compiler reports each remaining props.x = ... with its line. The body of a server action now runs in the browser, so a guard (if (!id) return) keeps it from reaching the server, and a prop it reads is sent to the browser. See Breaking changes below and the Hybrid guide's Server actions section.
Breaking changes
- Require Volt 0.21. Its
mix volt.js.checktype-checks a.vuefile's scripts with the project'stsconfig.json, path aliases and declaration files included, and leaves<script lang="elixir">out, so hybrid components type-check as written. - Hybrid components declare data the browser changes, and the server owns, with Vue's
defineModel("name")instead of writing to props. A server action writes the model's.value:contacts.value = contacts.value.filter(...)in place ofprops.contacts = .... Writingprops.x, or a barexfor a prop the script doesn't declare, is a compile error that points atdefineModel, since props are read-only in Vue, in its types and at runtime; the old convention only worked because the compiler rewrote the assignment. A model must be named after its variable (const open = defineModel("open")), the assign the LiveView sets. A function that writes a model is a server action, as one that wrote a prop was. - A server action's body now runs in the browser as written, and the action is sent afterwards, unless the body returns or throws first, so a guard such as
if (!dirty.value) returnvalidates in the browser. Before, only its prop assignments ran and the event always went. So a prop a server action reads is now sent to the browser; one the browser must never see, such as a token, isn't a prop, andhandle_event/3reads it instead. - Reactive mode and the full runtime keep their QuickBEAM runtime in
socket.privateinstead of the:__vapor_runtime__and:__vue_runtime__assigns, so a session recorder doesn't record a pid andrender/1reads only state. PhoenixVapor.Componentand itsvue/1macro, which returned its argument unchanged, are removed;use PhoenixVaporimports only the~VUEsigil. Use~VUEdirectly.- Template expressions follow JavaScript's semantics, as Vue renders them in the browser: truthiness (
0,""andNaNare falsy inv-if,? :,!,&&and||),===(1 === 1.0),==, relational comparison (undefined > 0is false),+concatenation and arithmetic withNaNandInfinity, andtypeof null. A missing assign isundefinedandnilisnull. Before, Elixir's semantics leaked through, sov-if="items.length > 0"rendered with noitems. - Modules are grouped by role:
PhoenixVapor.Compiler.*compiles templates,PhoenixVapor.Renderer.*renders them, and each mode's modules sit under it.PhoenixVapor.Runtimeis nowPhoenixVapor.Reactive.Runtime,PhoenixVapor.LiveVueisPhoenixVapor.Full,PhoenixVapor.VueRuntimeisPhoenixVapor.Full.Runtime, andPhoenixVapor.ScriptSetupisPhoenixVapor.Compiler.ScriptSetup; the runtimes andScriptSetupare internal.use PhoenixVapor,~VUE,PhoenixVapor.Vue,PhoenixVapor.TemplateandPhoenixVapor.ExpressionErrorare unchanged.
Added
- Elixir counterparts for hybrid computeds: a
def filtered(assigns)in<script lang="elixir">computesfilteredon the server, from the props, the refs and earlier computeds, so a computed built on a composable or a library such as es-toolkit still renders in the first paint and in a replay. A key in the counterpart's pattern, or read asassigns.key, is required, and one read asassigns[:key]is optional,nilwhen the render doesn't have it; a counterpart that requires state only the browser has is never called on the live render, with a compile-time warning. A counterpart named after a LiveView callback, such asrender, is a compile error. - A hybrid component's constants (
const PAGE_SIZE = 20) are evaluated once while compiling and known to its server render, and a computed calling a script function with an Elixir counterpart calls the counterpart. State only the browser has, such as a composable's value, is absent from the live render: an expression reading it isn't rendered, av-ifchain whose condition reads it renders no branch, and a counterpart reading it isn't called, until a replay that recorded it. - A
.vuefile is hybrid when<script setup>declares state the browser owns: aref(), or now any top-level name bound by a call the compiler can't run, such as a composable's (useLocalStorage,const { copy, copied } = useClipboard()) orreactive(). A file whose only state came from composables used to compile as server-only. - Session replay for hybrid components: while a session replayer such as PhoenixReplay records client state (
phx_replay:startonwindow), each hybrid component reports the client state its server render reads, refs and composables' results alike, asphx_replay:stateevents: all of it on start, or on mount during a recording (<html data-phx-replay>), then the latest value of what changed, at most once per flush interval, underphoenix_vapor:<wrapper id>, with non-plain values asnull; nothing is dispatched otherwise. Each hybrid LiveView definesreplay_render/1, which renders without the client hook and with the state recorded in@phoenix_replay_state. PhoenixVapor needs no dependency on PhoenixReplay. See the Hybrid guide's Session replay section. - An expression only JavaScript can evaluate, such as
items.filter(i => i.on)orMath.max(a, b), is reported at compile time as a warning: it runs in QuickBEAM on every render, while the rest of rendering is Elixir. A method a value doesn't have, such astrim()on a number, raisesPhoenixVapor.ExpressionError, as it throws in the browser. - In hybrid mode, components from packages, such as Reka UI, render on the server when everything they receive is known at compile time. Vue's server renderer runs them once in QuickBEAM while the template compiles, so rendering runs no JavaScript. Nested package components render together, providers such as
TooltipProviderrender only their content, and in hybrid mode the initial values of refs count as known. One that can't render is reported with the reason, such as a prop known only when rendering or an error from the component. Outside hybrid mode, where nothing takes over in the browser, only package components that render just their content, such as providers, render; others remain a compile error. - A macro call that reads props known only when rendering runs at compile time once per combination of the values their TypeScript types allow, up to 64, and rendering looks the result up. Types come from TypeScript's own checker, from the project's
node_modules, so variant types derived from a tailwind-variants config work. A value outside the type raisesPhoenixVapor.ExpressionError. - A package component's props may be expressions of known values, such as
:open="selected !== null"over a ref's initial value, and package parts inside the template's ownv-fororv-if, such as a tooltip per row, render in their ancestors' context. - A template's call to a
<script setup>function renders on the server through the function of the same name in snake_case that<script lang="elixir">defines, such asrole_tone/1forroleTone(role). The full runtime now leaves a<script lang="elixir">block out of its browser bundle. PhoenixVapor.Volt, a Volt plugin that resolvesphoenix_vaporimports from a path or umbrella dependency, whichresolve_dirs: ["deps"]doesn't find.- The
file:option andPhoenixVapor.Vue.component/2take any expression known at compile time, such asPath.join(@templates, "Card.vue"), besides a path relative to the module's file.
Fixed
- A server action sent the refs it read as they were after its body ran, so
selectedIds.value = []at the end of the body sent an empty selection. It sends them as they were when it was called. - While several server actions were in flight, the first answer reapplied the server's props, undoing the optimistic changes of the others until their own answers came. The props now apply once every action in flight has been answered, or has failed.
- An optimistic change the server declined stayed on screen: when
handle_event/3left the assign unchanged, no new props came. Once the server has handled a server action, the component's props and models are the server's again. - A server action whose body returned early, such as
if (!id) return, wasn't recognized: its body was parsed as a script, wherereturnis a syntax error, so it was a client handler and never reached the server. - A hybrid component's prop that only the template read, as
props.saved, stayed on the server: the browser never received it, and the server's own first paint rendered it empty. A prop the template reads now goes to the browser. - Reactive mode's direct DOM patching assumed the template was the LiveView's whole render, so inside a layout it matched the layout's slots instead of the template's, and updates didn't reach the page. It now finds the template in the rendered tree, and patches directly only when a diff changes nothing outside it.
- A hybrid computed that read something only the browser has, such as a composable's result (
refDebounced) or an import (sortByfrom es-toolkit), failed every server render withExpressionError. The server now decides this when compiling: it leaves such a computed, and what reads it, out of the first paint, with a warning naming what it lacks. An expression reading it isn't rendered, and av-ifchain whose condition reads it renders no branch, rather than itsv-else. - The QuickBEAM runtime that ran macro calls while compiling was never stopped, and the one for package components leaked when compiling raised. One runtime per compile now serves both and stops in an
after. - A hybrid component started a QuickBEAM runtime on every render to evaluate its computed values, and a computed that failed rendered as missing without a report. Computeds now evaluate as template expressions do: one that reads only refs is evaluated once while compiling, and package components can render with it; one that reads props is evaluated when rendering, on the LiveView process's runtime, with its last value reused while its inputs are unchanged. A computed's other computeds are evaluated first, so lists such as a filtered view of props render on the server. A failure is a compile-time warning, or an
ExpressionErrorwhen rendering. - A template literal's text between expressions rendered empty:
`a${x}b`gave1.