@neutro/form 0.0.4 → 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/dist/adapters/angular.cjs +24 -6
- package/dist/adapters/angular.d.cts +171 -6
- package/dist/adapters/angular.d.ts +171 -6
- package/dist/adapters/angular.js +25 -7
- package/dist/adapters/react.cjs +5 -1
- package/dist/adapters/react.d.cts +139 -1
- package/dist/adapters/react.d.ts +139 -1
- package/dist/adapters/react.js +6 -2
- package/dist/adapters/solid.cjs +12 -3
- package/dist/adapters/solid.d.cts +171 -1
- package/dist/adapters/solid.d.ts +171 -1
- package/dist/adapters/solid.js +12 -3
- package/dist/adapters/svelte.cjs +18 -9
- package/dist/adapters/svelte.d.cts +171 -1
- package/dist/adapters/svelte.d.ts +171 -1
- package/dist/adapters/svelte.js +18 -9
- package/dist/adapters/vue.cjs +15 -4
- package/dist/adapters/vue.d.cts +176 -1
- package/dist/adapters/vue.d.ts +176 -1
- package/dist/adapters/vue.js +23 -5
- package/dist/chunk-FDAQJJJ7.js +657 -0
- package/dist/core.cjs +510 -425
- package/dist/core.d.cts +248 -1
- package/dist/core.d.ts +248 -1
- package/dist/core.js +24 -569
- package/dist/devtools.cjs +244 -0
- package/dist/devtools.d.cts +141 -0
- package/dist/devtools.d.ts +141 -0
- package/dist/devtools.js +217 -0
- package/dist/testing.cjs +667 -0
- package/dist/testing.d.cts +254 -0
- package/dist/testing.d.ts +254 -0
- package/dist/testing.js +39 -0
- package/package.json +25 -7
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/devtools.ts
|
|
21
|
+
var devtools_exports = {};
|
|
22
|
+
__export(devtools_exports, {
|
|
23
|
+
devtools: () => devtools
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(devtools_exports);
|
|
26
|
+
|
|
27
|
+
// ../core/src/index.ts
|
|
28
|
+
function isDeepEqual(a, b, hash = /* @__PURE__ */ new WeakMap()) {
|
|
29
|
+
if (a === b) return true;
|
|
30
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
31
|
+
if (a instanceof RegExp && b instanceof RegExp) return a.toString() === b.toString();
|
|
32
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
33
|
+
if (hash.has(a) && hash.get(a) === b) return true;
|
|
34
|
+
hash.set(a, b);
|
|
35
|
+
if (a instanceof Set && b instanceof Set) {
|
|
36
|
+
if (a.size !== b.size) return false;
|
|
37
|
+
for (const item of a) if (!b.has(item)) return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (a instanceof Map && b instanceof Map) {
|
|
41
|
+
if (a.size !== b.size) return false;
|
|
42
|
+
for (const [key, val] of a) {
|
|
43
|
+
if (!b.has(key) || !isDeepEqual(val, b.get(key), hash)) return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const keysA = Reflect.ownKeys(a);
|
|
48
|
+
const keysB = new Set(Reflect.ownKeys(b));
|
|
49
|
+
if (keysA.length !== keysB.size) return false;
|
|
50
|
+
for (const key of keysA) {
|
|
51
|
+
if (!keysB.has(key) || !isDeepEqual(a[key], b[key], hash)) return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ../core/src/devtools.ts
|
|
57
|
+
var BADGE_STYLE = "background:#6366f1;color:#fff;padding:2px 6px;border-radius:3px;font-weight:bold;font-size:11px;";
|
|
58
|
+
var DIM_STYLE = "color:#888;font-weight:normal;";
|
|
59
|
+
var ACTION_STYLE = "color:#f59e0b;font-weight:bold;";
|
|
60
|
+
var RESET_STYLE = "color:inherit;font-weight:normal;";
|
|
61
|
+
function formatElapsed(ms) {
|
|
62
|
+
return ms < 1e3 ? `+${ms}ms` : `+${(ms / 1e3).toFixed(1)}s`;
|
|
63
|
+
}
|
|
64
|
+
function describeAction(action) {
|
|
65
|
+
switch (action.type) {
|
|
66
|
+
case "SET":
|
|
67
|
+
return `SET ${action.path}`;
|
|
68
|
+
case "VALIDATE":
|
|
69
|
+
return action.paths ? `VALIDATE [${action.paths.join(", ")}]` : "VALIDATE";
|
|
70
|
+
case "SUBMIT":
|
|
71
|
+
return "SUBMIT";
|
|
72
|
+
case "RESET":
|
|
73
|
+
return "RESET";
|
|
74
|
+
case "SET_ERRORS":
|
|
75
|
+
return `SET_ERRORS [${Object.keys(action.errors).join(", ")}]`;
|
|
76
|
+
case "CLEAR_ERRORS":
|
|
77
|
+
return "CLEAR_ERRORS";
|
|
78
|
+
case "CONNECT":
|
|
79
|
+
return `CONNECT ${action.path}`;
|
|
80
|
+
case "DISCONNECT":
|
|
81
|
+
return `DISCONNECT ${action.path}`;
|
|
82
|
+
case "BLUR":
|
|
83
|
+
return `BLUR ${action.path}`;
|
|
84
|
+
case "BATCH_START":
|
|
85
|
+
return "BATCH_START";
|
|
86
|
+
case "BATCH_END":
|
|
87
|
+
return "BATCH_END";
|
|
88
|
+
case "ARRAY_APPEND":
|
|
89
|
+
return `ARRAY_APPEND ${action.path}`;
|
|
90
|
+
case "ARRAY_INSERT":
|
|
91
|
+
return `ARRAY_INSERT ${action.path}[${action.index}]`;
|
|
92
|
+
case "ARRAY_REMOVE":
|
|
93
|
+
return `ARRAY_REMOVE ${action.path}[${action.index}]`;
|
|
94
|
+
case "ARRAY_MOVE":
|
|
95
|
+
return `ARRAY_MOVE ${action.path} ${action.from}\u2192${action.to}`;
|
|
96
|
+
case "ARRAY_SWAP":
|
|
97
|
+
return `ARRAY_SWAP ${action.path} [${action.i}\u2194${action.j}]`;
|
|
98
|
+
default:
|
|
99
|
+
return action.type;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function computeDiff(prev, next) {
|
|
103
|
+
const rows = [];
|
|
104
|
+
const slices = ["values", "errors", "touched", "dirty"];
|
|
105
|
+
for (const slice of slices) {
|
|
106
|
+
const prevSlice = prev[slice];
|
|
107
|
+
const nextSlice = next[slice];
|
|
108
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(prevSlice), ...Object.keys(nextSlice)]);
|
|
109
|
+
for (const key of allKeys) {
|
|
110
|
+
const p = prevSlice[key];
|
|
111
|
+
const n = nextSlice[key];
|
|
112
|
+
const equal = slice === "values" ? isDeepEqual(p, n) : p === n;
|
|
113
|
+
if (!equal) rows.push({ slice, key, prev: p, next: n });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (prev.isSubmitting !== next.isSubmitting) {
|
|
117
|
+
rows.push({
|
|
118
|
+
slice: "meta",
|
|
119
|
+
key: "isSubmitting",
|
|
120
|
+
prev: prev.isSubmitting,
|
|
121
|
+
next: next.isSubmitting
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (prev.isValidating !== next.isValidating) {
|
|
125
|
+
rows.push({
|
|
126
|
+
slice: "meta",
|
|
127
|
+
key: "isValidating",
|
|
128
|
+
prev: prev.isValidating,
|
|
129
|
+
next: next.isValidating
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return rows;
|
|
133
|
+
}
|
|
134
|
+
function logAction(action, state, prev, name, groupFn, lastTimeRef) {
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
const elapsed = now - lastTimeRef.value;
|
|
137
|
+
lastTimeRef.value = now;
|
|
138
|
+
const label = describeAction(action);
|
|
139
|
+
const timestamp = new Date(now).toLocaleTimeString("en", {
|
|
140
|
+
hour12: false,
|
|
141
|
+
hour: "2-digit",
|
|
142
|
+
minute: "2-digit",
|
|
143
|
+
second: "2-digit"
|
|
144
|
+
});
|
|
145
|
+
groupFn(
|
|
146
|
+
"%c NeutroForm: %s %c %s %c %s %s",
|
|
147
|
+
BADGE_STYLE,
|
|
148
|
+
name,
|
|
149
|
+
RESET_STYLE,
|
|
150
|
+
label,
|
|
151
|
+
DIM_STYLE,
|
|
152
|
+
timestamp,
|
|
153
|
+
formatElapsed(elapsed)
|
|
154
|
+
);
|
|
155
|
+
console.log("%c action", ACTION_STYLE, action);
|
|
156
|
+
const diff = computeDiff(prev, state);
|
|
157
|
+
if (diff.length > 0) {
|
|
158
|
+
console.table(diff);
|
|
159
|
+
} else {
|
|
160
|
+
console.log("%c no state change", DIM_STYLE);
|
|
161
|
+
}
|
|
162
|
+
console.groupCollapsed("%c full state", DIM_STYLE);
|
|
163
|
+
console.log("%o", state);
|
|
164
|
+
console.groupEnd();
|
|
165
|
+
console.groupEnd();
|
|
166
|
+
}
|
|
167
|
+
var registeredForms = /* @__PURE__ */ new WeakSet();
|
|
168
|
+
function devtools(form, options = {}) {
|
|
169
|
+
if (registeredForms.has(form)) {
|
|
170
|
+
console.warn(
|
|
171
|
+
"[NeutroForm devtools] devtools() was called twice on the same form instance. Ignoring duplicate registration."
|
|
172
|
+
);
|
|
173
|
+
return () => {
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
registeredForms.add(form);
|
|
177
|
+
const name = options.name ?? "Form";
|
|
178
|
+
const collapsed = options.collapsed ?? true;
|
|
179
|
+
const groupFn = collapsed ? console.groupCollapsed.bind(console) : console.group.bind(console);
|
|
180
|
+
let prevState = form.getState();
|
|
181
|
+
const lastTimeRef = { value: Date.now() };
|
|
182
|
+
let inBatch = false;
|
|
183
|
+
let batchActions = [];
|
|
184
|
+
groupFn("%c NeutroForm: %s %c init", BADGE_STYLE, name, RESET_STYLE);
|
|
185
|
+
console.log("%c initial state", DIM_STYLE, form.getState());
|
|
186
|
+
console.groupEnd();
|
|
187
|
+
const unsubscribe = form._subscribeToActions((action, state) => {
|
|
188
|
+
if (action.type === "BATCH_START") {
|
|
189
|
+
inBatch = true;
|
|
190
|
+
batchActions = [];
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (action.type === "BATCH_END") {
|
|
194
|
+
inBatch = false;
|
|
195
|
+
const count = batchActions.length;
|
|
196
|
+
if (count === 0) {
|
|
197
|
+
batchActions = [];
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
const elapsed = now - lastTimeRef.value;
|
|
202
|
+
lastTimeRef.value = now;
|
|
203
|
+
const timestamp = new Date(now).toLocaleTimeString("en", {
|
|
204
|
+
hour12: false,
|
|
205
|
+
hour: "2-digit",
|
|
206
|
+
minute: "2-digit",
|
|
207
|
+
second: "2-digit"
|
|
208
|
+
});
|
|
209
|
+
groupFn(
|
|
210
|
+
"%c NeutroForm: %s %c BATCH (%d mutations) %c %s %s",
|
|
211
|
+
BADGE_STYLE,
|
|
212
|
+
name,
|
|
213
|
+
RESET_STYLE,
|
|
214
|
+
count,
|
|
215
|
+
DIM_STYLE,
|
|
216
|
+
timestamp,
|
|
217
|
+
formatElapsed(elapsed)
|
|
218
|
+
);
|
|
219
|
+
const frozenTimeRef = { value: lastTimeRef.value };
|
|
220
|
+
for (const { action: a, state: s, prev: prev2 } of batchActions) {
|
|
221
|
+
logAction(a, s, prev2, name, groupFn, frozenTimeRef);
|
|
222
|
+
}
|
|
223
|
+
console.groupEnd();
|
|
224
|
+
batchActions = [];
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (inBatch) {
|
|
228
|
+
batchActions.push({ action, state, prev: prevState });
|
|
229
|
+
prevState = state;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const prev = prevState;
|
|
233
|
+
prevState = state;
|
|
234
|
+
logAction(action, state, prev, name, groupFn, lastTimeRef);
|
|
235
|
+
});
|
|
236
|
+
return () => {
|
|
237
|
+
registeredForms.delete(form);
|
|
238
|
+
unsubscribe();
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
242
|
+
0 && (module.exports = {
|
|
243
|
+
devtools
|
|
244
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @neutro/form-core
|
|
3
|
+
* High-Performance, Zero-Dependency, Framework-Agnostic Reactive Form Engine.
|
|
4
|
+
*/
|
|
5
|
+
type Primitive = string | number | boolean | null | undefined | Date | File;
|
|
6
|
+
type Prev = [never, 0, 1, 2, 3, 4, 5, ...any[]];
|
|
7
|
+
type PathImpl<T, K extends keyof T, Depth extends number = 5> = [Depth] extends [never] ? never : K extends string ? T[K] extends Primitive ? K : T[K] extends Array<infer U> ? K | `${K}.${number}` | (U extends object ? `${K}.${number}.${PathImpl<U, keyof U, Prev[Depth]>}` : never) : NonNullable<T[K]> extends object ? K | `${K}.${PathImpl<NonNullable<T[K]>, keyof NonNullable<T[K]>, Prev[Depth]>}` : K : never;
|
|
8
|
+
type Path<T> = PathImpl<T, keyof T> & string;
|
|
9
|
+
type _GetPathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? _GetPathValue<NonNullable<T[K]>, Rest> : T extends readonly any[] ? _GetPathValue<NonNullable<T[number]>, Rest> : unknown : P extends keyof T ? T[P] : T extends readonly any[] ? T[number] : unknown;
|
|
10
|
+
type GetPathValue<T, P extends string> = _GetPathValue<T, P>;
|
|
11
|
+
interface FormState<T> {
|
|
12
|
+
values: T;
|
|
13
|
+
errors: Record<string, string>;
|
|
14
|
+
touched: Record<string, boolean>;
|
|
15
|
+
dirty: Record<string, boolean>;
|
|
16
|
+
isSubmitting: boolean;
|
|
17
|
+
isValidating: boolean;
|
|
18
|
+
}
|
|
19
|
+
type FormSubscriber<T> = (state: FormState<T>) => void;
|
|
20
|
+
type PathSubscriber<V = any> = (value: V, fieldState: {
|
|
21
|
+
error?: string;
|
|
22
|
+
touched?: boolean;
|
|
23
|
+
dirty?: boolean;
|
|
24
|
+
}) => void;
|
|
25
|
+
type ValidationMode = 'onChange' | 'onBlur' | 'onTouched' | 'onSubmitOnly';
|
|
26
|
+
type FormAction = {
|
|
27
|
+
type: 'SET';
|
|
28
|
+
path: string;
|
|
29
|
+
value: unknown;
|
|
30
|
+
options?: {
|
|
31
|
+
touch?: boolean;
|
|
32
|
+
validate?: boolean;
|
|
33
|
+
};
|
|
34
|
+
} | {
|
|
35
|
+
type: 'VALIDATE';
|
|
36
|
+
paths?: string[];
|
|
37
|
+
} | {
|
|
38
|
+
type: 'SUBMIT';
|
|
39
|
+
} | {
|
|
40
|
+
type: 'RESET';
|
|
41
|
+
newValues?: unknown;
|
|
42
|
+
} | {
|
|
43
|
+
type: 'SET_ERRORS';
|
|
44
|
+
errors: Record<string, string>;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'CONNECT';
|
|
47
|
+
path: string;
|
|
48
|
+
} | {
|
|
49
|
+
type: 'DISCONNECT';
|
|
50
|
+
path: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'BLUR';
|
|
53
|
+
path: string;
|
|
54
|
+
} | {
|
|
55
|
+
type: 'BATCH_START';
|
|
56
|
+
} | {
|
|
57
|
+
type: 'BATCH_END';
|
|
58
|
+
} | {
|
|
59
|
+
type: 'ARRAY_APPEND';
|
|
60
|
+
path: string;
|
|
61
|
+
item: unknown;
|
|
62
|
+
} | {
|
|
63
|
+
type: 'ARRAY_INSERT';
|
|
64
|
+
path: string;
|
|
65
|
+
index: number;
|
|
66
|
+
item: unknown;
|
|
67
|
+
} | {
|
|
68
|
+
type: 'ARRAY_REMOVE';
|
|
69
|
+
path: string;
|
|
70
|
+
index: number;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'ARRAY_MOVE';
|
|
73
|
+
path: string;
|
|
74
|
+
from: number;
|
|
75
|
+
to: number;
|
|
76
|
+
} | {
|
|
77
|
+
type: 'ARRAY_SWAP';
|
|
78
|
+
path: string;
|
|
79
|
+
i: number;
|
|
80
|
+
j: number;
|
|
81
|
+
} | {
|
|
82
|
+
type: 'CLEAR_ERRORS';
|
|
83
|
+
};
|
|
84
|
+
interface AriaPropsOptions {
|
|
85
|
+
required?: boolean;
|
|
86
|
+
errorId?: string;
|
|
87
|
+
}
|
|
88
|
+
interface AriaProps {
|
|
89
|
+
'aria-invalid': 'true' | 'false';
|
|
90
|
+
'aria-describedby': string | undefined;
|
|
91
|
+
'aria-required': true | undefined;
|
|
92
|
+
}
|
|
93
|
+
interface ConnectOptions {
|
|
94
|
+
persist?: boolean;
|
|
95
|
+
format?: (val: string) => string;
|
|
96
|
+
validateOn?: ValidationMode;
|
|
97
|
+
}
|
|
98
|
+
interface FormInstance<T extends object> {
|
|
99
|
+
subscribe: (fn: FormSubscriber<T>) => () => void;
|
|
100
|
+
subscribeToPath<P extends Path<T>>(path: P, fn: PathSubscriber<GetPathValue<T, P>>): () => void;
|
|
101
|
+
subscribeToPath(path: string, fn: PathSubscriber): () => void;
|
|
102
|
+
get<P extends Path<T>>(path: P): GetPathValue<T, P>;
|
|
103
|
+
get(path: string | string[]): any;
|
|
104
|
+
set: (path: Path<T> | string | string[], val: any, options?: {
|
|
105
|
+
touch?: boolean;
|
|
106
|
+
validate?: boolean;
|
|
107
|
+
}) => void;
|
|
108
|
+
validate: (scopePaths?: Path<T>[] | string[] | string[][]) => Promise<boolean>;
|
|
109
|
+
connect: (path: Path<T> | string, el: HTMLElement, options?: ConnectOptions) => () => void;
|
|
110
|
+
submit: (onValid: (payload: Partial<T>) => void | Promise<void>) => Promise<boolean>;
|
|
111
|
+
handleSubmit: (onValid: (payload: Partial<T>) => void | Promise<void>, onInvalid?: (errors: Record<string, string>) => void) => (e?: Event) => void;
|
|
112
|
+
getState: () => FormState<T>;
|
|
113
|
+
getPayload: () => Partial<T>;
|
|
114
|
+
getAriaProps: (path: Path<T> | string, options?: AriaPropsOptions) => AriaProps;
|
|
115
|
+
batch: (fn: () => void) => void;
|
|
116
|
+
arrayAppend: (path: Path<T> | string | string[], item: any) => void;
|
|
117
|
+
arrayInsert: (path: Path<T> | string | string[], index: number, item: any) => void;
|
|
118
|
+
arrayRemove: (path: Path<T> | string | string[], index: number) => void;
|
|
119
|
+
arrayMove: (path: Path<T> | string | string[], fromIndex: number, toIndex: number) => void;
|
|
120
|
+
arraySwap: (path: Path<T> | string | string[], indexA: number, indexB: number) => void;
|
|
121
|
+
reset: (newValues?: T) => void;
|
|
122
|
+
getConnectedCount: () => number;
|
|
123
|
+
destroy: () => void;
|
|
124
|
+
setErrors: (errors: Record<Path<T> | (string & {}), string>) => void;
|
|
125
|
+
clearErrors: () => void;
|
|
126
|
+
/**
|
|
127
|
+
* Returns the effective ValidationMode for a field. Useful for debugging
|
|
128
|
+
* validation timing; framework adapters should rely on this only in custom
|
|
129
|
+
* event handlers, not in render logic.
|
|
130
|
+
*/
|
|
131
|
+
getFieldMode: (path: string) => ValidationMode;
|
|
132
|
+
_subscribeToActions: (fn: (action: FormAction, state: FormState<T>) => void) => () => void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface DevtoolsOptions {
|
|
136
|
+
name?: string;
|
|
137
|
+
collapsed?: boolean;
|
|
138
|
+
}
|
|
139
|
+
declare function devtools<T extends object>(form: FormInstance<T>, options?: DevtoolsOptions): () => void;
|
|
140
|
+
|
|
141
|
+
export { type DevtoolsOptions, devtools };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @neutro/form-core
|
|
3
|
+
* High-Performance, Zero-Dependency, Framework-Agnostic Reactive Form Engine.
|
|
4
|
+
*/
|
|
5
|
+
type Primitive = string | number | boolean | null | undefined | Date | File;
|
|
6
|
+
type Prev = [never, 0, 1, 2, 3, 4, 5, ...any[]];
|
|
7
|
+
type PathImpl<T, K extends keyof T, Depth extends number = 5> = [Depth] extends [never] ? never : K extends string ? T[K] extends Primitive ? K : T[K] extends Array<infer U> ? K | `${K}.${number}` | (U extends object ? `${K}.${number}.${PathImpl<U, keyof U, Prev[Depth]>}` : never) : NonNullable<T[K]> extends object ? K | `${K}.${PathImpl<NonNullable<T[K]>, keyof NonNullable<T[K]>, Prev[Depth]>}` : K : never;
|
|
8
|
+
type Path<T> = PathImpl<T, keyof T> & string;
|
|
9
|
+
type _GetPathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? _GetPathValue<NonNullable<T[K]>, Rest> : T extends readonly any[] ? _GetPathValue<NonNullable<T[number]>, Rest> : unknown : P extends keyof T ? T[P] : T extends readonly any[] ? T[number] : unknown;
|
|
10
|
+
type GetPathValue<T, P extends string> = _GetPathValue<T, P>;
|
|
11
|
+
interface FormState<T> {
|
|
12
|
+
values: T;
|
|
13
|
+
errors: Record<string, string>;
|
|
14
|
+
touched: Record<string, boolean>;
|
|
15
|
+
dirty: Record<string, boolean>;
|
|
16
|
+
isSubmitting: boolean;
|
|
17
|
+
isValidating: boolean;
|
|
18
|
+
}
|
|
19
|
+
type FormSubscriber<T> = (state: FormState<T>) => void;
|
|
20
|
+
type PathSubscriber<V = any> = (value: V, fieldState: {
|
|
21
|
+
error?: string;
|
|
22
|
+
touched?: boolean;
|
|
23
|
+
dirty?: boolean;
|
|
24
|
+
}) => void;
|
|
25
|
+
type ValidationMode = 'onChange' | 'onBlur' | 'onTouched' | 'onSubmitOnly';
|
|
26
|
+
type FormAction = {
|
|
27
|
+
type: 'SET';
|
|
28
|
+
path: string;
|
|
29
|
+
value: unknown;
|
|
30
|
+
options?: {
|
|
31
|
+
touch?: boolean;
|
|
32
|
+
validate?: boolean;
|
|
33
|
+
};
|
|
34
|
+
} | {
|
|
35
|
+
type: 'VALIDATE';
|
|
36
|
+
paths?: string[];
|
|
37
|
+
} | {
|
|
38
|
+
type: 'SUBMIT';
|
|
39
|
+
} | {
|
|
40
|
+
type: 'RESET';
|
|
41
|
+
newValues?: unknown;
|
|
42
|
+
} | {
|
|
43
|
+
type: 'SET_ERRORS';
|
|
44
|
+
errors: Record<string, string>;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'CONNECT';
|
|
47
|
+
path: string;
|
|
48
|
+
} | {
|
|
49
|
+
type: 'DISCONNECT';
|
|
50
|
+
path: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'BLUR';
|
|
53
|
+
path: string;
|
|
54
|
+
} | {
|
|
55
|
+
type: 'BATCH_START';
|
|
56
|
+
} | {
|
|
57
|
+
type: 'BATCH_END';
|
|
58
|
+
} | {
|
|
59
|
+
type: 'ARRAY_APPEND';
|
|
60
|
+
path: string;
|
|
61
|
+
item: unknown;
|
|
62
|
+
} | {
|
|
63
|
+
type: 'ARRAY_INSERT';
|
|
64
|
+
path: string;
|
|
65
|
+
index: number;
|
|
66
|
+
item: unknown;
|
|
67
|
+
} | {
|
|
68
|
+
type: 'ARRAY_REMOVE';
|
|
69
|
+
path: string;
|
|
70
|
+
index: number;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'ARRAY_MOVE';
|
|
73
|
+
path: string;
|
|
74
|
+
from: number;
|
|
75
|
+
to: number;
|
|
76
|
+
} | {
|
|
77
|
+
type: 'ARRAY_SWAP';
|
|
78
|
+
path: string;
|
|
79
|
+
i: number;
|
|
80
|
+
j: number;
|
|
81
|
+
} | {
|
|
82
|
+
type: 'CLEAR_ERRORS';
|
|
83
|
+
};
|
|
84
|
+
interface AriaPropsOptions {
|
|
85
|
+
required?: boolean;
|
|
86
|
+
errorId?: string;
|
|
87
|
+
}
|
|
88
|
+
interface AriaProps {
|
|
89
|
+
'aria-invalid': 'true' | 'false';
|
|
90
|
+
'aria-describedby': string | undefined;
|
|
91
|
+
'aria-required': true | undefined;
|
|
92
|
+
}
|
|
93
|
+
interface ConnectOptions {
|
|
94
|
+
persist?: boolean;
|
|
95
|
+
format?: (val: string) => string;
|
|
96
|
+
validateOn?: ValidationMode;
|
|
97
|
+
}
|
|
98
|
+
interface FormInstance<T extends object> {
|
|
99
|
+
subscribe: (fn: FormSubscriber<T>) => () => void;
|
|
100
|
+
subscribeToPath<P extends Path<T>>(path: P, fn: PathSubscriber<GetPathValue<T, P>>): () => void;
|
|
101
|
+
subscribeToPath(path: string, fn: PathSubscriber): () => void;
|
|
102
|
+
get<P extends Path<T>>(path: P): GetPathValue<T, P>;
|
|
103
|
+
get(path: string | string[]): any;
|
|
104
|
+
set: (path: Path<T> | string | string[], val: any, options?: {
|
|
105
|
+
touch?: boolean;
|
|
106
|
+
validate?: boolean;
|
|
107
|
+
}) => void;
|
|
108
|
+
validate: (scopePaths?: Path<T>[] | string[] | string[][]) => Promise<boolean>;
|
|
109
|
+
connect: (path: Path<T> | string, el: HTMLElement, options?: ConnectOptions) => () => void;
|
|
110
|
+
submit: (onValid: (payload: Partial<T>) => void | Promise<void>) => Promise<boolean>;
|
|
111
|
+
handleSubmit: (onValid: (payload: Partial<T>) => void | Promise<void>, onInvalid?: (errors: Record<string, string>) => void) => (e?: Event) => void;
|
|
112
|
+
getState: () => FormState<T>;
|
|
113
|
+
getPayload: () => Partial<T>;
|
|
114
|
+
getAriaProps: (path: Path<T> | string, options?: AriaPropsOptions) => AriaProps;
|
|
115
|
+
batch: (fn: () => void) => void;
|
|
116
|
+
arrayAppend: (path: Path<T> | string | string[], item: any) => void;
|
|
117
|
+
arrayInsert: (path: Path<T> | string | string[], index: number, item: any) => void;
|
|
118
|
+
arrayRemove: (path: Path<T> | string | string[], index: number) => void;
|
|
119
|
+
arrayMove: (path: Path<T> | string | string[], fromIndex: number, toIndex: number) => void;
|
|
120
|
+
arraySwap: (path: Path<T> | string | string[], indexA: number, indexB: number) => void;
|
|
121
|
+
reset: (newValues?: T) => void;
|
|
122
|
+
getConnectedCount: () => number;
|
|
123
|
+
destroy: () => void;
|
|
124
|
+
setErrors: (errors: Record<Path<T> | (string & {}), string>) => void;
|
|
125
|
+
clearErrors: () => void;
|
|
126
|
+
/**
|
|
127
|
+
* Returns the effective ValidationMode for a field. Useful for debugging
|
|
128
|
+
* validation timing; framework adapters should rely on this only in custom
|
|
129
|
+
* event handlers, not in render logic.
|
|
130
|
+
*/
|
|
131
|
+
getFieldMode: (path: string) => ValidationMode;
|
|
132
|
+
_subscribeToActions: (fn: (action: FormAction, state: FormState<T>) => void) => () => void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface DevtoolsOptions {
|
|
136
|
+
name?: string;
|
|
137
|
+
collapsed?: boolean;
|
|
138
|
+
}
|
|
139
|
+
declare function devtools<T extends object>(form: FormInstance<T>, options?: DevtoolsOptions): () => void;
|
|
140
|
+
|
|
141
|
+
export { type DevtoolsOptions, devtools };
|