what-core 0.1.1 → 0.3.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/a11y.js +425 -0
- package/dist/animation.js +540 -0
- package/dist/components.js +272 -115
- package/dist/data.js +444 -0
- package/dist/dom.js +702 -427
- package/dist/form.js +441 -0
- package/dist/h.js +191 -138
- package/dist/head.js +59 -42
- package/dist/helpers.js +125 -83
- package/dist/hooks.js +226 -124
- package/dist/index.js +2 -2
- package/dist/reactive.js +165 -108
- package/dist/scheduler.js +241 -0
- package/dist/skeleton.js +363 -0
- package/dist/store.js +114 -55
- package/dist/testing.js +367 -0
- package/dist/what.js +2 -2
- package/index.d.ts +15 -0
- package/package.json +1 -1
- package/src/animation.js +11 -2
- package/src/components.js +93 -0
- package/src/data.js +19 -9
- package/src/dom.js +181 -85
- package/src/hooks.js +22 -10
- package/src/index.js +2 -2
- package/src/reactive.js +15 -1
- package/src/store.js +24 -5
package/dist/form.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// What Framework - Form Utilities
|
|
2
|
+
// Controlled inputs, validation, and form state management
|
|
3
|
+
|
|
4
|
+
import { signal, computed, batch, effect } from './reactive.js';
|
|
5
|
+
import { h } from './h.js';
|
|
6
|
+
|
|
7
|
+
// --- useForm Hook ---
|
|
8
|
+
// Complete form state management with validation
|
|
9
|
+
|
|
10
|
+
export function useForm(options = {}) {
|
|
11
|
+
const {
|
|
12
|
+
defaultValues = {},
|
|
13
|
+
mode = 'onSubmit', // 'onSubmit' | 'onChange' | 'onBlur'
|
|
14
|
+
reValidateMode = 'onChange',
|
|
15
|
+
resolver,
|
|
16
|
+
} = options;
|
|
17
|
+
|
|
18
|
+
// Form state
|
|
19
|
+
const values = signal({ ...defaultValues });
|
|
20
|
+
const errors = signal({});
|
|
21
|
+
const touched = signal({});
|
|
22
|
+
const isDirty = signal(false);
|
|
23
|
+
const isSubmitting = signal(false);
|
|
24
|
+
const isSubmitted = signal(false);
|
|
25
|
+
const submitCount = signal(0);
|
|
26
|
+
|
|
27
|
+
// Computed states
|
|
28
|
+
const isValid = computed(() => Object.keys(errors()).length === 0);
|
|
29
|
+
const dirtyFields = computed(() => {
|
|
30
|
+
const dirty = {};
|
|
31
|
+
const current = values();
|
|
32
|
+
for (const key in current) {
|
|
33
|
+
if (current[key] !== defaultValues[key]) {
|
|
34
|
+
dirty[key] = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return dirty;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Validation
|
|
41
|
+
async function validate(fieldName) {
|
|
42
|
+
if (!resolver) return true;
|
|
43
|
+
|
|
44
|
+
const result = await resolver(values());
|
|
45
|
+
|
|
46
|
+
if (fieldName) {
|
|
47
|
+
// Validate single field
|
|
48
|
+
if (result.errors[fieldName]) {
|
|
49
|
+
errors.set({ ...errors.peek(), [fieldName]: result.errors[fieldName] });
|
|
50
|
+
return false;
|
|
51
|
+
} else {
|
|
52
|
+
const newErrors = { ...errors.peek() };
|
|
53
|
+
delete newErrors[fieldName];
|
|
54
|
+
errors.set(newErrors);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
// Validate all fields
|
|
59
|
+
errors.set(result.errors || {});
|
|
60
|
+
return Object.keys(result.errors || {}).length === 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Register a field
|
|
65
|
+
function register(name, options = {}) {
|
|
66
|
+
return {
|
|
67
|
+
name,
|
|
68
|
+
value: values()[name] ?? '',
|
|
69
|
+
onInput: (e) => {
|
|
70
|
+
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
|
71
|
+
setValue(name, value);
|
|
72
|
+
|
|
73
|
+
if (mode === 'onChange' || (isSubmitted.peek() && reValidateMode === 'onChange')) {
|
|
74
|
+
validate(name);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
onBlur: () => {
|
|
78
|
+
touched.set({ ...touched.peek(), [name]: true });
|
|
79
|
+
|
|
80
|
+
if (mode === 'onBlur' || (isSubmitted.peek() && reValidateMode === 'onBlur')) {
|
|
81
|
+
validate(name);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
onFocus: () => {},
|
|
85
|
+
ref: options.ref,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Set single field value
|
|
90
|
+
function setValue(name, value, options = {}) {
|
|
91
|
+
const { shouldValidate = false, shouldDirty = true } = options;
|
|
92
|
+
|
|
93
|
+
batch(() => {
|
|
94
|
+
values.set({ ...values.peek(), [name]: value });
|
|
95
|
+
if (shouldDirty) {
|
|
96
|
+
isDirty.set(true);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (shouldValidate) {
|
|
101
|
+
validate(name);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Get single field value
|
|
106
|
+
function getValue(name) {
|
|
107
|
+
return values()[name];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Set error for a field
|
|
111
|
+
function setError(name, error) {
|
|
112
|
+
errors.set({ ...errors.peek(), [name]: error });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Clear error for a field
|
|
116
|
+
function clearError(name) {
|
|
117
|
+
const newErrors = { ...errors.peek() };
|
|
118
|
+
delete newErrors[name];
|
|
119
|
+
errors.set(newErrors);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Clear all errors
|
|
123
|
+
function clearErrors() {
|
|
124
|
+
errors.set({});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Reset form
|
|
128
|
+
function reset(newValues = defaultValues) {
|
|
129
|
+
batch(() => {
|
|
130
|
+
values.set({ ...newValues });
|
|
131
|
+
errors.set({});
|
|
132
|
+
touched.set({});
|
|
133
|
+
isDirty.set(false);
|
|
134
|
+
isSubmitted.set(false);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Handle submit
|
|
139
|
+
function handleSubmit(onValid, onInvalid) {
|
|
140
|
+
return async (e) => {
|
|
141
|
+
if (e) e.preventDefault();
|
|
142
|
+
|
|
143
|
+
isSubmitting.set(true);
|
|
144
|
+
isSubmitted.set(true);
|
|
145
|
+
submitCount.set(submitCount.peek() + 1);
|
|
146
|
+
|
|
147
|
+
const isFormValid = await validate();
|
|
148
|
+
|
|
149
|
+
if (isFormValid) {
|
|
150
|
+
await onValid(values.peek());
|
|
151
|
+
} else if (onInvalid) {
|
|
152
|
+
onInvalid(errors.peek());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
isSubmitting.set(false);
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Watch a field
|
|
160
|
+
function watch(name) {
|
|
161
|
+
if (name) {
|
|
162
|
+
return computed(() => values()[name]);
|
|
163
|
+
}
|
|
164
|
+
return values;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
register,
|
|
169
|
+
handleSubmit,
|
|
170
|
+
setValue,
|
|
171
|
+
getValue,
|
|
172
|
+
setError,
|
|
173
|
+
clearError,
|
|
174
|
+
clearErrors,
|
|
175
|
+
reset,
|
|
176
|
+
watch,
|
|
177
|
+
validate,
|
|
178
|
+
// Form state
|
|
179
|
+
formState: {
|
|
180
|
+
values: () => values(),
|
|
181
|
+
errors: () => errors(),
|
|
182
|
+
touched: () => touched(),
|
|
183
|
+
isDirty: () => isDirty(),
|
|
184
|
+
isValid,
|
|
185
|
+
isSubmitting: () => isSubmitting(),
|
|
186
|
+
isSubmitted: () => isSubmitted(),
|
|
187
|
+
submitCount: () => submitCount(),
|
|
188
|
+
dirtyFields,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// --- Validation Resolvers ---
|
|
194
|
+
|
|
195
|
+
export function zodResolver(schema) {
|
|
196
|
+
return async (values) => {
|
|
197
|
+
try {
|
|
198
|
+
const result = await schema.parseAsync(values);
|
|
199
|
+
return { values: result, errors: {} };
|
|
200
|
+
} catch (e) {
|
|
201
|
+
const errors = {};
|
|
202
|
+
for (const issue of e.errors || []) {
|
|
203
|
+
const path = issue.path.join('.');
|
|
204
|
+
if (!errors[path]) {
|
|
205
|
+
errors[path] = { type: issue.code, message: issue.message };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { values: {}, errors };
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function yupResolver(schema) {
|
|
214
|
+
return async (values) => {
|
|
215
|
+
try {
|
|
216
|
+
const result = await schema.validate(values, { abortEarly: false });
|
|
217
|
+
return { values: result, errors: {} };
|
|
218
|
+
} catch (e) {
|
|
219
|
+
const errors = {};
|
|
220
|
+
for (const err of e.inner || []) {
|
|
221
|
+
if (!errors[err.path]) {
|
|
222
|
+
errors[err.path] = { type: err.type, message: err.message };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { values: {}, errors };
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Simple validation resolver
|
|
231
|
+
export function simpleResolver(rules) {
|
|
232
|
+
return async (values) => {
|
|
233
|
+
const errors = {};
|
|
234
|
+
|
|
235
|
+
for (const [field, fieldRules] of Object.entries(rules)) {
|
|
236
|
+
const value = values[field];
|
|
237
|
+
|
|
238
|
+
for (const rule of fieldRules) {
|
|
239
|
+
const error = rule(value, values);
|
|
240
|
+
if (error) {
|
|
241
|
+
errors[field] = { type: 'validation', message: error };
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { values, errors };
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Built-in validation rules
|
|
252
|
+
export const rules = {
|
|
253
|
+
required: (message = 'This field is required') => (value) => {
|
|
254
|
+
if (value === undefined || value === null || value === '') {
|
|
255
|
+
return message;
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
minLength: (min, message) => (value) => {
|
|
260
|
+
if (typeof value === 'string' && value.length < min) {
|
|
261
|
+
return message || `Must be at least ${min} characters`;
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
maxLength: (max, message) => (value) => {
|
|
266
|
+
if (typeof value === 'string' && value.length > max) {
|
|
267
|
+
return message || `Must be at most ${max} characters`;
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
min: (min, message) => (value) => {
|
|
272
|
+
if (typeof value === 'number' && value < min) {
|
|
273
|
+
return message || `Must be at least ${min}`;
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
max: (max, message) => (value) => {
|
|
278
|
+
if (typeof value === 'number' && value > max) {
|
|
279
|
+
return message || `Must be at most ${max}`;
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
pattern: (regex, message = 'Invalid format') => (value) => {
|
|
284
|
+
if (typeof value === 'string' && !regex.test(value)) {
|
|
285
|
+
return message;
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
email: (message = 'Invalid email address') => (value) => {
|
|
290
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
291
|
+
if (typeof value === 'string' && !emailRegex.test(value)) {
|
|
292
|
+
return message;
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
url: (message = 'Invalid URL') => (value) => {
|
|
297
|
+
try {
|
|
298
|
+
if (typeof value === 'string' && value) {
|
|
299
|
+
new URL(value);
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
return message;
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
match: (field, message) => (value, values) => {
|
|
307
|
+
if (value !== values[field]) {
|
|
308
|
+
return message || `Must match ${field}`;
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
custom: (validator) => validator,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// --- useField Hook ---
|
|
316
|
+
// Individual field control
|
|
317
|
+
|
|
318
|
+
export function useField(name, options = {}) {
|
|
319
|
+
const { validate: validateFn, defaultValue = '' } = options;
|
|
320
|
+
|
|
321
|
+
const value = signal(defaultValue);
|
|
322
|
+
const error = signal(null);
|
|
323
|
+
const isTouched = signal(false);
|
|
324
|
+
const isDirty = signal(false);
|
|
325
|
+
|
|
326
|
+
async function validate() {
|
|
327
|
+
if (!validateFn) return true;
|
|
328
|
+
const result = await validateFn(value.peek());
|
|
329
|
+
error.set(result || null);
|
|
330
|
+
return !result;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
name,
|
|
335
|
+
value: () => value(),
|
|
336
|
+
error: () => error(),
|
|
337
|
+
isTouched: () => isTouched(),
|
|
338
|
+
isDirty: () => isDirty(),
|
|
339
|
+
setValue: (v) => {
|
|
340
|
+
value.set(v);
|
|
341
|
+
isDirty.set(true);
|
|
342
|
+
},
|
|
343
|
+
setError: (e) => error.set(e),
|
|
344
|
+
validate,
|
|
345
|
+
reset: () => {
|
|
346
|
+
value.set(defaultValue);
|
|
347
|
+
error.set(null);
|
|
348
|
+
isTouched.set(false);
|
|
349
|
+
isDirty.set(false);
|
|
350
|
+
},
|
|
351
|
+
inputProps: () => ({
|
|
352
|
+
name,
|
|
353
|
+
value: value(),
|
|
354
|
+
onInput: (e) => {
|
|
355
|
+
value.set(e.target.value);
|
|
356
|
+
isDirty.set(true);
|
|
357
|
+
},
|
|
358
|
+
onBlur: () => {
|
|
359
|
+
isTouched.set(true);
|
|
360
|
+
validate();
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// --- Controlled Input Components ---
|
|
367
|
+
|
|
368
|
+
export function Input(props) {
|
|
369
|
+
const { register, error, ...rest } = props;
|
|
370
|
+
const registered = register ? register(props.name) : {};
|
|
371
|
+
|
|
372
|
+
return h('input', {
|
|
373
|
+
...rest,
|
|
374
|
+
...registered,
|
|
375
|
+
'aria-invalid': error ? 'true' : undefined,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function Textarea(props) {
|
|
380
|
+
const { register, error, ...rest } = props;
|
|
381
|
+
const registered = register ? register(props.name) : {};
|
|
382
|
+
|
|
383
|
+
return h('textarea', {
|
|
384
|
+
...rest,
|
|
385
|
+
...registered,
|
|
386
|
+
'aria-invalid': error ? 'true' : undefined,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function Select(props) {
|
|
391
|
+
const { register, error, children, ...rest } = props;
|
|
392
|
+
const registered = register ? register(props.name) : {};
|
|
393
|
+
|
|
394
|
+
return h('select', {
|
|
395
|
+
...rest,
|
|
396
|
+
...registered,
|
|
397
|
+
'aria-invalid': error ? 'true' : undefined,
|
|
398
|
+
}, children);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function Checkbox(props) {
|
|
402
|
+
const { register, ...rest } = props;
|
|
403
|
+
const registered = register ? register(props.name) : {};
|
|
404
|
+
|
|
405
|
+
return h('input', {
|
|
406
|
+
type: 'checkbox',
|
|
407
|
+
...rest,
|
|
408
|
+
...registered,
|
|
409
|
+
checked: registered.value,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function Radio(props) {
|
|
414
|
+
const { register, value: radioValue, ...rest } = props;
|
|
415
|
+
const registered = register ? register(props.name) : {};
|
|
416
|
+
|
|
417
|
+
return h('input', {
|
|
418
|
+
type: 'radio',
|
|
419
|
+
value: radioValue,
|
|
420
|
+
...rest,
|
|
421
|
+
checked: registered.value === radioValue,
|
|
422
|
+
onChange: (e) => {
|
|
423
|
+
if (e.target.checked && registered.onInput) {
|
|
424
|
+
registered.onInput({ target: { value: radioValue } });
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// --- Form Error Display ---
|
|
431
|
+
|
|
432
|
+
export function ErrorMessage({ name, errors, render }) {
|
|
433
|
+
const error = errors ? errors()[name] : null;
|
|
434
|
+
if (!error) return null;
|
|
435
|
+
|
|
436
|
+
if (render) {
|
|
437
|
+
return render({ message: error.message, type: error.type });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return h('span', { class: 'what-error', role: 'alert' }, error.message);
|
|
441
|
+
}
|