@kontsedal/olas-core 0.0.1-rc.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/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/index.cjs +363 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +178 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +178 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +339 -0
- package/dist/index.mjs.map +1 -0
- package/dist/root-BImHnGj1.mjs +3270 -0
- package/dist/root-BImHnGj1.mjs.map +1 -0
- package/dist/root-Bazp5_Ik.cjs +3347 -0
- package/dist/root-Bazp5_Ik.cjs.map +1 -0
- package/dist/testing.cjs +81 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +56 -0
- package/dist/testing.d.cts.map +1 -0
- package/dist/testing.d.mts +56 -0
- package/dist/testing.d.mts.map +1 -0
- package/dist/testing.mjs +78 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-CAMgqCMz.d.mts +816 -0
- package/dist/types-CAMgqCMz.d.mts.map +1 -0
- package/dist/types-emq_lZd7.d.cts +816 -0
- package/dist/types-emq_lZd7.d.cts.map +1 -0
- package/package.json +47 -0
- package/src/__dev__.d.ts +8 -0
- package/src/controller/define.ts +50 -0
- package/src/controller/index.ts +12 -0
- package/src/controller/instance.ts +499 -0
- package/src/controller/root.ts +160 -0
- package/src/controller/types.ts +195 -0
- package/src/devtools.ts +0 -0
- package/src/emitter.ts +79 -0
- package/src/errors.ts +49 -0
- package/src/forms/field.ts +303 -0
- package/src/forms/form-types.ts +130 -0
- package/src/forms/form.ts +640 -0
- package/src/forms/index.ts +2 -0
- package/src/forms/types.ts +1 -0
- package/src/forms/validators.ts +70 -0
- package/src/index.ts +89 -0
- package/src/query/client.ts +934 -0
- package/src/query/define.ts +154 -0
- package/src/query/entry.ts +322 -0
- package/src/query/focus-online.ts +73 -0
- package/src/query/index.ts +3 -0
- package/src/query/infinite.ts +462 -0
- package/src/query/keys.ts +33 -0
- package/src/query/local.ts +113 -0
- package/src/query/mutation.ts +384 -0
- package/src/query/plugin.ts +135 -0
- package/src/query/types.ts +168 -0
- package/src/query/use.ts +321 -0
- package/src/scope.ts +42 -0
- package/src/selection.ts +146 -0
- package/src/signals/index.ts +3 -0
- package/src/signals/readonly.ts +22 -0
- package/src/signals/runtime.ts +115 -0
- package/src/signals/types.ts +31 -0
- package/src/testing.ts +142 -0
- package/src/timing/debounced.ts +32 -0
- package/src/timing/index.ts +2 -0
- package/src/timing/throttled.ts +46 -0
- package/src/utils.ts +13 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import type { Field } from '../controller/types'
|
|
2
|
+
import type { DevtoolsEmitter } from '../devtools'
|
|
3
|
+
import {
|
|
4
|
+
batch,
|
|
5
|
+
type Computed,
|
|
6
|
+
computed,
|
|
7
|
+
effect,
|
|
8
|
+
type ReadSignal,
|
|
9
|
+
type Signal,
|
|
10
|
+
signal,
|
|
11
|
+
} from '../signals'
|
|
12
|
+
import { isAbortError } from '../utils'
|
|
13
|
+
import type { Validator } from './types'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hook attached by `ctx.form` (or `createForm`) so a Field can publish
|
|
17
|
+
* `field:validated` devtools events with its owning controller path + the
|
|
18
|
+
* field's name within the form schema. See devtools §20.9 and FieldImpl.bind.
|
|
19
|
+
*/
|
|
20
|
+
export type FieldDevtoolsOwner = {
|
|
21
|
+
controllerPath: readonly string[]
|
|
22
|
+
fieldName: string
|
|
23
|
+
emitter: DevtoolsEmitter
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class FieldImpl<T> implements Field<T> {
|
|
27
|
+
private readonly value$: Signal<T>
|
|
28
|
+
private readonly errors$: Signal<string[]>
|
|
29
|
+
private readonly touched$: Signal<boolean>
|
|
30
|
+
private readonly dirty$: Signal<boolean>
|
|
31
|
+
private readonly validating$: Signal<boolean>
|
|
32
|
+
private readonly isValid$: Computed<boolean>
|
|
33
|
+
private readonly revalidateTrigger$: Signal<number>
|
|
34
|
+
|
|
35
|
+
private readonly validators: ReadonlyArray<Validator<T>>
|
|
36
|
+
/** The value `reset()` returns to. Mutated by `setAsInitial()` so a form
|
|
37
|
+
* initialized from server data resets to *that* data, not the empty seed. */
|
|
38
|
+
private initial: T
|
|
39
|
+
private validatorDispose: (() => void) | null = null
|
|
40
|
+
private currentAbort: AbortController | null = null
|
|
41
|
+
private runId = 0
|
|
42
|
+
private disposed = false
|
|
43
|
+
private devtoolsOwner: FieldDevtoolsOwner | null = null
|
|
44
|
+
|
|
45
|
+
constructor(initial: T, validators: ReadonlyArray<Validator<T>> = []) {
|
|
46
|
+
this.initial = initial
|
|
47
|
+
this.validators = validators
|
|
48
|
+
this.value$ = signal(initial)
|
|
49
|
+
this.errors$ = signal<string[]>([])
|
|
50
|
+
this.touched$ = signal(false)
|
|
51
|
+
this.dirty$ = signal(false)
|
|
52
|
+
this.validating$ = signal(false)
|
|
53
|
+
this.revalidateTrigger$ = signal(0)
|
|
54
|
+
this.isValid$ = computed(() => this.errors$.value.length === 0 && !this.validating$.value)
|
|
55
|
+
|
|
56
|
+
if (validators.length > 0) {
|
|
57
|
+
this.validatorDispose = effect(() => {
|
|
58
|
+
this.runValidators()
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- ReadSignal<T> ---
|
|
64
|
+
get value(): T {
|
|
65
|
+
return this.value$.value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
peek(): T {
|
|
69
|
+
return this.value$.peek()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
subscribe(handler: (value: T) => void): () => void {
|
|
73
|
+
return this.value$.subscribe(handler)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- Field-only signals ---
|
|
77
|
+
get errors(): ReadSignal<string[]> {
|
|
78
|
+
return this.errors$
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get isValid(): ReadSignal<boolean> {
|
|
82
|
+
return this.isValid$
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get isDirty(): ReadSignal<boolean> {
|
|
86
|
+
return this.dirty$
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get touched(): ReadSignal<boolean> {
|
|
90
|
+
return this.touched$
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get isValidating(): ReadSignal<boolean> {
|
|
94
|
+
return this.validating$
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- mutating methods ---
|
|
98
|
+
set(value: T): void {
|
|
99
|
+
if (this.disposed) return
|
|
100
|
+
this.value$.set(value)
|
|
101
|
+
this.dirty$.set(true)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reseat the field as if this value had been its constructor `initial`.
|
|
106
|
+
* Sets the value, re-anchors `reset()`'s target, and does NOT mark dirty.
|
|
107
|
+
* Used by `Form` when applying its own `initial` (in the constructor and
|
|
108
|
+
* on `reset()`), so server-loaded forms don't start dirty. Internal-ish —
|
|
109
|
+
* exposed for `Form`'s use, not for user code that just wants to write.
|
|
110
|
+
*/
|
|
111
|
+
setAsInitial(value: T): void {
|
|
112
|
+
if (this.disposed) return
|
|
113
|
+
this.initial = value
|
|
114
|
+
batch(() => {
|
|
115
|
+
this.value$.set(value)
|
|
116
|
+
this.dirty$.set(false)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
reset(): void {
|
|
121
|
+
if (this.disposed) return
|
|
122
|
+
this.currentAbort?.abort()
|
|
123
|
+
this.currentAbort = null
|
|
124
|
+
batch(() => {
|
|
125
|
+
this.value$.set(this.initial)
|
|
126
|
+
this.dirty$.set(false)
|
|
127
|
+
this.touched$.set(false)
|
|
128
|
+
this.errors$.set([])
|
|
129
|
+
this.validating$.set(false)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
markTouched(): void {
|
|
134
|
+
if (this.disposed) return
|
|
135
|
+
this.touched$.set(true)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async revalidate(): Promise<boolean> {
|
|
139
|
+
if (this.disposed) return this.isValid$.peek()
|
|
140
|
+
// Bump the trigger to force re-run.
|
|
141
|
+
this.revalidateTrigger$.update((n) => n + 1)
|
|
142
|
+
await this.waitUntilSettled()
|
|
143
|
+
return this.isValid$.peek()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
dispose(): void {
|
|
147
|
+
if (this.disposed) return
|
|
148
|
+
this.disposed = true
|
|
149
|
+
this.validatorDispose?.()
|
|
150
|
+
this.validatorDispose = null
|
|
151
|
+
this.currentAbort?.abort()
|
|
152
|
+
this.currentAbort = null
|
|
153
|
+
this.devtoolsOwner = null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Bind this field to a devtools owner. Each subsequent validation pass
|
|
158
|
+
* publishes a `field:validated` event with the supplied path + name.
|
|
159
|
+
* Idempotent — calling again replaces the owner. Internal: called by
|
|
160
|
+
* `createForm` / `createFieldArray` so the form's keys reach the panel.
|
|
161
|
+
*/
|
|
162
|
+
bindDevtoolsOwner(owner: FieldDevtoolsOwner | null): void {
|
|
163
|
+
this.devtoolsOwner = owner
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private emitValidated(valid: boolean, errors: readonly string[]): void {
|
|
167
|
+
if (!__DEV__) return
|
|
168
|
+
const owner = this.devtoolsOwner
|
|
169
|
+
if (owner === null) return
|
|
170
|
+
owner.emitter.emit({
|
|
171
|
+
type: 'field:validated',
|
|
172
|
+
path: owner.controllerPath,
|
|
173
|
+
field: owner.fieldName,
|
|
174
|
+
valid,
|
|
175
|
+
errors: [...errors],
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- internal ---
|
|
180
|
+
private async waitUntilSettled(): Promise<void> {
|
|
181
|
+
// If a validation pass is in progress, wait for validating$ to become false.
|
|
182
|
+
if (!this.validating$.peek()) return
|
|
183
|
+
await new Promise<void>((resolve) => {
|
|
184
|
+
const unsub = this.validating$.subscribe((v) => {
|
|
185
|
+
if (!v) {
|
|
186
|
+
unsub()
|
|
187
|
+
resolve()
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private runValidators(): void {
|
|
194
|
+
if (this.disposed) return
|
|
195
|
+
|
|
196
|
+
// Track value and revalidate trigger.
|
|
197
|
+
const value = this.value$.value
|
|
198
|
+
void this.revalidateTrigger$.value
|
|
199
|
+
|
|
200
|
+
// Abort previous in-flight run.
|
|
201
|
+
this.currentAbort?.abort()
|
|
202
|
+
const abort = new AbortController()
|
|
203
|
+
this.currentAbort = abort
|
|
204
|
+
const myId = ++this.runId
|
|
205
|
+
|
|
206
|
+
const syncErrors: string[] = []
|
|
207
|
+
const asyncPromises: Promise<string | null>[] = []
|
|
208
|
+
|
|
209
|
+
for (const validator of this.validators) {
|
|
210
|
+
const result = validator(value, abort.signal)
|
|
211
|
+
if (result instanceof Promise) {
|
|
212
|
+
asyncPromises.push(result)
|
|
213
|
+
} else if (result != null) {
|
|
214
|
+
syncErrors.push(result)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (syncErrors.length > 0) {
|
|
219
|
+
batch(() => {
|
|
220
|
+
this.errors$.set(syncErrors)
|
|
221
|
+
this.validating$.set(false)
|
|
222
|
+
})
|
|
223
|
+
this.emitValidated(false, syncErrors)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (asyncPromises.length === 0) {
|
|
228
|
+
batch(() => {
|
|
229
|
+
this.errors$.set([])
|
|
230
|
+
this.validating$.set(false)
|
|
231
|
+
})
|
|
232
|
+
this.emitValidated(true, [])
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
batch(() => {
|
|
237
|
+
this.errors$.set([])
|
|
238
|
+
this.validating$.set(true)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
Promise.allSettled(asyncPromises).then((results) => {
|
|
242
|
+
if (myId !== this.runId || this.disposed) return
|
|
243
|
+
const asyncErrors: string[] = []
|
|
244
|
+
for (const r of results) {
|
|
245
|
+
if (r.status === 'fulfilled') {
|
|
246
|
+
if (r.value != null) asyncErrors.push(r.value)
|
|
247
|
+
} else if (!isAbortError(r.reason)) {
|
|
248
|
+
const msg = r.reason instanceof Error ? r.reason.message : String(r.reason)
|
|
249
|
+
asyncErrors.push(msg)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
batch(() => {
|
|
253
|
+
this.errors$.set(asyncErrors)
|
|
254
|
+
this.validating$.set(false)
|
|
255
|
+
})
|
|
256
|
+
this.emitValidated(asyncErrors.length === 0, asyncErrors)
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Internal — type guard / accessor for the binding hook. Avoids exposing
|
|
263
|
+
* `bindDevtoolsOwner` on the public `Field<T>` type while letting `createForm`
|
|
264
|
+
* call it via a structural check.
|
|
265
|
+
*/
|
|
266
|
+
export function bindFieldDevtoolsOwner<T>(field: Field<T>, owner: FieldDevtoolsOwner | null): void {
|
|
267
|
+
const impl = field as { bindDevtoolsOwner?: (o: FieldDevtoolsOwner | null) => void }
|
|
268
|
+
if (typeof impl.bindDevtoolsOwner === 'function') {
|
|
269
|
+
impl.bindDevtoolsOwner(owner)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function createField<T>(initial: T, validators?: ReadonlyArray<Validator<T>>): Field<T> {
|
|
274
|
+
return new FieldImpl(initial, validators)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Wrap an async validator with a debounce. The debounce timer resets on every
|
|
279
|
+
* value change. While debouncing or the request is in flight, the field's
|
|
280
|
+
* `isValidating` is true and `isValid` is false (treat-as-invalid-until-proven-valid).
|
|
281
|
+
*/
|
|
282
|
+
export function debouncedValidator<T>(
|
|
283
|
+
fn: (value: T, signal: AbortSignal) => Promise<string | null>,
|
|
284
|
+
ms: number,
|
|
285
|
+
): Validator<T> {
|
|
286
|
+
return (value, signal) =>
|
|
287
|
+
new Promise<string | null>((resolve, reject) => {
|
|
288
|
+
if (signal.aborted) {
|
|
289
|
+
reject(new DOMException('Aborted', 'AbortError'))
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
const timer = setTimeout(() => {
|
|
293
|
+
signal.removeEventListener('abort', onAbort)
|
|
294
|
+
fn(value, signal).then(resolve, reject)
|
|
295
|
+
}, ms)
|
|
296
|
+
const onAbort = () => {
|
|
297
|
+
clearTimeout(timer)
|
|
298
|
+
signal.removeEventListener('abort', onAbort)
|
|
299
|
+
reject(new DOMException('Aborted', 'AbortError'))
|
|
300
|
+
}
|
|
301
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
302
|
+
})
|
|
303
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { Field } from '../controller/types'
|
|
2
|
+
import type { ReadSignal } from '../signals/types'
|
|
3
|
+
import type { Validator } from './types'
|
|
4
|
+
|
|
5
|
+
export type FormSchema = {
|
|
6
|
+
[key: string]: Field<any> | Form<any> | FieldArray<any>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type FormValue<S extends FormSchema> = {
|
|
10
|
+
[K in keyof S]: S[K] extends Field<infer T>
|
|
11
|
+
? T
|
|
12
|
+
: S[K] extends Form<infer SS>
|
|
13
|
+
? FormValue<SS>
|
|
14
|
+
: S[K] extends FieldArray<infer I>
|
|
15
|
+
? FieldArrayValue<I>
|
|
16
|
+
: never
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type FormErrors<S extends FormSchema> = {
|
|
20
|
+
[K in keyof S]?: S[K] extends Field<any>
|
|
21
|
+
? string[] | undefined
|
|
22
|
+
: S[K] extends Form<infer SS>
|
|
23
|
+
? FormErrors<SS>
|
|
24
|
+
: S[K] extends FieldArray<infer I>
|
|
25
|
+
? Array<FieldArrayItemErrors<I> | undefined>
|
|
26
|
+
: never
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type FieldArrayValue<I> =
|
|
30
|
+
I extends Field<infer T> ? T[] : I extends Form<infer S> ? FormValue<S>[] : never
|
|
31
|
+
|
|
32
|
+
export type FieldArrayItemErrors<I> =
|
|
33
|
+
I extends Field<any> ? string[] : I extends Form<infer S> ? FormErrors<S> : never
|
|
34
|
+
|
|
35
|
+
export type ItemInitial<I> =
|
|
36
|
+
I extends Field<infer T> ? T : I extends Form<infer S> ? DeepPartial<FormValue<S>> : never
|
|
37
|
+
|
|
38
|
+
export type DeepPartial<T> = T extends object
|
|
39
|
+
? T extends ReadonlyArray<infer U>
|
|
40
|
+
? ReadonlyArray<DeepPartial<U>>
|
|
41
|
+
: { [K in keyof T]?: DeepPartial<T[K]> }
|
|
42
|
+
: T
|
|
43
|
+
|
|
44
|
+
export type FormValidator<S extends FormSchema> = Validator<FormValue<S>>
|
|
45
|
+
export type FieldArrayValidator<I> = Validator<FieldArrayValue<I>>
|
|
46
|
+
|
|
47
|
+
export type FormOptions<S extends FormSchema> = {
|
|
48
|
+
initial?: (() => DeepPartial<FormValue<S>> | undefined) | DeepPartial<FormValue<S>>
|
|
49
|
+
validators?: FormValidator<S>[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type FieldArrayOptions<I> = {
|
|
53
|
+
initial?: Array<ItemInitial<I>>
|
|
54
|
+
validators?: FieldArrayValidator<I>[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A nested form. Created via `ctx.form(schema, options?)`. `value` aggregates
|
|
59
|
+
* every leaf into the structurally-typed `FormValue<S>`; `errors` mirrors that
|
|
60
|
+
* shape with `string[] | undefined`. `flatErrors` is a flattened view useful
|
|
61
|
+
* for rendering a single error summary. Spec §8, §20.7.
|
|
62
|
+
*
|
|
63
|
+
* IMPORTANT: `Form.value` is a `ReadSignal<FormValue<S>>` while `Field.value`
|
|
64
|
+
* is `T` directly — different shapes. See `.wiki/pitfalls/field-value-shape.md`.
|
|
65
|
+
*/
|
|
66
|
+
export type Form<S extends FormSchema> = {
|
|
67
|
+
readonly fields: { [K in keyof S]: S[K] }
|
|
68
|
+
readonly value: ReadSignal<FormValue<S>>
|
|
69
|
+
readonly errors: ReadSignal<FormErrors<S>>
|
|
70
|
+
readonly topLevelErrors: ReadSignal<string[]>
|
|
71
|
+
readonly flatErrors: ReadSignal<Array<{ path: string; errors: string[] }>>
|
|
72
|
+
readonly isValid: ReadSignal<boolean>
|
|
73
|
+
readonly isDirty: ReadSignal<boolean>
|
|
74
|
+
readonly touched: ReadSignal<boolean>
|
|
75
|
+
readonly isValidating: ReadSignal<boolean>
|
|
76
|
+
|
|
77
|
+
/** Deep-merge a partial value into the form, batched. */
|
|
78
|
+
set(partial: DeepPartial<FormValue<S>>): void
|
|
79
|
+
/**
|
|
80
|
+
* Re-seat the form's leaves from `partial` as their new initials —
|
|
81
|
+
* each leaf calls `setAsInitial(value)`, so `isDirty` stays false and a
|
|
82
|
+
* subsequent `reset()` returns *here*. Internal-ish but exported for
|
|
83
|
+
* `Form`-traversal code (nested-form initial application).
|
|
84
|
+
*/
|
|
85
|
+
resetWithInitial(partial: DeepPartial<FormValue<S>>): void
|
|
86
|
+
/** Reset every leaf to its initial value. */
|
|
87
|
+
reset(): void
|
|
88
|
+
/** Mark every leaf as touched (so error messages appear). */
|
|
89
|
+
markAllTouched(): void
|
|
90
|
+
/** Re-run every leaf's validators. Resolves with true if all leaves are valid. */
|
|
91
|
+
validate(): Promise<boolean>
|
|
92
|
+
/** Idempotent. Called by the owning controller's dispose. */
|
|
93
|
+
dispose(): void
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A dynamically-sized list of `Field` or `Form` items. Created via
|
|
98
|
+
* `ctx.fieldArray(itemFactory, options?)`. The factory is invoked per
|
|
99
|
+
* insertion. Spec §8, §20.7.
|
|
100
|
+
*/
|
|
101
|
+
export type FieldArray<I extends Field<any> | Form<any>> = {
|
|
102
|
+
readonly items: ReadSignal<ReadonlyArray<I>>
|
|
103
|
+
readonly value: ReadSignal<FieldArrayValue<I>>
|
|
104
|
+
readonly errors: ReadSignal<Array<FieldArrayItemErrors<I> | undefined>>
|
|
105
|
+
readonly topLevelErrors: ReadSignal<string[]>
|
|
106
|
+
readonly isValid: ReadSignal<boolean>
|
|
107
|
+
readonly isDirty: ReadSignal<boolean>
|
|
108
|
+
readonly touched: ReadSignal<boolean>
|
|
109
|
+
readonly isValidating: ReadSignal<boolean>
|
|
110
|
+
readonly size: ReadSignal<number>
|
|
111
|
+
|
|
112
|
+
add(initial?: ItemInitial<I>): void
|
|
113
|
+
insert(index: number, initial?: ItemInitial<I>): void
|
|
114
|
+
remove(index: number): void
|
|
115
|
+
move(from: number, to: number): void
|
|
116
|
+
at(index: number): I | undefined
|
|
117
|
+
clear(): void
|
|
118
|
+
|
|
119
|
+
reset(): void
|
|
120
|
+
markAllTouched(): void
|
|
121
|
+
validate(): Promise<boolean>
|
|
122
|
+
dispose(): void
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Brand markers used by traversal logic to distinguish primitive types.
|
|
126
|
+
export const FORM_BRAND = Symbol.for('olas.form')
|
|
127
|
+
export const FIELD_ARRAY_BRAND = Symbol.for('olas.fieldArray')
|
|
128
|
+
|
|
129
|
+
export type FormBranded = { readonly [FORM_BRAND]: true }
|
|
130
|
+
export type FieldArrayBranded = { readonly [FIELD_ARRAY_BRAND]: true }
|