feat(components): two-way binding for Input (v-model / updateModelValue)#461
Merged
alexgrozav merged 12 commits intoJun 10, 2026
Merged
Conversation
Solid/Svelte emit component callback props via an isComponent-aware rewriteEventName (native DOM listeners stay lowercase); Angular lowercases only the leading char of an @output() name. Prerequisite for component custom-event emission (UXF-5).
…rse/IR/lower) New @inkline/core primitives defineModel/defineEmits. Compiler parses them into IRComponent.models + synthesized value prop + update:<name> model event + emitName, and lowers $bind:<prop> on component instances (getter convention, twoWayProp marker). IR_VERSION 1->2 + migration. Diagnostics INK0043/INK0044. Codegen for these lands in following phases.
… side) Shared modelReads/modelSetters/emit rewrite rules + eventToCallbackProp/callbackPropRules. Per target: Vue defineModel/defineEmits, Svelte $bindable + callback props, Angular model()/output(), React/Solid value prop + onUpdate callback, Qwik QRL callbacks, Astro read-only let + no-op (INK0045). component.models is the single source (no synthesized prop/event). Model/callback names excluded from attribute fallthrough.
Each target renders the lowered update:<prop> event idiomatically via the twoWayProp marker: React/Solid onUpdate<Prop> callback, Qwik onUpdate<Prop>$ QRL, Vue v-model:<prop>, Svelte bind:<prop>, Angular [value]+( <prop>Change), Astro one-way (drops the inert update). Also: Svelte child models use $bindable only (dropped the spurious onUpdateValue callback). New same-module BoundField + native NativeBind fixtures + per-target parent assertions + scenarios.
Migrate IInputControlBase to defineModel("value") wired into both <Show> branches; IInputControl forwards it via $bind:value. Two compiler fixes the migration required: two-way-binding lowering now resolves $bind: to forwarded models (not just createSignal state), and Qwik keeps prop-less components' params untyped. Adds a two-way Storybook story, README/AGENTS/authoring docs, and a changeset for the 7 framework packages.
…n-void tags
The Svelte target lowered slots to the deprecated `<slot>` element and self-closed every empty element (incl. non-void `<span>`/`<textarea>`), producing slot_element_deprecated and element_invalid_self_closing_tag warnings on build.
Slots now compile to runes: default -> {@render children?.()}, named -> {@render <name>?.()}, fallback wrapped in {#if}/{:else}, scoped slots thread positional args. Each slot is declared as a typed Snippet prop on $props(); a named slot's prop binds to a <name>Snippet local so {@render} can't collide with an in-scope binding (e.g. {#each items as item} vs an `item` slot). Non-void empty elements now print as <tag></tag>. Output is functionally identical.
src/index.ts exported `unplugin` as both a named and the default export, so bundling the barrel warned "named and default exports together". The default was redundant with the named `unplugin` and unused (the per-bundler entries supply the default plugins). The barrel now exports only named symbols, clearing the warning without changing the per-bundler entries' CommonJS interop.
…mplate> A multi-child Fragment (component default slot, multi-root render, or multi-node slot fallback) was wrapped in a directive-less <template>, which Vue renders as an inert HTML element — dropping every child. Now emits a cGroup (bare siblings), matching Svelte and the JSX targets. Adds the MultiChildSlot fixture + a Vue slots assertion; updates the SlotScoped Vue snapshot (fallback no longer wrapped). Pre-existing bug surfaced by the Input two-way story.
… level INK0045 (Astro two-way binding / events not interactive) is an advisory about a target limitation, not an actionable warning — reclassify it from `warning` to `info`, matching the other advisory codes (INK0061, INK0066). Add `meetsLevel(severity, level)` to gate diagnostics by a minimum severity. The CLI `inkline compile --watch` loop now reports `warning` and above, so the INK0045 notice no longer spams the dev loop on every recompile while genuine warnings (e.g. missing key) still surface. One-shot `inkline compile` and `inkline check` keep the `info` floor and print everything.
A zero-arg call such as a styleframe recipe `inputAppendRecipe()` was treated as a reactive signal read and rewritten per target — stripped to a bare `inputAppendRecipe` (React/Svelte/Vue/Astro) or `inputAppendRecipe.value` (Qwik) — so the recipe *function*, not its result, ended up in the class attribute. The expression rewriter now applies a target's reactive-read convention only to identifiers that are actually reactive accessors (signals, memos, model getters), collected from the component IR via `reactiveReadNames()` and threaded in as `rules.reactiveReads`. Every other zero-arg call (an imported recipe, a plain helper) keeps its call syntax across all 7 targets. Also fixes resource fetchers whose call was likewise stripped. Adds a RecipeNoArgs fixture + per-target regression tests and unit gating tests.
…props param
Svelte declared each model's $bindable() binding under the public prop name
while reads/writes resolve to the getter local, so an aliased model
(defineModel("open") bound to a local isOpen) referenced an undeclared
variable in both the template and the reconstructed whole-props object. The
binding now renames when the names differ (open: isOpen = $bindable()).
Qwik only emitted the props parameter for components with plain props, slots,
or attribute fallthrough, yet models/events compile to props.<x> references.
A model- or event-only component with a non-fallthrough root (e.g. a Fragment)
emitted component$(() => while referencing props.value. Models and events are
now included in the parameter condition.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #461 +/- ##
==========================================
+ Coverage 93.51% 93.93% +0.41%
==========================================
Files 108 109 +1
Lines 4890 5160 +270
Branches 1687 1786 +99
==========================================
+ Hits 4573 4847 +274
+ Misses 269 265 -4
Partials 48 48
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds two-way binding (
$bind:) support to the compiler and wires it through theInputcomponent, so a single authored model emits idiomatic two-way bindings per target — Vuev-model, Sveltebind:, React/Solid/Qwik value +onUpdate/event pairs. Bundles several compiler codegen fixes surfaced while implementing it (Svelte{@render}snippets and aliased-model bindings, Vue multi-child slot fragments, Qwik model/event-only props, zero-arg recipe call syntax, component event-name casing, INK0045 reclassified to info, and a redundant plugin barrel export removed).Issue
https://linear.app/grozav/issue/UXF-5/input-two-way-binding-v-model-updatemodelvalue
Test plan
vp run readypasses locallytwo-way-bindinglower-pass and CLIcompiletests pass; Input headless snapshots updatedChecklist
core/core/AGENTS.mdanddocs/authoring-components.mdfor the new authoring API