@playfast/reform-forms 1.2.1 → 1.4.0

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.
package/src/formState.ts CHANGED
@@ -1,12 +1,7 @@
1
1
  import { Function as Fn, Option, Record } from 'effect'
2
- import {
3
- getNestedValue,
4
- moveAt,
5
- recalculateDirtyPaths,
6
- replaceAt,
7
- setNestedValue,
8
- } from './path'
2
+ import { getNestedValue, moveAt, recalculateDirtyPaths, replaceAt, setNestedValue } from './path'
9
3
  import type { FormState } from './formTypes'
4
+ import { movedIndexMap, reindexMetadata, removedIndexMap, swappedIndexMap } from './formReindex'
10
5
 
11
6
  const arrayKeyCounter = { current: 0 }
12
7
  const makeArrayKey = (): string => `form-item-${arrayKeyCounter.current++}`
@@ -103,18 +98,39 @@ const updateKeys = (
103
98
  return { ...state.arrayKeys, [path]: transform(existing) }
104
99
  }
105
100
 
106
- export const setAtPath = <Values>(input: SetPathInput<Values>): FormState<Values> =>
107
- withValues(input.state, setNestedValue(input.state.values, input.path, input.value))
101
+ // The subtree at `path` is being replaced, so every key describing a row inside it
102
+ // is about to describe nothing. Left behind, the next append folds over the stale
103
+ // array and hands a new row the identity of one the user discarded.
104
+ const withoutSubtree = (
105
+ keys: Readonly<Record<string, ReadonlyArray<string>>>,
106
+ path: string,
107
+ ): Record<string, ReadonlyArray<string>> =>
108
+ Record.fromEntries(
109
+ Record.toEntries(keys).filter(
110
+ ([key]) => key !== path && !key.startsWith(`${path}[`) && !key.startsWith(`${path}.`),
111
+ ),
112
+ )
113
+
114
+ export const setAtPath = <Values>(input: SetPathInput<Values>): FormState<Values> => ({
115
+ ...withValues(input.state, setNestedValue(input.state.values, input.path, input.value)),
116
+ arrayKeys: {
117
+ ...withoutSubtree(input.state.arrayKeys, input.path),
118
+ ...arrayKeysFor(input.value, input.path),
119
+ },
120
+ })
108
121
 
109
122
  export const appendAtPath = <Values>(input: SetPathInput<Values>): FormState<Values> => {
110
123
  const elements = currentArray({ source: input.state.values, path: input.path })
111
124
  const nextValues = setNestedValue(input.state.values, input.path, [...elements, input.value])
112
125
  return {
113
126
  ...withValues(input.state, nextValues),
114
- arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) => [
115
- ...keys,
116
- makeArrayKey(),
117
- ]),
127
+ arrayKeys: {
128
+ ...updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) => [...keys, makeArrayKey()]),
129
+ // Arrays nested inside the appended item need keys of their own — `initialState`
130
+ // seeds every array in the tree, and a nested list without them falls back to a
131
+ // positional key, so its rows change identity the first time one is touched.
132
+ ...arrayKeysFor(input.value, `${input.path}[${elements.length}]`),
133
+ },
118
134
  }
119
135
  }
120
136
 
@@ -128,10 +144,19 @@ export const removeAtPath = <Values>(input: RemovePathInput<Values>): FormState<
128
144
  input.path,
129
145
  elements.filter((_, position) => position !== input.index),
130
146
  )
147
+ const moved = reindexMetadata({
148
+ state: input.state,
149
+ path: input.path,
150
+ moveIndex: removedIndexMap(input.index),
151
+ })
131
152
  return {
132
153
  ...withValues(input.state, nextValues),
133
- arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) =>
134
- keys.filter((_, position) => position !== input.index),
154
+ touched: moved.touched,
155
+ errors: moved.errors,
156
+ arrayKeys: updateKeys(
157
+ Fn.unsafeCoerce({ ...input.state, arrayKeys: moved.nestedKeys }),
158
+ input.path,
159
+ (keys) => keys.filter((_, position) => position !== input.index),
135
160
  ),
136
161
  }
137
162
  }
@@ -143,10 +168,25 @@ export const moveAtPath = <Values>(input: MovePathInput<Values>): FormState<Valu
143
168
  return input.state
144
169
  }
145
170
  const nextValues = setNestedValue(input.state.values, input.path, next)
171
+ const moved = reindexMetadata({
172
+ state: input.state,
173
+ path: input.path,
174
+ // `moveAt` accepts `to === elements.length` as "move it to the end", but a move
175
+ // keeps the length, so the item lands one slot short of that. The metadata has to
176
+ // follow the value, not the argument.
177
+ moveIndex: movedIndexMap({
178
+ from: input.from,
179
+ to: Math.min(input.to, elements.length - 1),
180
+ }),
181
+ })
146
182
  return {
147
183
  ...withValues(input.state, nextValues),
148
- arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) =>
149
- moveAt(keys, input.from, input.to),
184
+ touched: moved.touched,
185
+ errors: moved.errors,
186
+ arrayKeys: updateKeys(
187
+ Fn.unsafeCoerce({ ...input.state, arrayKeys: moved.nestedKeys }),
188
+ input.path,
189
+ (keys) => moveAt(keys, input.from, input.to),
150
190
  ),
151
191
  }
152
192
  }
@@ -168,14 +208,24 @@ export const swapAtPath = <Values>(input: SwapPathInput<Values>): FormState<Valu
168
208
  elements[input.first],
169
209
  )
170
210
  const nextValues = setNestedValue(input.state.values, input.path, swapped)
211
+ const moved = reindexMetadata({
212
+ state: input.state,
213
+ path: input.path,
214
+ moveIndex: swappedIndexMap({ first: input.first, second: input.second }),
215
+ })
171
216
  return {
172
217
  ...withValues(input.state, nextValues),
173
- arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) =>
174
- replaceAt(
175
- replaceAt(keys, input.first, keys[input.second] ?? makeArrayKey()),
176
- input.second,
177
- keys[input.first] ?? makeArrayKey(),
178
- ),
218
+ touched: moved.touched,
219
+ errors: moved.errors,
220
+ arrayKeys: updateKeys(
221
+ Fn.unsafeCoerce({ ...input.state, arrayKeys: moved.nestedKeys }),
222
+ input.path,
223
+ (keys) =>
224
+ replaceAt(
225
+ replaceAt(keys, input.first, keys[input.second] ?? makeArrayKey()),
226
+ input.second,
227
+ keys[input.first] ?? makeArrayKey(),
228
+ ),
179
229
  ),
180
230
  }
181
231
  }