@zeno-lib/forms 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,15 @@
1
1
  "use client";
2
2
  import { evaluate } from "@tanstack/react-form";
3
- import { useCallback, useRef } from "react";
3
+ import { useCallback, useLayoutEffect, useRef } from "react";
4
4
  //#region src/lib/use-rebased-default-values.ts
5
5
  function useRebasedDefaultValues(callerDefaults) {
6
6
  const liveForm = useRef(null);
7
7
  const lastCallerDefaults = useRef(callerDefaults);
8
8
  let defaultValues = callerDefaults;
9
- if (evaluate(lastCallerDefaults.current, callerDefaults)) {
10
- if (liveForm.current) defaultValues = liveForm.current.options.defaultValues;
11
- } else lastCallerDefaults.current = callerDefaults;
9
+ if (liveForm.current && evaluate(lastCallerDefaults.current, callerDefaults)) defaultValues = liveForm.current.options.defaultValues;
10
+ useLayoutEffect(() => {
11
+ lastCallerDefaults.current = callerDefaults;
12
+ }, [callerDefaults]);
12
13
  const onMount = useCallback((props) => {
13
14
  liveForm.current = props.formApi;
14
15
  }, []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeno-lib/forms",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "repository": {
5
5
  "url": "https://github.com/zeno-lib/zeno.git",
6
6
  "directory": "packages/forms"
@@ -7,7 +7,7 @@ import {
7
7
  within,
8
8
  } from "@zeno-lib/test/testing-library"
9
9
  import userEvent from "@zeno-lib/test/user-event"
10
- import { type ReactNode, useState } from "react"
10
+ import { type ReactNode, StrictMode, useState } from "react"
11
11
  import { afterEach, describe, expect, onTestFinished, test, vi } from "vitest"
12
12
  import { z } from "zod"
13
13
  import { useForm } from "./create-form"
@@ -178,6 +178,13 @@ describe("FormDialog", () => {
178
178
  expect(name.value).toBe("Ada")
179
179
  })
180
180
 
181
+ test("opens with per-session defaults under Strict Mode", async () => {
182
+ const user = userEvent.setup()
183
+ render(<Harness />, { wrapper: StrictMode })
184
+ const name = await openEdit(user)
185
+ expect(name.value).toBe("Ada")
186
+ })
187
+
181
188
  test("an edit opened on a record with keys the schema lacks saves to a .strict() schema", async () => {
182
189
  const user = userEvent.setup()
183
190
  const strictSchema = schema.strict()
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import { type AnyFormApi, evaluate } from "@tanstack/react-form"
4
- import { useCallback, useRef } from "react"
4
+ import { useCallback, useLayoutEffect, useRef } from "react"
5
5
 
6
6
  // TanStack's `useForm` calls `formApi.update(options)` after every render. While
7
7
  // the form is untouched, `update` re-applies `options.defaultValues` whenever it
@@ -24,14 +24,19 @@ function useRebasedDefaultValues<T>(callerDefaults: T): {
24
24
  const lastCallerDefaults = useRef(callerDefaults)
25
25
 
26
26
  let defaultValues = callerDefaults
27
- if (evaluate(lastCallerDefaults.current, callerDefaults)) {
28
- if (liveForm.current) {
29
- defaultValues = liveForm.current.options.defaultValues as T
30
- }
31
- } else {
32
- lastCallerDefaults.current = callerDefaults
27
+ if (
28
+ liveForm.current &&
29
+ evaluate(lastCallerDefaults.current, callerDefaults)
30
+ ) {
31
+ defaultValues = liveForm.current.options.defaultValues as T
33
32
  }
34
33
 
34
+ // Write on commit, not during render: under Strict Mode the second render
35
+ // would see the new defaults as unchanged and forward the old ones.
36
+ useLayoutEffect(() => {
37
+ lastCallerDefaults.current = callerDefaults
38
+ }, [callerDefaults])
39
+
35
40
  const onMount = useCallback((props: { formApi: AnyFormApi }) => {
36
41
  liveForm.current = props.formApi
37
42
  }, [])
@@ -65,30 +65,35 @@ function titleInput() {
65
65
  }
66
66
 
67
67
  describe("useForm — reset(values) rebases defaults", () => {
68
- test("reset after submit sticks across rerenders with fixed defaults", async () => {
69
- const user = userEvent.setup()
70
- let state: Captured | undefined
71
- const capture = vi.fn((next: Captured) => {
72
- state = next
73
- })
74
- const record = { title: "Draft" }
75
- const { rerender } = render(<EditTitle capture={capture} record={record} />)
68
+ for (const reactStrictMode of [false, true]) {
69
+ test(`reset after submit sticks across rerenders with fixed defaults (reactStrictMode: ${reactStrictMode})`, async () => {
70
+ const user = userEvent.setup()
71
+ let state: Captured | undefined
72
+ const capture = vi.fn((next: Captured) => {
73
+ state = next
74
+ })
75
+ const record = { title: "Draft" }
76
+ const { rerender } = render(
77
+ <EditTitle capture={capture} record={record} />,
78
+ { reactStrictMode }
79
+ )
76
80
 
77
- await user.clear(titleInput())
78
- await user.type(titleInput(), "Saved title")
79
- await user.click(screen.getByRole("button", { name: SAVE }))
81
+ await user.clear(titleInput())
82
+ await user.type(titleInput(), "Saved title")
83
+ await user.click(screen.getByRole("button", { name: SAVE }))
80
84
 
81
- await waitFor(() => expect(state?.isTouched).toBe(false))
82
- for (const tick of [1, 2, 3]) {
83
- rerender(<EditTitle capture={capture} record={record} tick={tick} />)
84
- }
85
- await act(() => Promise.resolve())
85
+ await waitFor(() => expect(state?.isTouched).toBe(false))
86
+ for (const tick of [1, 2, 3]) {
87
+ rerender(<EditTitle capture={capture} record={record} tick={tick} />)
88
+ }
89
+ await act(() => Promise.resolve())
86
90
 
87
- expect(titleInput().value).toBe("Saved title")
88
- expect(state?.values).toEqual({ title: "Saved title" })
89
- expect(state?.isDefaultValue).toBe(true)
90
- expect(state?.isDirty).toBe(false)
91
- })
91
+ expect(titleInput().value).toBe("Saved title")
92
+ expect(state?.values).toEqual({ title: "Saved title" })
93
+ expect(state?.isDefaultValue).toBe(true)
94
+ expect(state?.isDirty).toBe(false)
95
+ })
96
+ }
92
97
 
93
98
  test("new caller defaults apply while the form is untouched", async () => {
94
99
  let state: Captured | undefined