@tanstack/form-core 0.0.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.
- package/LICENSE +21 -0
- package/build/cjs/FieldApi.js +259 -0
- package/build/cjs/FieldApi.js.map +1 -0
- package/build/cjs/FormApi.js +316 -0
- package/build/cjs/FormApi.js.map +1 -0
- package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +31 -0
- package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
- package/build/cjs/index.js +27 -0
- package/build/cjs/index.js.map +1 -0
- package/build/cjs/utils.js +88 -0
- package/build/cjs/utils.js.map +1 -0
- package/build/esm/index.js +634 -0
- package/build/esm/index.js.map +1 -0
- package/build/stats-html.html +2689 -0
- package/build/stats-react.json +190 -0
- package/build/types/FieldApi.d.ts +69 -0
- package/build/types/FormApi.d.ts +74 -0
- package/build/types/index.d.ts +167 -0
- package/build/types/tests/test.test.d.ts +0 -0
- package/build/types/utils.d.ts +16 -0
- package/build/umd/index.development.js +696 -0
- package/build/umd/index.development.js.map +1 -0
- package/build/umd/index.production.js +22 -0
- package/build/umd/index.production.js.map +1 -0
- package/package.json +32 -0
- package/src/FieldApi.ts +303 -0
- package/src/FormApi.ts +424 -0
- package/src/index.ts +3 -0
- package/src/tests/test.test.tsx +5 -0
- package/src/utils.ts +144 -0
package/src/FormApi.ts
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import type { FormEvent } from 'react'
|
|
2
|
+
import { Store } from '@tanstack/store'
|
|
3
|
+
//
|
|
4
|
+
import type { DeepKeys, DeepValue, Updater } from './utils'
|
|
5
|
+
import { functionalUpdate, getBy, setBy } from './utils'
|
|
6
|
+
import type { FieldApi, FieldMeta } from './FieldApi'
|
|
7
|
+
|
|
8
|
+
export type FormOptions<TData> = {
|
|
9
|
+
defaultValues?: TData
|
|
10
|
+
defaultState?: Partial<FormState<TData>>
|
|
11
|
+
onSubmit?: (values: TData, formApi: FormApi<TData>) => Promise<any>
|
|
12
|
+
onInvalidSubmit?: (values: TData, formApi: FormApi<TData>) => void
|
|
13
|
+
validate?: (values: TData, formApi: FormApi<TData>) => Promise<any>
|
|
14
|
+
debugForm?: boolean
|
|
15
|
+
validatePristine?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type FieldInfo<TFormData> = {
|
|
19
|
+
instances: Record<string, FieldApi<any, TFormData>>
|
|
20
|
+
} & ValidationMeta
|
|
21
|
+
|
|
22
|
+
export type ValidationMeta = {
|
|
23
|
+
validationCount?: number
|
|
24
|
+
validationPromise?: Promise<ValidationError>
|
|
25
|
+
validationResolve?: (error: ValidationError) => void
|
|
26
|
+
validationReject?: (error: unknown) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ValidationError = undefined | false | null | string
|
|
30
|
+
|
|
31
|
+
export type FormState<TData> = {
|
|
32
|
+
values: TData
|
|
33
|
+
// Form Validation
|
|
34
|
+
isFormValidating: boolean
|
|
35
|
+
formValidationCount: number
|
|
36
|
+
isFormValid: boolean
|
|
37
|
+
formError?: ValidationError
|
|
38
|
+
// Fields
|
|
39
|
+
fieldMeta: Record<DeepKeys<TData>, FieldMeta>
|
|
40
|
+
isFieldsValidating: boolean
|
|
41
|
+
isFieldsValid: boolean
|
|
42
|
+
isSubmitting: boolean
|
|
43
|
+
// General
|
|
44
|
+
isTouched: boolean
|
|
45
|
+
isSubmitted: boolean
|
|
46
|
+
isValidating: boolean
|
|
47
|
+
isValid: boolean
|
|
48
|
+
canSubmit: boolean
|
|
49
|
+
submissionAttempts: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getDefaultFormState<TData>(
|
|
53
|
+
defaultState: Partial<FormState<TData>>,
|
|
54
|
+
): FormState<TData> {
|
|
55
|
+
return {
|
|
56
|
+
values: {} as any,
|
|
57
|
+
fieldMeta: {} as any,
|
|
58
|
+
canSubmit: true,
|
|
59
|
+
isFieldsValid: false,
|
|
60
|
+
isFieldsValidating: false,
|
|
61
|
+
isFormValid: false,
|
|
62
|
+
isFormValidating: false,
|
|
63
|
+
isSubmitted: false,
|
|
64
|
+
isSubmitting: false,
|
|
65
|
+
isTouched: false,
|
|
66
|
+
isValid: false,
|
|
67
|
+
isValidating: false,
|
|
68
|
+
submissionAttempts: 0,
|
|
69
|
+
formValidationCount: 0,
|
|
70
|
+
...defaultState,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class FormApi<TFormData> {
|
|
75
|
+
// // This carries the context for nested fields
|
|
76
|
+
options: FormOptions<TFormData> = {}
|
|
77
|
+
store!: Store<FormState<TFormData>>
|
|
78
|
+
// Do not use __state directly, as it is not reactive.
|
|
79
|
+
// Please use form.useStore() utility to subscribe to state
|
|
80
|
+
state!: FormState<TFormData>
|
|
81
|
+
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData>> = {} as any
|
|
82
|
+
fieldName?: string
|
|
83
|
+
validationMeta: ValidationMeta = {}
|
|
84
|
+
|
|
85
|
+
constructor(opts?: FormOptions<TFormData>) {
|
|
86
|
+
this.store = new Store<FormState<TFormData>>(
|
|
87
|
+
getDefaultFormState({
|
|
88
|
+
...opts?.defaultState,
|
|
89
|
+
values: opts?.defaultValues ?? opts?.defaultState?.values,
|
|
90
|
+
isFormValid: !opts?.validate,
|
|
91
|
+
}),
|
|
92
|
+
{
|
|
93
|
+
onUpdate: (next) => {
|
|
94
|
+
// Computed state
|
|
95
|
+
const fieldMetaValues = Object.values(next.fieldMeta) as (
|
|
96
|
+
| FieldMeta
|
|
97
|
+
| undefined
|
|
98
|
+
)[]
|
|
99
|
+
|
|
100
|
+
const isFieldsValidating = fieldMetaValues.some(
|
|
101
|
+
(field) => field?.isValidating,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const isFieldsValid = !fieldMetaValues.some((field) => field?.error)
|
|
105
|
+
|
|
106
|
+
const isTouched = fieldMetaValues.some((field) => field?.isTouched)
|
|
107
|
+
|
|
108
|
+
const isValidating = isFieldsValidating || next.isFormValidating
|
|
109
|
+
const isFormValid = !next.formError
|
|
110
|
+
const isValid = isFieldsValid && isFormValid
|
|
111
|
+
const canSubmit =
|
|
112
|
+
(next.submissionAttempts === 0 && !isTouched) ||
|
|
113
|
+
(!isValidating && !next.isSubmitting && isValid)
|
|
114
|
+
|
|
115
|
+
next = {
|
|
116
|
+
...next,
|
|
117
|
+
isFieldsValidating,
|
|
118
|
+
isFieldsValid,
|
|
119
|
+
isFormValid,
|
|
120
|
+
isValid,
|
|
121
|
+
canSubmit,
|
|
122
|
+
isTouched,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create a shortcut for the state
|
|
126
|
+
// Write it back to the store
|
|
127
|
+
this.store.state = next
|
|
128
|
+
this.state = next
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
this.state = this.store.state
|
|
134
|
+
|
|
135
|
+
this.update(opts || {})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
update = (options: FormOptions<TFormData>) => {
|
|
139
|
+
this.store.batch(() => {
|
|
140
|
+
if (
|
|
141
|
+
options.defaultState &&
|
|
142
|
+
options.defaultState !== this.options.defaultState
|
|
143
|
+
) {
|
|
144
|
+
this.store.setState((prev) => ({
|
|
145
|
+
...prev,
|
|
146
|
+
...options.defaultState,
|
|
147
|
+
}))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (options.defaultValues !== this.options.defaultValues) {
|
|
151
|
+
this.store.setState((prev) => ({
|
|
152
|
+
...prev,
|
|
153
|
+
values: options.defaultValues as TFormData,
|
|
154
|
+
}))
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
this.options = options
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
reset = () =>
|
|
162
|
+
this.store.setState(() => getDefaultFormState(this.options.defaultValues!))
|
|
163
|
+
|
|
164
|
+
validateAllFields = async () => {
|
|
165
|
+
const fieldValidationPromises: Promise<ValidationError>[] = [] as any
|
|
166
|
+
|
|
167
|
+
this.store.batch(() => {
|
|
168
|
+
void (Object.values(this.fieldInfo) as FieldInfo<any>[]).forEach(
|
|
169
|
+
(field) => {
|
|
170
|
+
Object.values(field.instances).forEach((instance) => {
|
|
171
|
+
// If any fields are not touched
|
|
172
|
+
if (!instance.state.meta.isTouched) {
|
|
173
|
+
// Mark them as touched
|
|
174
|
+
instance.setMeta((prev) => ({ ...prev, isTouched: true }))
|
|
175
|
+
// Validate the field
|
|
176
|
+
if (instance.options.validate) {
|
|
177
|
+
fieldValidationPromises.push(instance.validate())
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
return Promise.all(fieldValidationPromises)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
validateForm = async () => {
|
|
189
|
+
const { validate } = this.options
|
|
190
|
+
|
|
191
|
+
if (!validate) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Use the formValidationCount for all field instances to
|
|
196
|
+
// track freshness of the validation
|
|
197
|
+
this.store.setState((prev) => ({
|
|
198
|
+
...prev,
|
|
199
|
+
isValidating: true,
|
|
200
|
+
formValidationCount: prev.formValidationCount + 1,
|
|
201
|
+
}))
|
|
202
|
+
|
|
203
|
+
const formValidationCount = this.state.formValidationCount
|
|
204
|
+
|
|
205
|
+
const checkLatest = () =>
|
|
206
|
+
formValidationCount === this.state.formValidationCount
|
|
207
|
+
|
|
208
|
+
if (!this.validationMeta.validationPromise) {
|
|
209
|
+
this.validationMeta.validationPromise = new Promise((resolve, reject) => {
|
|
210
|
+
this.validationMeta.validationResolve = resolve
|
|
211
|
+
this.validationMeta.validationReject = reject
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const doValidation = async () => {
|
|
216
|
+
try {
|
|
217
|
+
const error = await validate(this.state.values, this)
|
|
218
|
+
|
|
219
|
+
if (checkLatest()) {
|
|
220
|
+
this.store.setState((prev) => ({
|
|
221
|
+
...prev,
|
|
222
|
+
isValidating: false,
|
|
223
|
+
error: error
|
|
224
|
+
? typeof error === 'string'
|
|
225
|
+
? error
|
|
226
|
+
: 'Invalid Form Values'
|
|
227
|
+
: null,
|
|
228
|
+
}))
|
|
229
|
+
|
|
230
|
+
this.validationMeta.validationResolve?.(error)
|
|
231
|
+
}
|
|
232
|
+
} catch (err) {
|
|
233
|
+
if (checkLatest()) {
|
|
234
|
+
this.validationMeta.validationReject?.(err)
|
|
235
|
+
}
|
|
236
|
+
} finally {
|
|
237
|
+
delete this.validationMeta.validationPromise
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
doValidation()
|
|
242
|
+
|
|
243
|
+
return this.validationMeta.validationPromise
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
handleSubmit = async (e: FormEvent & { __handled?: boolean }) => {
|
|
247
|
+
e.preventDefault()
|
|
248
|
+
e.stopPropagation()
|
|
249
|
+
|
|
250
|
+
// Check to see that the form and all fields have been touched
|
|
251
|
+
// If they have not, touch them all and run validation
|
|
252
|
+
// Run form validation
|
|
253
|
+
// Submit the form
|
|
254
|
+
|
|
255
|
+
this.store.setState((old) => ({
|
|
256
|
+
...old,
|
|
257
|
+
// Submittion attempts mark the form as not submitted
|
|
258
|
+
isSubmitted: false,
|
|
259
|
+
// Count submission attempts
|
|
260
|
+
submissionAttempts: old.submissionAttempts + 1,
|
|
261
|
+
}))
|
|
262
|
+
|
|
263
|
+
// Don't let invalid forms submit
|
|
264
|
+
if (!this.state.canSubmit) return
|
|
265
|
+
|
|
266
|
+
this.store.setState((d) => ({ ...d, isSubmitting: true }))
|
|
267
|
+
|
|
268
|
+
const done = () => {
|
|
269
|
+
this.store.setState((prev) => ({ ...prev, isSubmitting: false }))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Validate all fields
|
|
273
|
+
await this.validateAllFields()
|
|
274
|
+
|
|
275
|
+
// Fields are invalid, do not submit
|
|
276
|
+
if (!this.state.isFieldsValid) {
|
|
277
|
+
done()
|
|
278
|
+
this.options.onInvalidSubmit?.(this.state.values, this)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Run validation for the form
|
|
283
|
+
await this.validateForm()
|
|
284
|
+
|
|
285
|
+
if (!this.state.isValid) {
|
|
286
|
+
done()
|
|
287
|
+
this.options.onInvalidSubmit?.(this.state.values, this)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
// Run the submit code
|
|
293
|
+
await this.options.onSubmit?.(this.state.values, this)
|
|
294
|
+
|
|
295
|
+
this.store.batch(() => {
|
|
296
|
+
this.store.setState((prev) => ({ ...prev, isSubmitted: true }))
|
|
297
|
+
done()
|
|
298
|
+
})
|
|
299
|
+
} catch (err) {
|
|
300
|
+
done()
|
|
301
|
+
throw err
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
getFieldValue = <TField extends DeepKeys<TFormData>>(
|
|
306
|
+
field: TField,
|
|
307
|
+
): DeepValue<TFormData, TField> => getBy(this.state.values, field)
|
|
308
|
+
|
|
309
|
+
getFieldMeta = <TField extends DeepKeys<TFormData>>(
|
|
310
|
+
field: TField,
|
|
311
|
+
): FieldMeta => {
|
|
312
|
+
return this.state.fieldMeta[field]
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
getFieldInfo = <TField extends DeepKeys<TFormData>>(field: TField) => {
|
|
316
|
+
return (this.fieldInfo[field] ||= {
|
|
317
|
+
instances: {},
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
setFieldMeta = <TField extends DeepKeys<TFormData>>(
|
|
322
|
+
field: TField,
|
|
323
|
+
updater: Updater<FieldMeta>,
|
|
324
|
+
) => {
|
|
325
|
+
this.store.setState((prev) => {
|
|
326
|
+
return {
|
|
327
|
+
...prev,
|
|
328
|
+
fieldMeta: {
|
|
329
|
+
...prev.fieldMeta,
|
|
330
|
+
[field]: functionalUpdate(updater, prev.fieldMeta[field]),
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
setFieldValue = <TField extends DeepKeys<TFormData>>(
|
|
337
|
+
field: TField,
|
|
338
|
+
updater: Updater<DeepValue<TFormData, TField>>,
|
|
339
|
+
opts?: { touch?: boolean },
|
|
340
|
+
) => {
|
|
341
|
+
const touch = opts?.touch ?? true
|
|
342
|
+
|
|
343
|
+
this.store.batch(() => {
|
|
344
|
+
this.store.setState((prev) => {
|
|
345
|
+
return {
|
|
346
|
+
...prev,
|
|
347
|
+
values: setBy(prev.values, field, updater),
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
if (touch) {
|
|
352
|
+
this.setFieldMeta(field, (prev) => ({
|
|
353
|
+
...prev,
|
|
354
|
+
isTouched: true,
|
|
355
|
+
}))
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
pushFieldValue = <TField extends DeepKeys<TFormData>>(
|
|
361
|
+
field: TField,
|
|
362
|
+
value: DeepValue<TFormData, TField>,
|
|
363
|
+
opts?: { touch?: boolean },
|
|
364
|
+
) => {
|
|
365
|
+
return this.setFieldValue(
|
|
366
|
+
field,
|
|
367
|
+
(prev) => [...(Array.isArray(prev) ? prev : []), value] as any,
|
|
368
|
+
opts,
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
insertFieldValue = <TField extends DeepKeys<TFormData>>(
|
|
373
|
+
field: TField,
|
|
374
|
+
index: number,
|
|
375
|
+
value: DeepValue<TFormData, TField>,
|
|
376
|
+
opts?: { touch?: boolean },
|
|
377
|
+
) => {
|
|
378
|
+
this.setFieldValue(
|
|
379
|
+
field,
|
|
380
|
+
(prev) => {
|
|
381
|
+
// invariant( // TODO: bring in invariant
|
|
382
|
+
// Array.isArray(prev),
|
|
383
|
+
// `Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
|
|
384
|
+
// )
|
|
385
|
+
return (prev as DeepValue<TFormData, TField>[]).map((d, i) =>
|
|
386
|
+
i === index ? value : d,
|
|
387
|
+
) as any
|
|
388
|
+
},
|
|
389
|
+
opts,
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
spliceFieldValue = <TField extends DeepKeys<TFormData>>(
|
|
394
|
+
field: TField,
|
|
395
|
+
index: number,
|
|
396
|
+
opts?: { touch?: boolean },
|
|
397
|
+
) => {
|
|
398
|
+
this.setFieldValue(
|
|
399
|
+
field,
|
|
400
|
+
(prev) => {
|
|
401
|
+
// invariant( // TODO: bring in invariant
|
|
402
|
+
// Array.isArray(prev),
|
|
403
|
+
// `Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
|
|
404
|
+
// )
|
|
405
|
+
return (prev as DeepValue<TFormData, TField>[]).filter(
|
|
406
|
+
(_d, i) => i !== index,
|
|
407
|
+
) as any
|
|
408
|
+
},
|
|
409
|
+
opts,
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
swapFieldValues = <TField extends DeepKeys<TFormData>>(
|
|
414
|
+
field: TField,
|
|
415
|
+
index1: number,
|
|
416
|
+
index2: number,
|
|
417
|
+
) => {
|
|
418
|
+
this.setFieldValue(field, (prev: any) => {
|
|
419
|
+
const prev1 = prev[index1]!
|
|
420
|
+
const prev2 = prev[index2]!
|
|
421
|
+
return setBy(setBy(prev, [index1], prev2), [index2], prev1)
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
}
|
package/src/index.ts
ADDED
package/src/utils.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export type UpdaterFn<TInput, TOutput = TInput> = (input: TInput) => TOutput
|
|
2
|
+
|
|
3
|
+
export type Updater<TInput, TOutput = TInput> =
|
|
4
|
+
| TOutput
|
|
5
|
+
| UpdaterFn<TInput, TOutput>
|
|
6
|
+
|
|
7
|
+
export function functionalUpdate<TInput, TOutput = TInput>(
|
|
8
|
+
updater: Updater<TInput, TOutput>,
|
|
9
|
+
input: TInput,
|
|
10
|
+
): TOutput {
|
|
11
|
+
return typeof updater === 'function'
|
|
12
|
+
? (updater as UpdaterFn<TInput, TOutput>)(input)
|
|
13
|
+
: updater
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getBy(obj: any, path: any) {
|
|
17
|
+
if (!path) {
|
|
18
|
+
throw new Error('A path string is required to use getBy')
|
|
19
|
+
}
|
|
20
|
+
const pathArray = makePathArray(path)
|
|
21
|
+
const pathObj = pathArray
|
|
22
|
+
return pathObj.reduce((current: any, pathPart: any) => {
|
|
23
|
+
if (typeof current !== 'undefined') {
|
|
24
|
+
return current[pathPart]
|
|
25
|
+
}
|
|
26
|
+
return undefined
|
|
27
|
+
}, obj)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function setBy(obj: any, _path: any, updater: Updater<any>) {
|
|
31
|
+
const path = makePathArray(_path)
|
|
32
|
+
|
|
33
|
+
function doSet(parent?: any): any {
|
|
34
|
+
if (!path.length) {
|
|
35
|
+
return functionalUpdate(updater, parent)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const key = path.shift()
|
|
39
|
+
|
|
40
|
+
if (typeof key === 'string') {
|
|
41
|
+
if (typeof parent === 'object') {
|
|
42
|
+
return {
|
|
43
|
+
...parent,
|
|
44
|
+
[key]: doSet(parent[key]),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
[key]: doSet(),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof key === 'number') {
|
|
53
|
+
if (Array.isArray(parent)) {
|
|
54
|
+
const prefix = parent.slice(0, key)
|
|
55
|
+
return [
|
|
56
|
+
...(prefix.length ? prefix : new Array(key)),
|
|
57
|
+
doSet(parent[key]),
|
|
58
|
+
...parent.slice(key + 1),
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
return [...new Array(key), doSet()]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw new Error('Uh oh!')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return doSet(obj)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const reFindNumbers0 = /^(\d*)$/gm
|
|
71
|
+
const reFindNumbers1 = /\.(\d*)\./gm
|
|
72
|
+
const reFindNumbers2 = /^(\d*)\./gm
|
|
73
|
+
const reFindNumbers3 = /\.(\d*$)/gm
|
|
74
|
+
const reFindMultiplePeriods = /\.{2,}/gm
|
|
75
|
+
|
|
76
|
+
function makePathArray(str: string) {
|
|
77
|
+
return str
|
|
78
|
+
.replace('[', '.')
|
|
79
|
+
.replace(']', '')
|
|
80
|
+
.replace(reFindNumbers0, '__int__$1')
|
|
81
|
+
.replace(reFindNumbers1, '.__int__$1.')
|
|
82
|
+
.replace(reFindNumbers2, '__int__$1.')
|
|
83
|
+
.replace(reFindNumbers3, '.__int__$1')
|
|
84
|
+
.replace(reFindMultiplePeriods, '.')
|
|
85
|
+
.split('.')
|
|
86
|
+
.map((d) => {
|
|
87
|
+
if (d.indexOf('__int__') === 0) {
|
|
88
|
+
return parseInt(d.substring('__int__'.length), 10)
|
|
89
|
+
}
|
|
90
|
+
return d
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type RequiredByKey<T, K extends keyof T> = Omit<T, K> &
|
|
95
|
+
Required<Pick<T, K>>
|
|
96
|
+
|
|
97
|
+
type ComputeRange<
|
|
98
|
+
N extends number,
|
|
99
|
+
Result extends Array<unknown> = [],
|
|
100
|
+
> = Result['length'] extends N
|
|
101
|
+
? Result
|
|
102
|
+
: ComputeRange<N, [...Result, Result['length']]>
|
|
103
|
+
type Index40 = ComputeRange<40>[number]
|
|
104
|
+
|
|
105
|
+
// Is this type a tuple?
|
|
106
|
+
type IsTuple<T> = T extends readonly any[] & { length: infer Length }
|
|
107
|
+
? Length extends Index40
|
|
108
|
+
? T
|
|
109
|
+
: never
|
|
110
|
+
: never
|
|
111
|
+
|
|
112
|
+
// If this type is a tuple, what indices are allowed?
|
|
113
|
+
type AllowedIndexes<
|
|
114
|
+
Tuple extends ReadonlyArray<any>,
|
|
115
|
+
Keys extends number = never,
|
|
116
|
+
> = Tuple extends readonly []
|
|
117
|
+
? Keys
|
|
118
|
+
: Tuple extends readonly [infer _, ...infer Tail]
|
|
119
|
+
? AllowedIndexes<Tail, Keys | Tail['length']>
|
|
120
|
+
: Keys
|
|
121
|
+
|
|
122
|
+
export type DeepKeys<T> = unknown extends T
|
|
123
|
+
? keyof T
|
|
124
|
+
: object extends T
|
|
125
|
+
? string
|
|
126
|
+
: T extends readonly any[] & IsTuple<T>
|
|
127
|
+
? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>>
|
|
128
|
+
: T extends any[]
|
|
129
|
+
? never & 'Dynamic length array indexing is not supported'
|
|
130
|
+
: T extends Date
|
|
131
|
+
? never
|
|
132
|
+
: T extends object
|
|
133
|
+
? (keyof T & string) | DeepKeysPrefix<T, keyof T>
|
|
134
|
+
: never
|
|
135
|
+
|
|
136
|
+
type DeepKeysPrefix<T, TPrefix> = TPrefix extends keyof T & (number | string)
|
|
137
|
+
? `${TPrefix}.${DeepKeys<T[TPrefix]> & string}`
|
|
138
|
+
: never
|
|
139
|
+
|
|
140
|
+
export type DeepValue<T, TProp> = T extends Record<string | number, any>
|
|
141
|
+
? TProp extends `${infer TBranch}.${infer TDeepProp}`
|
|
142
|
+
? DeepValue<T[TBranch], TDeepProp>
|
|
143
|
+
: T[TProp & string]
|
|
144
|
+
: never
|