Conversation
|
I think this is useful and should come at no cost of learning if it follows closely the existing syntax of JS:
I remember this was proposed in the back in vuejs/vue, it might be worth checking the existing issues/discussions on it if you haven't already |
|
tonaly agree! |
|
It would be nice to lay out the interaction with other DOM-controlling directives, like Is it ok to put <!-- is this allowed? -->
<template v-match>
<div v-case v-if />
<div v-else>Still in first case?</div>
<div v-case />
<div v-case />
</template>
<!-- if not then this is they way I guess? -->
<template v-match>
<template v-case>
<div v-if />
<div v-else>Still in first case?</div>
</template>
<div v-case />
<div v-case />
</template>I assume all tags inside the v-match MUST have a v-case and no text node is allowed? |
probably like: <template v-case>Text</template> |
…dapted for MoonBit) Implement patterned templates inspired by Vue RFC #823, generating native MoonBit match expressions instead of equality comparisons. Supports: - String literal matching: v-case='"loading"' - Enum pattern matching: v-case='Ok(data)' with variable binding - Wildcard/default: v-case='_' or v-case.default - Nested match expressions Validation: - v-case must be direct child of v-match element - At most one v-case.default per v-match - v-match must have at least one v-case child - Non-v-case elements inside v-match are rejected Codegen generates MoonBit match expressions for both client (DOM) and server (SSR) targets. Refs: vuejs/rfcs#823 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The reference implementation is split across these Draft PRs:
The RFC file and PR body now allow Core regression tests cover AST reuse, header source maps, import usage, invalid headers, VDOM/Vapor updates and SSR hydration. Local unit and jsdom suites pass 6,703 tests. Real Vite browser checks pass for dev and production VDOM, Vapor and template-only Vapor, including header-only HMR. Tooling passes 360 local tests with 4 existing skips, and 150 tests with the companion compiler loaded. Coverage includes imported generics, optional properties, tuple combinations, array rest, header diagnostics and edits, hover/completion, definition/rename, and CLI exit statuses. The type algorithm is an ordinary checked declaration file referenced through Scope regressions retain setup/prop/loop/slot shadowing, These remain reference implementations for RFC discussion. The top-level form currently supports inline HTML with the descriptor AST preserved. Preprocessors, external |
| ## SFC top-level template | ||
|
|
||
| An SFC may place `v-match` directly on its top-level `<template>`, as in the basic example. It is equivalent to a plain SFC `<template>` containing one inner `<template v-match>` around the entire template content. No extra DOM wrapper is introduced. | ||
|
|
||
| The subject resolves in the component's template scope. Direct children are match arms, each with its own lexical bindings; evaluation order, guards, validation, narrowing, and exhaustive coverage are the same as for an inner match. Other SFC block attributes retain their existing meaning. This does not enable other template directives on the SFC block. |
There was a problem hiding this comment.
I don't think v-match on the top-level template should be supported. This would leak knowledge of the template syntax to the top-level tag, which is inconsistent with its existing semantics; supporting it without supporting other structural directives would cause cognitive load and fragmentation.
Summary
Introduce
v-matchandv-whenfor declarative pattern-based conditional rendering in Vue templates.v-matchevaluates a subject expression once. Each directv-whenchild declares a pattern, optional bindings (including object and array rest), and an optional guard. Branches are checked from top to bottom, only the first matching branch renders, and no fallthrough is possible. Template type checking requires exhaustive coverage, with_available as the fallback pattern.The initial syntax keeps
v-whenin its long form. Shorthand candidates and their trade-offs are recorded below for future discussion.The proposed syntax intentionally follows the vocabulary of the TC39 ECMAScript Pattern Matching proposal (
match/when, pattern guards, declaration bindings) while also catching up with Flow's shippedmatchfeature (constvariable declaration patterns,ifguards,_wildcard,|alternatives,asbindings, and exhaustiveness-aware tooling).Basic example
The above is equivalent in behavior to a
v-if/v-else-ifchain that first evaluatesresult, checks each branch in order, introduces branch-local template bindings such asarticleanderror, and renders the fallback only if no previous branch matched.Motivation
Chained
v-ifis repetitive for one subjectWhen rendering different content based on a single reactive value, developers currently repeat the same expression in every branch:
This has several drawbacks:
v-matchmakes the subject explicit, andv-whenmakes each branch an arm of the same match.Vue code increasingly models UI state as tagged data
Vue applications commonly consume typed state from composables, loaders, data-fetching libraries, state stores, routers, and RPC clients. These values are often modeled as discriminated unions or tagged objects:
Templates need an ergonomic way to render these states without repeatedly indexing into the same object and without losing branch-specific type information.
Prior art is converging on patterns plus bindings
matchexpressions withwhenarms, declaration patterns such asconst status, rest binding patterns, and guard patterns.matchhas shipped with object, array, wildcard,const,|,as, instance, guard, exhaustiveness, and unused-pattern checks.This RFC does not attempt to add JavaScript pattern matching to Vue. It proposes a template-level feature that borrows the parts that map cleanly to rendering: arm syntax, structural patterns, branch-local bindings, guard conditions, and type-tooling hooks.
Detailed design
Recommended syntax:
v-match/v-whenv-matchevaluates the subject expression.v-whendeclares a pattern arm.v-when="_"declares an unconditional fallback arm.if (<guard>)suffix acts as a branch guard.The name
v-whenis recommended over the previousv-casedirection because it directly mirrors TC39'smatch (...) { when ... }vocabulary and avoids suggesting JavaScriptswitchfallthrough behavior.SFC top-level template
An SFC may place
v-matchdirectly on its top-level<template>, as in the basic example. It is equivalent to a plain SFC<template>containing one inner<template v-match>around the entire template content. No extra DOM wrapper is introduced.The subject resolves in the component's template scope. Direct children are match arms, each with its own lexical bindings; evaluation order, guards, validation, narrowing, and exhaustive coverage are the same as for an inner match. Other SFC block attributes retain their existing meaning. This does not enable other template directives on the SFC block.
Shorthand discussion (deferred)
Vue already provides short forms such as
@forv-on,:forv-bind, and#forv-slot. A short form ofv-whencould make repeated arms similarly concise. The candidates below have readability drawbacks, so this RFC defers a shorthand and recommends the long form.?: conditional branching?takes its cue from JavaScript's conditional operator,condition ? consequent : alternate. It suggests a branch selected by a condition and avoids spelling a JavaScript compound assignment when followed by the attribute's=:Its weakness is that
?suggests conditional rendering generally, so it could be mistaken for av-ifshorthand. Its value would still be a pattern, not a truthiness test:?="true"would match the subject againsttrue. This mnemonic is proposed for Vue; it is not shorthand defined by TC39 or Flow.|: pattern-matching branches|has a direct precedent in OCaml pattern matching and F# match expressions, where| pattern -> expressionintroduces an alternative branch.However, writing it as an attribute produces
|="pattern", which visually reads as JavaScript's bitwise OR assignment operator|=. Combining it with or-patterns further repeats the symbol:The attribute name and quoted pattern are unambiguous to a parser, but the compound-assignment resemblance is a readability drawback.
|is therefore not recommended as the attribute shorthand. This does not affect|inside patterns, which retains its role as the or combinator.~or no shorthand~="pattern"can suggest similarity or matching, but that is a weaker mnemonic for selecting a branch, and JavaScript uses~for bitwise negation. Keeping onlyv-whenremains a viable choice if none of the symbols reads clearly enough.v-matchremains the explicit name of the enclosing construct. It appears once per block, whilev-whenis repeated for every arm, so shortening the arms provides most of the benefit and keeps the subject easy to find. A second symbol for the host is not proposed.Parsing and tooling
Any selected shorthand must normalize to
v-whenbefore structural validation, binding analysis, and code generation. Long and short forms must produce the same directive AST apart from source locations, preserve pattern bindings and guards, and never emit the shorthand as an HTML attribute or component prop, including during SSR. Both forms require a directv-matchparent and can be mixed across arms.The pattern, including the
_fallback, stays in the quoted attribute value. Neither form accepts directive arguments or modifiers; a missing pattern and combining long and short forms on the same element are compile-time errors. The guard afterifremains a normal JavaScript expression, including its usual operators. Existing@,:, and#syntax keeps its meaning insidev-match.Editor highlighting, completion, formatting, linting, and Volar / vue-tsc would need to recognize both forms and preserve the source spelling. These candidates are not implemented by the current Vue parser. Shorthand can be deferred independently of the long form.
Branch order
Branches are tested in source order. Only the first branch whose pattern and guard match is rendered.
The above renders the first branch when
statusis either'loading'or'pending', the second when it is'error', and the wildcard branch otherwise.Pattern grammar
v-whenuses a pattern grammar, not a normal JavaScript expression grammar. This is similar to howv-foralready has directive-specific syntax.The initial pattern grammar should include the subset that is most useful in templates and that aligns with TC39 and Flow:
Literal patterns
Literal patterns match primitive values:
Literal matching uses JavaScript strict equality semantics, with
NaNmatched usingNumber.isNaN.Value patterns
Identifiers and member expressions can be used as value patterns:
A value pattern compares the subject to the runtime value of the expression. To keep the initial feature deterministic and friendly to tooling, arbitrary expressions are not allowed as patterns. Developers can assign an expression to a binding in
<script setup>and match against that binding.Wildcard pattern
_matches any remaining value:v-when="_"is the idiomatic fallback._also matches any value at a nested pattern position, such as{ status: _ }, without introducing a binding. To access the matched value, use a binding pattern instead.Binding patterns
A binding pattern matches any value and exposes it as a branch-local template binding:
For the initial RFC,
constis the only supported binding declaration:letor function-scopedvar.letandvarare reserved for future discussion and should produce compile-time errors in the initial implementation.Bindings are scoped to the matching branch only. They are visible to the element that carries
v-when, its other directive expressions, its attributes, its children, and the optional guard expression. They are not visible to sibling branches or outside thev-matchblock.Object patterns
Object patterns match object shape and can introduce bindings:
Shorthand binding follows Flow's object pattern spelling:
{ const data }is equivalent to{ data: const data }.Object rest can bind the remaining own enumerable properties:
Runtime matching follows open structural matching: extra properties do not make the pattern fail. Exhaustiveness analysis must use the same rule; it must not treat omission of
...as a demand for an exact set of properties.A lone
...is allowed to explicitly acknowledge additional properties, borrowing Flow's spelling. Under this RFC's open object semantics it does not change runtime matching or coverage and introduces no binding:Array patterns
Array patterns match arrays and tuples structurally. They require
Array.isArray(subject). Without rest, the subject must have exactly the listed number of elements; a trailing...or...const restpermits additional elements. An array rest binding collects those additional elements. General iterables and array-like objects are not included in the initial template grammar.This is a change from the earlier draft where an array in
v-casemeant "one of these values". In the revised design, arrays are structural patterns, matching TC39 and Flow. Use|for multiple alternatives.Rest patterns and bindings
Object and array rest are required in the initial feature, including runtime support, branch-local type inference, and exhaustiveness analysis. They use the following syntax inside an object or array pattern:
...accepts the remainder without binding it....const restaccepts the same values and binds the remainder. These forms borrow the pattern-specific rest spelling from TC39 and Flow;...restwithoutconstis not supported. Rest is not a standalone top-level pattern.For object rest, every explicitly listed key is excluded from the result, whether its subpattern is a literal, wildcard, or binding. The rest binding is a fresh ordinary object containing the remaining own enumerable string and symbol properties, with JavaScript object-rest copy semantics. Inherited and non-enumerable properties are not copied. An empty remainder is valid and binds
{}.For
{ kind: 'message', text: 'Hello', sender: 'Ada' },metadatais{ sender: 'Ada' }; bothkindandtextare excluded.For array rest, the listed prefix must match first. The rest binding is a fresh array containing the remaining element values in index order, without copying non-index properties. Rest accepts zero additional elements:
[const first, ...const tail]matches any non-empty array, and matching[42]bindsfirstto42andtailto[].[...const items]matches any array, including an empty one.For an array-typed subject this match is exhaustive:
[]covers length zero, and the second arm covers every positive length.[]and[const first]alone would leave arrays with two or more elements uncovered. A bare[ ... ]is likewise exhaustive for an array-typed subject, but not forunknownor an array-or-nullunion.Rest can appear at multiple nesting levels, with at most one rest entry per object or array pattern:
Rest bindings have the same scope as other pattern bindings and are available in the arm's guard. They are initialized when their pattern succeeds, before that guard runs. A failed guard discards the arm's bindings and continues matching. Copies are shallow: nested objects retain their identity, the source is not mutated, and the binding's
constdoes not deep-freeze the copied value. Copying participates in normal reactive reads; rest values may be recreated when rendering is reevaluated, with no stable-identity guarantee. Unbound...requires no remainder allocation or reads of discarded values.The checker infers object rest using TypeScript's object-rest rules on the narrowed subject, excluding the listed keys for each remaining union member. For tuple rest it preserves the known tail: matching
[const first, ...const tail]against[string, number, boolean]givestailthe type[number, boolean]. For a generalstring[],tailisstring[]. Both rest forms copy into new containers, including when the input container is readonly.Rest must be last in its enclosing pattern and must not have a trailing comma. Multiple rest entries, elements or properties after rest,
...let rest,...var rest, and duplicate binding names are compile-time errors. Rest bindings inside an or-pattern follow the initial restriction on bindings in alternatives. Nested destructuring directly after...is not supported; authors can match the bound remainder in a nestedv-match.Or patterns
|combines multiple patterns:TC39 currently spells this combinator as
or, while Flow uses|. This RFC recommends|for Vue templates because it matches Flow's shipped syntax and the union-like notation TypeScript users already read in type positions. Supportingoras a future alias remains possible.For the initial implementation, bindings inside
|patterns are not supported. This follows Flow's current restriction and keeps branch-local binding types predictable:Authors can use separate arms instead.
As patterns
aspatterns bind the whole matched value after the pattern succeeds:This mirrors Flow's
aspattern and gives templates a concise way to pass the refined object itself while still testing its shape.Parenthesized patterns
Parentheses can disambiguate complex patterns:
Guards
A
v-whenpattern can be followed by anif (<guard>)suffix:The guard runs only after the pattern succeeds. Pattern bindings are available inside the guard.
This spelling follows Flow's guard placement. It also corresponds to TC39 guard patterns:
when <pattern> and if (<guard>). Vue should document the simpler template spelling while keeping the conceptual mapping clear.Guarded arms do not contribute to exhaustive coverage, even if a guard appears constant. A guard can fail at runtime even when the structural pattern matched; an unguarded arm must cover the remaining values. The initial checker does not attempt to prove relationships between guard expressions.
Wildcard fallback
An unguarded
_arm provides the fallback using the same pattern grammar as every other branch:Rules:
_arm must be last and unique within itsv-matchblock, including when parenthesized._ if (showFallback)is an ordinary conditional arm. If its guard fails, matching continues; it does not count as exhaustive coverage or as the unconditional fallback.{ status: _ }only accept values at that position. The enclosing structural pattern must still match.v-ifwithoutv-else; static exhaustiveness does not introduce a runtime exception..defaultmodifier is proposed. A fallback that needs the subject can useconst valueor_ as valueinstead of a bare wildcard.For example, a guarded wildcard can precede an unconditional one:
Reactivity and evaluation
v-matchevaluates its subject expression once per render and stores it in a compiler-generated temporary.Each branch pattern is checked against that temporary. Branch-local bindings are derived from the matched subject and participate in normal template reactivity because they are re-created on each render from reactive source values.
The compiler must not call
expensiveResult()once per branch.Compilation
The compiler can lower
v-matchto an equivalent conditional chain.For:
The generated render logic is conceptually:
The actual implementation can use compiler IR helpers instead of literally emitting this structure. The important properties are:
Validation rules
The compiler should enforce these rules, also applying them to any shorthand adopted later:
v-whenmust be a direct child of av-matchelement or<template>.v-matchwith nov-whenchildren should warn.v-whendirect children of av-matchblock should warn._arm must be last and unique, including across long and short forms.v-whenmust have a valid pattern and must not have arguments or modifiers.v-whenmust not be combined withv-if,v-else-if,v-else,v-for, or anotherv-matchon the same element.letandvarbinding patterns should be compile-time errors in the initial implementation.|patterns should be compile-time errors in the initial implementation....const identifierand obey the same binding-name and or-pattern restrictions as other bindings.Type tooling
Volar / vue-tsc must use
v-matchandv-whenas type-narrowing boundaries.Inside the first branch,
resultis narrowed to{ status: 'success'; data: Article }, andarticleis typed asArticle. Inside the second branch,erroris typed asError.asbindings preserve the narrowed whole-subject type. A nestedv-matchstarts from the type available within its enclosing branch.Exhaustiveness checking
Exhaustiveness is a required part of this proposal's type-tooling contract, inspired by Flow's exhaustive checking. Every
v-matchanalyzed by the template type checker must cover its subject type. A non-exhaustive match is an error by default, not an optional lint suggestion, and causesvue-tsc --noEmitto fail. Volar must show the corresponding diagnostic in the editor without requiring a separate ESLint rule or directive modifier.Coverage rules
The checker starts with the subject's type at the
v-matchsite, after normal template ref unwrapping. It tracks the values that remain unhandled as it visits arms in source order:asbindings do not change the underlying pattern's coverage._,const value, or_ as valuecovers all remaining values. A wildcard nested in a structural pattern only covers its position, subject to the enclosing shape and property-presence checks.ifguard removes no values from the remaining space. This also applies to_ if (...), binding patterns with guards, and apparently complementary guards.The required analyzable cases include finite literal unions, booleans, enum members with statically known values, discriminated object unions, finite combinations of nested object and tuple patterns, and array length partitions with trailing rest. Coverage must account for structural subcases, rather than only removing top-level union members.
nullandundefinedare distinct cases when present in the subject type. For example,ref<Result>()includes an initialundefinedvalue and requires anundefinedarm or an unconditional fallback in addition to allResultvariants. An optional object property is not covered merely by matching its present value; the absent-property case must also be handled.An unrestricted
stringornumber,any,unknown, or a generic type whose constraints do not establish a covered value space requires an unguarded catch-all pattern. Finite literal arms cannot exhaust an open primitive type. If the checker cannot prove coverage for a more complex type or pattern combination, it must report that limitation and request a catch-all; it must not silently mark the match exhaustive. A subject typed asneverhas no remaining values and is vacuously covered.Missing-case diagnostics
The diagnostic is attached to the
v-matchsubject expression. It identifies uncovered cases and suggests validv-whenpatterns when they can be enumerated.Adding
<ErrorPanel v-when="'error'" />completes coverage. Adding an unguarded_arm also completes coverage but intentionally handles future variants through the fallback. When authors want additions to a union to require a new branch, they should enumerate the variants without a catch-all.A guarded variant still needs an unguarded arm:
For a subject with success and error variants, removing the final arm must report the uncovered error variant. Neither complementary guards nor a final guarded wildcard discharge that requirement.
For nested patterns, the suggested missing case should be as specific as practical. If the subject is
['left' | 'right', 'top' | 'bottom']and three combinations are covered, the diagnostic identifies the fourth tuple pattern. A pattern{ status: 'success', data: null }only covers successful results withnulldata; it cannot discharge all successful results if other data values are possible.Unreachable arms and intentional empty rendering
The checker also reports a warning on an arm whose entire pattern is proven unable to match any remaining value, either because earlier unguarded arms cover it or because it is outside the subject type. A guarded arm does not make a later arm unreachable merely by matching the same structural pattern. These warnings are separate from non-exhaustive-match errors; syntactically invalid wildcard placement remains a compiler error.
Intentionally rendering nothing is expressed by an empty arm, which still counts toward coverage:
The checker must analyze the arm before any optimization removes its empty render output. It must not infer that an omitted case means an intentional empty branch.
Compiler boundary
The SFC compiler performs syntax and structural validation without requiring a TypeScript program. Semantic coverage diagnostics belong to Volar / vue-tsc, including JavaScript templates when the template checker has inferred types for them. Running only the template compiler or a build that does not run template type checking does not certify exhaustiveness.
If no runtime arm matches because type checking was skipped or the runtime value violates its declared type, the block renders nothing. This is Vue's conditional-rendering behavior, not Flow's match-expression exception behavior. The checker must not remove that runtime path or allow an exhaustiveness result to change client or SSR branch selection.
Compiler and tooling work may be developed in stages, but exhaustiveness checking with these default errors is part of the feature's acceptance criteria, not a deferred stretch goal.
Drawbacks
v-matchandv-when.v-casedraft.Alternatives
Dedicated default syntax
v-when.defaultcould identify a fallback separately. This RFC recommendsv-when="_"instead:_is already a wildcard pattern, so it needs no separate modifier or fallback-only parsing rule. It also composes with guards and nested patterns, following Flow's wildcard spelling. Any shorthand should keep_in the attribute value, where every other pattern lives.Keep the previous
v-match/v-casedesignThe earlier draft used:
This is simple, but it only covers strict equality and does not scale to object patterns, branch-local bindings, or guard expressions. It also misses the TC39
whenvocabulary.Keep using
v-if/v-else-ifThe current directives remain fully supported and are still best when each branch checks unrelated conditions:
v-matchis intended for branches that all inspect the same subject.Renderless
<Match>componentThis can be approximated in userland, but it cannot provide compiler-level binding scopes, branch-specific type narrowing, or the same optimization opportunities.
Separate guard directive
Instead of
v-when="{ ... } if (...)", guards could be a second directive:This is easier to parse but weaker as a pattern-matching story. Flow places guards on the arm, and TC39 models guards as part of the pattern. Keeping the guard inside
v-whenmakes the branch read as one unit.Adopt all Flow patterns immediately
Flow includes instance patterns such as:
Vue could eventually support class / component instance patterns, but they are not required for the initial template feature. Object, array, wildcard, binding,
|,as, and guard patterns cover the common UI state cases with less runtime and parser complexity.Adoption strategy
v-if/v-else-if/v-elsecode continues to work. Any shorthand would reserve new attribute syntax; compatibility with existing literal attributes must be reviewed before adopting a symbol.v-matchbeside conditional rendering, emphasizing one-subject branching and branch-local bindings.v-whenfirst, then any adopted shorthand with the same semantics. Formatters and codemods should not force either spelling.v-matchwhen av-ifchain repeatedly checks the same subject.v-match/v-when.Unresolved questions
..., beyond the initial unbound rest and...const identifierforms?letbindings ever be supported in templates, or should Vue intentionally keep branch bindings immutable withconstonly?insemantics, own-property semantics, or align exactly with the eventual ECMAScript proposal?Symbol.customMatcheradvances?v-whenlong-form and defers symbol selection.