oxform-core 0.1.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/export/index.ts +7 -0
- package/export/schema.ts +1 -0
- package/package.json +40 -0
- package/readme.md +13 -0
- package/src/field-api.constants.ts +15 -0
- package/src/field-api.ts +139 -0
- package/src/form-api.ts +84 -0
- package/src/form-api.types.ts +148 -0
- package/src/form-array-field-api.ts +233 -0
- package/src/form-context-api.ts +232 -0
- package/src/form-field-api.ts +174 -0
- package/src/more-types.ts +178 -0
- package/src/tests/array/append.spec.ts +138 -0
- package/src/tests/array/insert.spec.ts +182 -0
- package/src/tests/array/move.spec.ts +175 -0
- package/src/tests/array/prepend.spec.ts +138 -0
- package/src/tests/array/remove.spec.ts +174 -0
- package/src/tests/array/swap.spec.ts +152 -0
- package/src/tests/array/update.spec.ts +148 -0
- package/src/tests/field/change.spec.ts +226 -0
- package/src/tests/field/reset.spec.ts +617 -0
- package/src/tests/field/set-errors.spec.ts +254 -0
- package/src/tests/field-api/field-api.spec.ts +341 -0
- package/src/tests/form-api/reset.spec.ts +535 -0
- package/src/tests/form-api/submit.spec.ts +409 -0
- package/src/types.ts +5 -0
- package/src/utils/get.ts +5 -0
- package/src/utils/testing/sleep.ts +1 -0
- package/src/utils/testing/tests.ts +18 -0
- package/src/utils/update.ts +6 -0
- package/src/utils/validate.ts +8 -0
- package/tsconfig.json +3 -0
- package/tsdown.config.ts +10 -0
- package/vitest.config.ts +3 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { defaultMeta, defaultStatus } from '#field-api.constants';
|
|
2
|
+
import type {
|
|
3
|
+
FieldMeta,
|
|
4
|
+
FormBaseStore,
|
|
5
|
+
FormIssue,
|
|
6
|
+
FormOptions,
|
|
7
|
+
FormStore,
|
|
8
|
+
PersistedFieldMeta,
|
|
9
|
+
PersistedFormStatus,
|
|
10
|
+
ValidateOptions,
|
|
11
|
+
} from '#form-api.types';
|
|
12
|
+
import type { DeepKeys } from '#more-types';
|
|
13
|
+
import type { StandardSchema } from '#types';
|
|
14
|
+
import { get } from '#utils/get';
|
|
15
|
+
import { validate } from '#utils/validate';
|
|
16
|
+
import { Derived, Store } from '@tanstack/store';
|
|
17
|
+
import { isDeepEqual, isFunction, mergeDeep, stringToPath } from 'remeda';
|
|
18
|
+
|
|
19
|
+
export class FormContextApi<
|
|
20
|
+
Schema extends StandardSchema,
|
|
21
|
+
Values extends StandardSchema.InferInput<Schema> = StandardSchema.InferInput<Schema>,
|
|
22
|
+
Field extends DeepKeys<Values> = DeepKeys<Values>,
|
|
23
|
+
> {
|
|
24
|
+
public options!: FormOptions<Schema>;
|
|
25
|
+
public persisted: Store<FormBaseStore<Schema>>;
|
|
26
|
+
public store!: Derived<FormStore<Schema>>;
|
|
27
|
+
|
|
28
|
+
constructor(options: FormOptions<Schema>) {
|
|
29
|
+
const values = mergeDeep(options.defaultValues as never, options.values ?? {}) as Values;
|
|
30
|
+
|
|
31
|
+
this.options = options;
|
|
32
|
+
|
|
33
|
+
this.persisted = new Store<FormBaseStore<Schema>>({
|
|
34
|
+
values,
|
|
35
|
+
fields: {},
|
|
36
|
+
refs: {},
|
|
37
|
+
status: defaultStatus,
|
|
38
|
+
errors: {},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.store = new Derived<FormStore<Schema>>({
|
|
42
|
+
deps: [this.persisted],
|
|
43
|
+
fn: ({ currDepVals }) => {
|
|
44
|
+
const persisted = currDepVals[0] as FormBaseStore<Schema>;
|
|
45
|
+
|
|
46
|
+
const invalid = Object.values(persisted.errors).some(issues => issues.length > 0);
|
|
47
|
+
const dirty = Object.values(persisted.fields).some(meta => meta.dirty);
|
|
48
|
+
const fields = Object.fromEntries(
|
|
49
|
+
Object.entries(persisted.fields).map(([key, meta]) => {
|
|
50
|
+
return [key, this.buildFieldMeta(key, meta, persisted.values as Values, persisted.errors)];
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
...persisted,
|
|
56
|
+
fields,
|
|
57
|
+
status: {
|
|
58
|
+
...persisted.status,
|
|
59
|
+
submitted: persisted.status.submits > 0,
|
|
60
|
+
valid: !invalid,
|
|
61
|
+
dirty: persisted.status.dirty || dirty,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public get status() {
|
|
69
|
+
return this.store.state.status;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public get values() {
|
|
73
|
+
return this.store.state.values;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private get validator() {
|
|
77
|
+
const store = this.store.state;
|
|
78
|
+
const validate = this.options.validate;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
change: isFunction(validate?.change) ? validate.change(store) : validate?.change,
|
|
82
|
+
submit: isFunction(validate?.submit) ? validate.submit(store) : (validate?.submit ?? this.options.schema),
|
|
83
|
+
blur: isFunction(validate?.blur) ? validate.blur(store) : validate?.blur,
|
|
84
|
+
focus: isFunction(validate?.focus) ? validate.focus(store) : validate?.focus,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public buildFieldMeta = (
|
|
89
|
+
fieldName: string,
|
|
90
|
+
persistedMeta: PersistedFieldMeta | undefined,
|
|
91
|
+
values: Values,
|
|
92
|
+
errors: Record<string, FormIssue[]>,
|
|
93
|
+
): FieldMeta => {
|
|
94
|
+
const path = stringToPath(fieldName);
|
|
95
|
+
const value = get(values as never, path);
|
|
96
|
+
const defaultValue = get(this.options.defaultValues, path);
|
|
97
|
+
const invalid = errors[fieldName]?.length > 0;
|
|
98
|
+
const baseMeta = persistedMeta ?? defaultMeta;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
...baseMeta,
|
|
102
|
+
default: isDeepEqual(value, defaultValue),
|
|
103
|
+
pristine: !baseMeta.dirty,
|
|
104
|
+
valid: !invalid,
|
|
105
|
+
} satisfies FieldMeta;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
public setFieldMeta = (name: string, meta: Partial<PersistedFieldMeta>) => {
|
|
109
|
+
this.persisted.setState(current => {
|
|
110
|
+
return {
|
|
111
|
+
...current,
|
|
112
|
+
fields: {
|
|
113
|
+
...current.fields,
|
|
114
|
+
[name]: {
|
|
115
|
+
...defaultMeta,
|
|
116
|
+
...this.persisted.state.fields[name],
|
|
117
|
+
...meta,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
public resetFieldMeta = (name: string) => {
|
|
125
|
+
this.persisted.setState(current => {
|
|
126
|
+
const fields = { ...current.fields };
|
|
127
|
+
const all = Object.keys(current.fields);
|
|
128
|
+
const affected = all.filter(key => key.startsWith(name));
|
|
129
|
+
|
|
130
|
+
for (const key of affected) {
|
|
131
|
+
delete fields[key];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
...current,
|
|
136
|
+
fields,
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
public recomputeFieldMeta = (name: string) => {
|
|
142
|
+
this.persisted.setState(current => {
|
|
143
|
+
const related: string[] = this.options.related?.[name as never] ?? [];
|
|
144
|
+
const all = Object.keys(current.fields);
|
|
145
|
+
const affected = all.filter(key => key.startsWith(name) || related.includes(key));
|
|
146
|
+
const updated = affected.reduce(
|
|
147
|
+
(acc, key) => {
|
|
148
|
+
return {
|
|
149
|
+
...acc,
|
|
150
|
+
[key]: this.buildFieldMeta(key, current.fields[key], current.values as Values, current.errors),
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
{} as Record<string, FieldMeta>,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
...current,
|
|
158
|
+
fields: {
|
|
159
|
+
...current.fields,
|
|
160
|
+
...updated,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
public setStatus = (status: Partial<PersistedFormStatus>) => {
|
|
167
|
+
this.persisted.setState(current => {
|
|
168
|
+
return {
|
|
169
|
+
...current,
|
|
170
|
+
status: {
|
|
171
|
+
...current.status,
|
|
172
|
+
...status,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
public validate = async (field?: Field | Field[], options?: ValidateOptions) => {
|
|
179
|
+
const validator = options?.type ? this.validator[options.type] : this.options.schema;
|
|
180
|
+
|
|
181
|
+
if (!validator) return [];
|
|
182
|
+
|
|
183
|
+
this.setStatus({ validating: true });
|
|
184
|
+
|
|
185
|
+
const { issues: allIssues } = await validate(validator, this.store.state.values);
|
|
186
|
+
|
|
187
|
+
if (!allIssues) {
|
|
188
|
+
this.persisted.setState(current => {
|
|
189
|
+
return { ...current, errors: {}, status: { ...current.status, validating: false } };
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const fields = field ? (Array.isArray(field) ? field : [field]) : undefined;
|
|
196
|
+
const related: string[] = fields?.flatMap(field => this.options.related?.[field as never] ?? []) ?? [];
|
|
197
|
+
const affected = [...(fields ?? []), ...related];
|
|
198
|
+
|
|
199
|
+
const issues = allIssues.filter(issue => {
|
|
200
|
+
const path = issue.path?.join('.') ?? 'root';
|
|
201
|
+
return !fields || affected.some(key => path.startsWith(key));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const errors = issues.reduce((acc, issue) => {
|
|
205
|
+
const path = issue.path?.join('.') ?? 'root';
|
|
206
|
+
return {
|
|
207
|
+
...acc,
|
|
208
|
+
[path]: [...(acc[path] ?? []), issue],
|
|
209
|
+
};
|
|
210
|
+
}, {} as any);
|
|
211
|
+
|
|
212
|
+
this.persisted.setState(current => {
|
|
213
|
+
const existing = { ...current.errors };
|
|
214
|
+
|
|
215
|
+
for (const key of affected) {
|
|
216
|
+
delete existing[key];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
...current,
|
|
221
|
+
errors: {
|
|
222
|
+
...(fields ? existing : {}), // when validating a specific set of fields, keep the existing errors
|
|
223
|
+
...errors,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
this.setStatus({ validating: false });
|
|
229
|
+
|
|
230
|
+
return issues;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { defaultMeta } from '#field-api.constants';
|
|
2
|
+
import type { FieldChangeOptions, FormIssue, FormResetFieldOptions, FormSetErrorsOptions } from '#form-api.types';
|
|
3
|
+
import type { FormContextApi } from '#form-context-api';
|
|
4
|
+
import type { DeepKeys, DeepValue } from '#more-types';
|
|
5
|
+
import type { StandardSchema } from '#types';
|
|
6
|
+
import { get } from '#utils/get';
|
|
7
|
+
import { update, type Updater } from '#utils/update';
|
|
8
|
+
import { batch } from '@tanstack/store';
|
|
9
|
+
import { setPath, stringToPath } from 'remeda';
|
|
10
|
+
|
|
11
|
+
export class FormFieldApi<
|
|
12
|
+
Schema extends StandardSchema,
|
|
13
|
+
Values extends StandardSchema.InferInput<Schema> = StandardSchema.InferInput<Schema>,
|
|
14
|
+
Field extends DeepKeys<Values> = DeepKeys<Values>,
|
|
15
|
+
> {
|
|
16
|
+
constructor(private context: FormContextApi<Schema>) {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Changes the value of a specific field with optional control over side effects.
|
|
20
|
+
* @param name - The name of the field to change
|
|
21
|
+
* @param value - The new value to set for the field
|
|
22
|
+
* @param options - Optional configuration for controlling validation, dirty state, and touched state
|
|
23
|
+
*/
|
|
24
|
+
public change = <Name extends Field>(
|
|
25
|
+
name: Name,
|
|
26
|
+
updater: Updater<DeepValue<Values, Name>>,
|
|
27
|
+
options?: FieldChangeOptions,
|
|
28
|
+
) => {
|
|
29
|
+
const shouldDirty = options?.should?.dirty !== false;
|
|
30
|
+
const shouldTouch = options?.should?.touch !== false;
|
|
31
|
+
const shouldValidate = options?.should?.validate !== false;
|
|
32
|
+
|
|
33
|
+
this.context.persisted.setState(current => {
|
|
34
|
+
const value = get(current.values as never, stringToPath(name)) as DeepValue<Values, Name>;
|
|
35
|
+
|
|
36
|
+
const values = setPath(
|
|
37
|
+
current.values as never,
|
|
38
|
+
stringToPath(name) as never,
|
|
39
|
+
update(updater, value) as never,
|
|
40
|
+
) as Values;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...current,
|
|
44
|
+
values,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (shouldValidate) void this.context.validate(name, { type: 'change' });
|
|
49
|
+
|
|
50
|
+
batch(() => {
|
|
51
|
+
if (shouldDirty) this.context.setFieldMeta(name, { dirty: true });
|
|
52
|
+
if (shouldTouch) this.context.setFieldMeta(name, { touched: true });
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
public focus = <Name extends Field>(name: Name) => {
|
|
57
|
+
const ref = this.context.store.state.refs[name as never];
|
|
58
|
+
|
|
59
|
+
if (ref) ref.focus();
|
|
60
|
+
|
|
61
|
+
this.context.setFieldMeta(name as never, { touched: true });
|
|
62
|
+
void this.context.validate(name as never, { type: 'focus' });
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
public blur = <Name extends Field>(name: Name) => {
|
|
66
|
+
const ref = this.context.store.state.refs[name as never];
|
|
67
|
+
|
|
68
|
+
if (ref) ref.blur();
|
|
69
|
+
|
|
70
|
+
this.context.setFieldMeta(name as never, { blurred: true });
|
|
71
|
+
void this.context.validate(name as never, { type: 'blur' });
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
public get = <Name extends Field>(name: Name) => {
|
|
75
|
+
return get(this.context.store.state.values as never, stringToPath(name)) as DeepValue<Values, Name>;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
public meta = <Name extends Field>(name: Name) => {
|
|
79
|
+
const meta = this.context.store.state.fields[name];
|
|
80
|
+
|
|
81
|
+
if (meta) return meta;
|
|
82
|
+
|
|
83
|
+
const updated = this.context.buildFieldMeta(
|
|
84
|
+
name,
|
|
85
|
+
undefined,
|
|
86
|
+
this.context.store.state.values as Values,
|
|
87
|
+
this.context.store.state.errors,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
this.context.setFieldMeta(name, updated);
|
|
91
|
+
|
|
92
|
+
return updated;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
public register = <Name extends Field>(name: Name) => {
|
|
96
|
+
return (element: HTMLElement | null) => {
|
|
97
|
+
if (!element) return;
|
|
98
|
+
|
|
99
|
+
this.context.persisted.setState(current => {
|
|
100
|
+
return {
|
|
101
|
+
...current,
|
|
102
|
+
refs: {
|
|
103
|
+
...current.refs,
|
|
104
|
+
[name]: element,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
public errors = <Name extends Field>(name: Name) => {
|
|
112
|
+
return this.context.store.state.errors[name] ?? [];
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
public setErrors = <Name extends Field>(name: Name, errors: FormIssue[], options?: FormSetErrorsOptions) => {
|
|
116
|
+
this.context.persisted.setState(current => {
|
|
117
|
+
const existing = current.errors[name] ?? [];
|
|
118
|
+
let updated: FormIssue[];
|
|
119
|
+
|
|
120
|
+
switch (options?.mode) {
|
|
121
|
+
case 'append':
|
|
122
|
+
updated = [...existing, ...errors];
|
|
123
|
+
break;
|
|
124
|
+
case 'keep':
|
|
125
|
+
updated = existing.length > 0 ? existing : errors;
|
|
126
|
+
break;
|
|
127
|
+
case 'replace':
|
|
128
|
+
default:
|
|
129
|
+
updated = errors;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
...current,
|
|
135
|
+
errors: {
|
|
136
|
+
...current.errors,
|
|
137
|
+
[name]: updated,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
public reset = <Name extends Field>(name: Name, options?: FormResetFieldOptions<DeepValue<Values, Name>>) => {
|
|
144
|
+
const path = stringToPath(name as never);
|
|
145
|
+
const defaultValue = get(this.context.options.defaultValues, path) as DeepValue<Values, Name>;
|
|
146
|
+
const value = options?.value ?? defaultValue;
|
|
147
|
+
|
|
148
|
+
this.context.persisted.setState(current => {
|
|
149
|
+
const values = setPath(current.values as never, path as never, value as never);
|
|
150
|
+
const fields = { ...current.fields };
|
|
151
|
+
const refs = { ...current.refs };
|
|
152
|
+
const errors = { ...current.errors };
|
|
153
|
+
|
|
154
|
+
if (options?.meta) {
|
|
155
|
+
const currentMeta = options?.keep?.meta ? (fields[name as string] ?? defaultMeta) : defaultMeta;
|
|
156
|
+
fields[name as string] = {
|
|
157
|
+
...currentMeta,
|
|
158
|
+
...options.meta,
|
|
159
|
+
};
|
|
160
|
+
} else if (!options?.keep?.meta) delete fields[name as string];
|
|
161
|
+
|
|
162
|
+
if (!options?.keep?.refs) delete refs[name as string];
|
|
163
|
+
if (!options?.keep?.errors) delete errors[name as string];
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
...current,
|
|
167
|
+
values,
|
|
168
|
+
fields,
|
|
169
|
+
refs,
|
|
170
|
+
errors,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// reference: https://github.com/TanStack/form/blob/main/packages/form-core/src/util-types.ts#L161
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @private
|
|
5
|
+
*/
|
|
6
|
+
export type UnwrapOneLevelOfArray<T> = T extends (infer U)[] ? U : T;
|
|
7
|
+
|
|
8
|
+
type Narrowable = string | number | bigint | boolean;
|
|
9
|
+
|
|
10
|
+
type NarrowRaw<A> =
|
|
11
|
+
| (A extends [] ? [] : never)
|
|
12
|
+
| (A extends Narrowable ? A : never)
|
|
13
|
+
| {
|
|
14
|
+
[K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw<A[K]>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Try<A1, A2, Catch = never> = A1 extends A2 ? A1 : Catch;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @private
|
|
21
|
+
*/
|
|
22
|
+
export type Narrow<A> = Try<A, [], NarrowRaw<A>>;
|
|
23
|
+
|
|
24
|
+
export interface AnyDeepKeyAndValue<K extends string = string, V extends any = any> {
|
|
25
|
+
key: K;
|
|
26
|
+
value: V;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ArrayAccessor<TParent extends AnyDeepKeyAndValue> =
|
|
30
|
+
`${TParent['key'] extends never ? '' : TParent['key']}.${number}`;
|
|
31
|
+
|
|
32
|
+
export interface ArrayDeepKeyAndValue<
|
|
33
|
+
in out TParent extends AnyDeepKeyAndValue,
|
|
34
|
+
in out T extends ReadonlyArray<any>,
|
|
35
|
+
> extends AnyDeepKeyAndValue {
|
|
36
|
+
key: ArrayAccessor<TParent>;
|
|
37
|
+
value: T[number] | Nullable<TParent['value']>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type DeepKeyAndValueArray<
|
|
41
|
+
TParent extends AnyDeepKeyAndValue,
|
|
42
|
+
T extends ReadonlyArray<any>,
|
|
43
|
+
TAcc,
|
|
44
|
+
> = DeepKeysAndValuesImpl<
|
|
45
|
+
NonNullable<T[number]>,
|
|
46
|
+
ArrayDeepKeyAndValue<TParent, T>,
|
|
47
|
+
TAcc | ArrayDeepKeyAndValue<TParent, T>
|
|
48
|
+
>;
|
|
49
|
+
|
|
50
|
+
export type TupleAccessor<
|
|
51
|
+
TParent extends AnyDeepKeyAndValue,
|
|
52
|
+
TKey extends string,
|
|
53
|
+
> = `${TParent['key'] extends never ? '' : TParent['key']}[${TKey}]`;
|
|
54
|
+
|
|
55
|
+
export interface TupleDeepKeyAndValue<
|
|
56
|
+
in out TParent extends AnyDeepKeyAndValue,
|
|
57
|
+
in out T,
|
|
58
|
+
in out TKey extends AllTupleKeys<T>,
|
|
59
|
+
> extends AnyDeepKeyAndValue {
|
|
60
|
+
key: TupleAccessor<TParent, TKey>;
|
|
61
|
+
value: T[TKey] | Nullable<TParent['value']>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type AllTupleKeys<T> = T extends any ? keyof T & `${number}` : never;
|
|
65
|
+
|
|
66
|
+
export type DeepKeyAndValueTuple<
|
|
67
|
+
TParent extends AnyDeepKeyAndValue,
|
|
68
|
+
T extends ReadonlyArray<any>,
|
|
69
|
+
TAcc,
|
|
70
|
+
TAllKeys extends AllTupleKeys<T> = AllTupleKeys<T>,
|
|
71
|
+
> = TAllKeys extends any
|
|
72
|
+
? DeepKeysAndValuesImpl<
|
|
73
|
+
NonNullable<T[TAllKeys]>,
|
|
74
|
+
TupleDeepKeyAndValue<TParent, T, TAllKeys>,
|
|
75
|
+
TAcc | TupleDeepKeyAndValue<TParent, T, TAllKeys>
|
|
76
|
+
>
|
|
77
|
+
: never;
|
|
78
|
+
|
|
79
|
+
export type AllObjectKeys<T> = T extends any ? keyof T & (string | number) : never;
|
|
80
|
+
|
|
81
|
+
export type ObjectAccessor<
|
|
82
|
+
TParent extends AnyDeepKeyAndValue,
|
|
83
|
+
TKey extends string | number,
|
|
84
|
+
> = TParent['key'] extends never ? `${TKey}` : `${TParent['key']}.${TKey}`;
|
|
85
|
+
|
|
86
|
+
export type Nullable<T> = T & (undefined | null);
|
|
87
|
+
|
|
88
|
+
export type ObjectValue<TParent extends AnyDeepKeyAndValue, T, TKey extends AllObjectKeys<T>> =
|
|
89
|
+
| T[TKey]
|
|
90
|
+
| Nullable<TParent['value']>;
|
|
91
|
+
|
|
92
|
+
export interface ObjectDeepKeyAndValue<
|
|
93
|
+
in out TParent extends AnyDeepKeyAndValue,
|
|
94
|
+
in out T,
|
|
95
|
+
in out TKey extends AllObjectKeys<T>,
|
|
96
|
+
> extends AnyDeepKeyAndValue {
|
|
97
|
+
key: ObjectAccessor<TParent, TKey>;
|
|
98
|
+
value: ObjectValue<TParent, T, TKey>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type DeepKeyAndValueObject<
|
|
102
|
+
TParent extends AnyDeepKeyAndValue,
|
|
103
|
+
T,
|
|
104
|
+
TAcc,
|
|
105
|
+
TAllKeys extends AllObjectKeys<T> = AllObjectKeys<T>,
|
|
106
|
+
> = TAllKeys extends any
|
|
107
|
+
? DeepKeysAndValuesImpl<
|
|
108
|
+
NonNullable<T[TAllKeys]>,
|
|
109
|
+
ObjectDeepKeyAndValue<TParent, T, TAllKeys>,
|
|
110
|
+
TAcc | ObjectDeepKeyAndValue<TParent, T, TAllKeys>
|
|
111
|
+
>
|
|
112
|
+
: never;
|
|
113
|
+
|
|
114
|
+
export type UnknownAccessor<TParent extends AnyDeepKeyAndValue> = TParent['key'] extends never
|
|
115
|
+
? string
|
|
116
|
+
: `${TParent['key']}.${string}`;
|
|
117
|
+
|
|
118
|
+
export interface UnknownDeepKeyAndValue<TParent extends AnyDeepKeyAndValue> extends AnyDeepKeyAndValue {
|
|
119
|
+
key: UnknownAccessor<TParent>;
|
|
120
|
+
value: unknown;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type DeepKeysAndValues<T> =
|
|
124
|
+
DeepKeysAndValuesImpl<T> extends AnyDeepKeyAndValue ? DeepKeysAndValuesImpl<T> : never;
|
|
125
|
+
|
|
126
|
+
export type DeepKeysAndValuesImpl<T, TParent extends AnyDeepKeyAndValue = never, TAcc = never> = unknown extends T
|
|
127
|
+
? TAcc | UnknownDeepKeyAndValue<TParent>
|
|
128
|
+
: unknown extends T // this stops runaway recursion when T is any
|
|
129
|
+
? T
|
|
130
|
+
: T extends string | number | boolean | bigint | Date
|
|
131
|
+
? TAcc
|
|
132
|
+
: T extends ReadonlyArray<any>
|
|
133
|
+
? number extends T['length']
|
|
134
|
+
? DeepKeyAndValueArray<TParent, T, TAcc>
|
|
135
|
+
: DeepKeyAndValueTuple<TParent, T, TAcc>
|
|
136
|
+
: keyof T extends never
|
|
137
|
+
? TAcc | UnknownDeepKeyAndValue<TParent>
|
|
138
|
+
: T extends object
|
|
139
|
+
? DeepKeyAndValueObject<TParent, T, TAcc>
|
|
140
|
+
: TAcc;
|
|
141
|
+
|
|
142
|
+
export type DeepRecord<T> = {
|
|
143
|
+
[TRecord in DeepKeysAndValues<T> as TRecord['key']]: TRecord['value'];
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The keys of an object or array, deeply nested.
|
|
148
|
+
*/
|
|
149
|
+
export type DeepKeys<T> = unknown extends T ? string : DeepKeysAndValues<T>['key'];
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Infer the type of a deeply nested property within an object or an array.
|
|
153
|
+
*/
|
|
154
|
+
export type DeepValue<TValue, TAccessor> = unknown extends TValue
|
|
155
|
+
? TValue
|
|
156
|
+
: TAccessor extends DeepKeys<TValue>
|
|
157
|
+
? DeepRecord<TValue>[TAccessor]
|
|
158
|
+
: never;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The keys of an object or array, deeply nested and only with a value of TValue
|
|
162
|
+
*/
|
|
163
|
+
export type DeepKeysOfType<TData, TValue> = Extract<
|
|
164
|
+
DeepKeysAndValues<TData>,
|
|
165
|
+
AnyDeepKeyAndValue<string, TValue>
|
|
166
|
+
>['key'];
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Maps the deep keys of TFormData to the shallow keys of TFieldGroupData.
|
|
170
|
+
* Since using template strings as keys is impractical, it relies on shallow keys only.
|
|
171
|
+
*/
|
|
172
|
+
export type FieldsMap<TFormData, TFieldGroupData> = TFieldGroupData extends any[]
|
|
173
|
+
? never
|
|
174
|
+
: string extends keyof TFieldGroupData
|
|
175
|
+
? never
|
|
176
|
+
: {
|
|
177
|
+
[K in keyof TFieldGroupData]: DeepKeysOfType<TFormData, TFieldGroupData[K]>;
|
|
178
|
+
};
|