Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-planets-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect-app/vue-components": minor
---

Restore OmegaForm input reactivity broken by the @tanstack/vue-form 1.32 bump — the Field slot's state prop became a stale snapshot, so bind inputs to the reactive field.state instead.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { mount } from "@vue/test-utils"
import * as S from "effect-app/Schema"
import { describe, expect, it } from "vitest"
import { useOmegaForm } from "../../src/components/OmegaForm"
import OmegaIntlProvider from "../OmegaIntlProvider.vue"

describe("OmegaForm input reactivity", () => {
it("reflects value changes from the slot `state` in the view", async () => {
const schema = S.Struct({
name: S.String
})

const wrapper = mount({
components: { OmegaIntlProvider },
template: `
<OmegaIntlProvider>
<component :is="form.Form">
<component :is="form.Input" label="Name" name="name">
<template #default="{ field, state }">
<input
data-testid="input"
:value="state.value"
@input="(e) => field.handleChange(e.target.value)"
/>
<span data-testid="display">{{ state.value }}</span>
</template>
</component>
</component>
</OmegaIntlProvider>
`,
setup() {
const form = useOmegaForm(schema, {
defaultValues: { name: "" }
})
return { form }
}
})

await wrapper.vm.$nextTick()

const input = wrapper.find("[data-testid='input']")
await input.setValue("hello")
await wrapper.vm.$nextTick()

// The slot `state` must stay in sync with the form store
expect(wrapper.find("[data-testid='display']").text()).toBe("hello")
expect((input.element as HTMLInputElement).value).toBe("hello")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type InputProps<From extends Record<PropertyKey, any>, TName extends Deep
inputClass: string | undefined | null
}
field: OmegaFieldInternalApi<From, TName>
/** be sure to use this state and not `field.state` as it is not reactive */
/** reactive field state, sourced from `field.state` (the Field slot's own `state` prop is a stale snapshot since @tanstack/vue-form 1.32) */
state: OmegaFieldInternalApi<From, TName>["state"]
}

Expand Down
11 changes: 6 additions & 5 deletions packages/vue-components/src/components/OmegaForm/OmegaArray.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@
:is="form.Field"
:name="name"
>
<template #default="{ field, state }">
<!-- `field.state` is reactive; the Field slot's `state` prop is a stale snapshot since @tanstack/vue-form 1.32 -->
<template #default="{ field }">
<slot
name="pre-array"
v-bind="{ field, state }"
v-bind="{ field, state: field.state }"
/>
<component
:is="form.Field"
v-for="(_, i) of items"
:key="`${name}[${Number(i)}]`"
:name="`${name}[${Number(i)}]` as DeepKeys<From>"
>
<template #default="{ field: subField, state: subState }">
<template #default="{ field: subField }">
<slot
v-bind="{
subField,
subState,
subState: subField.state,
index: Number(i),
field
}"
Expand All @@ -27,7 +28,7 @@
</component>
<slot
name="post-array"
v-bind="{ field, state }"
v-bind="{ field, state: field.state }"
/>
<!-- TODO: legacy slot, remove this slot -->
<slot
Expand Down
10 changes: 8 additions & 2 deletions packages/vue-components/src/components/OmegaForm/OmegaInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@
:name="name"
:validators="validators"
>
<template #default="{ field, state }">
<!--
Read `state` from `field.state`, not the Field slot's `state` prop.
Since @tanstack/vue-form 1.32 the slot `state` is a one-time snapshot
taken when the field mounts and never updates, whereas `field.state`
is a reactive getter backed by the field store.
-->
<template #default="{ field }">
<OmegaInternalInput
v-if="meta"
v-bind="{ ...$attrs, ...$props, inputClass: computedClass }"
:field="field as any"
:state="state"
:state="field.state"
:register="form.registerField"
:label="label ?? errori18n(propsName)"
:meta="meta"
Expand Down
Loading