what-core 0.4.1 → 0.5.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/components.js +213 -319
- package/dist/dom.js +730 -857
- package/dist/h.js +140 -191
- package/dist/head.js +42 -59
- package/dist/helpers.js +124 -187
- package/dist/hooks.js +186 -279
- package/dist/reactive.js +244 -317
- package/dist/store.js +73 -118
- package/dist/what.js +5 -3
- package/index.d.ts +391 -152
- package/package.json +4 -3
- package/render.d.ts +11 -0
- package/src/a11y.js +52 -6
- package/src/dom.js +137 -22
- package/src/form.js +85 -54
- package/src/helpers.js +1 -12
- package/src/hooks.js +11 -0
- package/src/index.js +1 -1
- package/src/render.js +114 -51
- package/src/store.js +6 -1
package/render.d.ts
ADDED
package/src/a11y.js
CHANGED
|
@@ -24,6 +24,30 @@ export function useFocus() {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Capture/restore focus around transient UI (dialogs, popovers, drawers).
|
|
28
|
+
// Parent components can call capture() before opening and restore() on close.
|
|
29
|
+
export function useFocusRestore() {
|
|
30
|
+
const previousFocusRef = { current: null };
|
|
31
|
+
|
|
32
|
+
function capture(target) {
|
|
33
|
+
if (typeof document === 'undefined') return;
|
|
34
|
+
previousFocusRef.current = target || document.activeElement || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function restore(fallbackTarget) {
|
|
38
|
+
const target = previousFocusRef.current || fallbackTarget;
|
|
39
|
+
if (target && typeof target.focus === 'function') {
|
|
40
|
+
target.focus();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
capture,
|
|
46
|
+
restore,
|
|
47
|
+
previous: () => previousFocusRef.current,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
27
51
|
// --- Focus Trap ---
|
|
28
52
|
// Keep focus within a container (for modals, dialogs, etc.)
|
|
29
53
|
|
|
@@ -36,7 +60,7 @@ export function useFocusTrap(containerRef) {
|
|
|
36
60
|
previousFocus = document.activeElement;
|
|
37
61
|
const container = containerRef.current || containerRef;
|
|
38
62
|
|
|
39
|
-
if (!container) return;
|
|
63
|
+
if (!container || typeof container.querySelectorAll !== 'function') return;
|
|
40
64
|
|
|
41
65
|
// Find all focusable elements
|
|
42
66
|
const focusables = getFocusableElements(container);
|
|
@@ -104,14 +128,31 @@ function getFocusableElements(container) {
|
|
|
104
128
|
|
|
105
129
|
export function FocusTrap({ children, active = true }) {
|
|
106
130
|
const containerRef = { current: null };
|
|
131
|
+
const refVersion = signal(0);
|
|
107
132
|
const trap = useFocusTrap(containerRef);
|
|
133
|
+
let trapCleanup = null;
|
|
134
|
+
|
|
135
|
+
const setRef = (el) => {
|
|
136
|
+
containerRef.current = el;
|
|
137
|
+
refVersion.set(v => v + 1);
|
|
138
|
+
};
|
|
108
139
|
|
|
109
140
|
// Scope the effect to the component lifecycle so it disposes on unmount
|
|
110
141
|
const dispose = effect(() => {
|
|
111
|
-
|
|
112
|
-
|
|
142
|
+
// Re-run activation after the ref element is attached/updated.
|
|
143
|
+
refVersion();
|
|
144
|
+
|
|
145
|
+
if (trapCleanup) {
|
|
146
|
+
trapCleanup();
|
|
147
|
+
trapCleanup = null;
|
|
148
|
+
trap.deactivate();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (active && containerRef.current) {
|
|
152
|
+
trapCleanup = trap.activate();
|
|
113
153
|
return () => {
|
|
114
|
-
|
|
154
|
+
trapCleanup?.();
|
|
155
|
+
trapCleanup = null;
|
|
115
156
|
trap.deactivate();
|
|
116
157
|
};
|
|
117
158
|
}
|
|
@@ -121,10 +162,15 @@ export function FocusTrap({ children, active = true }) {
|
|
|
121
162
|
const ctx = getCurrentComponent?.();
|
|
122
163
|
if (ctx) {
|
|
123
164
|
ctx._cleanupCallbacks = ctx._cleanupCallbacks || [];
|
|
124
|
-
ctx._cleanupCallbacks.push(
|
|
165
|
+
ctx._cleanupCallbacks.push(() => {
|
|
166
|
+
dispose();
|
|
167
|
+
trapCleanup?.();
|
|
168
|
+
trapCleanup = null;
|
|
169
|
+
trap.deactivate();
|
|
170
|
+
});
|
|
125
171
|
}
|
|
126
172
|
|
|
127
|
-
return h('div', { ref:
|
|
173
|
+
return h('div', { ref: setRef }, children);
|
|
128
174
|
}
|
|
129
175
|
|
|
130
176
|
// --- Screen Reader Announcements ---
|
package/src/dom.js
CHANGED
|
@@ -33,6 +33,16 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
|
33
33
|
// Track all mounted component contexts for disposal
|
|
34
34
|
const mountedComponents = new Set();
|
|
35
35
|
|
|
36
|
+
function isDomNode(value) {
|
|
37
|
+
if (!value || typeof value !== 'object') return false;
|
|
38
|
+
if (typeof Node !== 'undefined' && value instanceof Node) return true;
|
|
39
|
+
return typeof value.nodeType === 'number' && typeof value.nodeName === 'string';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isVNode(value) {
|
|
43
|
+
return !!value && typeof value === 'object' && (value._vnode === true || 'tag' in value);
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
// Dispose a component: run effect cleanups, hook cleanups, onCleanup callbacks
|
|
37
47
|
function disposeComponent(ctx) {
|
|
38
48
|
if (ctx.disposed) return;
|
|
@@ -60,12 +70,16 @@ function disposeComponent(ctx) {
|
|
|
60
70
|
mountedComponents.delete(ctx);
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
// Dispose all components attached to a DOM subtree
|
|
64
|
-
function disposeTree(node) {
|
|
73
|
+
// Dispose all components and reactive effects attached to a DOM subtree
|
|
74
|
+
export function disposeTree(node) {
|
|
65
75
|
if (!node) return;
|
|
66
76
|
if (node._componentCtx) {
|
|
67
77
|
disposeComponent(node._componentCtx);
|
|
68
78
|
}
|
|
79
|
+
// Dispose reactive function child effects ({() => ...} wrappers)
|
|
80
|
+
if (node._dispose) {
|
|
81
|
+
try { node._dispose(); } catch (e) { /* already disposed */ }
|
|
82
|
+
}
|
|
69
83
|
if (node.childNodes) {
|
|
70
84
|
for (const child of node.childNodes) {
|
|
71
85
|
disposeTree(child);
|
|
@@ -90,7 +104,7 @@ export function mount(vnode, container) {
|
|
|
90
104
|
|
|
91
105
|
// --- Create DOM from VNode ---
|
|
92
106
|
|
|
93
|
-
function createDOM(vnode, parent, isSvg) {
|
|
107
|
+
export function createDOM(vnode, parent, isSvg) {
|
|
94
108
|
// Null/false/true → placeholder comment (preserves child indices for reconciliation)
|
|
95
109
|
if (vnode == null || vnode === false || vnode === true) {
|
|
96
110
|
return document.createComment('');
|
|
@@ -101,16 +115,34 @@ function createDOM(vnode, parent, isSvg) {
|
|
|
101
115
|
return document.createTextNode(String(vnode));
|
|
102
116
|
}
|
|
103
117
|
|
|
104
|
-
//
|
|
118
|
+
// DOM node passthrough (compiler-first components can return real nodes)
|
|
119
|
+
if (isDomNode(vnode)) {
|
|
120
|
+
return vnode;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Reactive function child — creates a wrapper that updates fine-grained
|
|
124
|
+
// Handles both primitives ({() => count()}) and vnodes ({() => items().map(...)})
|
|
105
125
|
if (typeof vnode === 'function') {
|
|
106
|
-
const
|
|
107
|
-
|
|
126
|
+
const wrapper = document.createElement('what-c');
|
|
127
|
+
let mounted = false;
|
|
128
|
+
const dispose = effect(() => {
|
|
108
129
|
const val = vnode();
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
// Normalize: null/false/true → empty, primitives and vnodes → array
|
|
131
|
+
const vnodes = (val == null || val === false || val === true)
|
|
132
|
+
? []
|
|
133
|
+
: Array.isArray(val) ? val : [val];
|
|
134
|
+
if (!mounted) {
|
|
135
|
+
mounted = true;
|
|
136
|
+
for (const v of vnodes) {
|
|
137
|
+
const node = createDOM(v, wrapper, parent?._isSvg);
|
|
138
|
+
if (node) wrapper.appendChild(node);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
reconcileChildren(wrapper, vnodes);
|
|
142
|
+
}
|
|
112
143
|
});
|
|
113
|
-
|
|
144
|
+
wrapper._dispose = dispose;
|
|
145
|
+
return wrapper;
|
|
114
146
|
}
|
|
115
147
|
|
|
116
148
|
// Array (fragment)
|
|
@@ -123,6 +155,11 @@ function createDOM(vnode, parent, isSvg) {
|
|
|
123
155
|
return frag;
|
|
124
156
|
}
|
|
125
157
|
|
|
158
|
+
// Unknown object child fallback
|
|
159
|
+
if (!isVNode(vnode)) {
|
|
160
|
+
return document.createTextNode(String(vnode));
|
|
161
|
+
}
|
|
162
|
+
|
|
126
163
|
// Component
|
|
127
164
|
if (typeof vnode.tag === 'function') {
|
|
128
165
|
return createComponent(vnode, parent, isSvg);
|
|
@@ -137,9 +174,16 @@ function createDOM(vnode, parent, isSvg) {
|
|
|
137
174
|
: document.createElement(vnode.tag);
|
|
138
175
|
|
|
139
176
|
applyProps(el, vnode.props, {}, svgContext);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
177
|
+
const hasRawHtml = vnode.props && (
|
|
178
|
+
Object.prototype.hasOwnProperty.call(vnode.props, 'dangerouslySetInnerHTML') ||
|
|
179
|
+
Object.prototype.hasOwnProperty.call(vnode.props, 'innerHTML')
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (!hasRawHtml) {
|
|
183
|
+
for (const child of vnode.children) {
|
|
184
|
+
const node = createDOM(child, el, svgContext && vnode.tag !== 'foreignObject');
|
|
185
|
+
if (node) el.appendChild(node);
|
|
186
|
+
}
|
|
143
187
|
}
|
|
144
188
|
|
|
145
189
|
// Store vnode on element for diffing
|
|
@@ -265,6 +309,7 @@ function createComponent(vnode, parent, isSvg) {
|
|
|
265
309
|
});
|
|
266
310
|
|
|
267
311
|
ctx.effects.push(dispose);
|
|
312
|
+
wrapper._vnode = vnode; // Store vnode for keyed reconciliation
|
|
268
313
|
return wrapper;
|
|
269
314
|
}
|
|
270
315
|
|
|
@@ -627,18 +672,41 @@ function patchNode(parent, domNode, vnode) {
|
|
|
627
672
|
return placeholder;
|
|
628
673
|
}
|
|
629
674
|
|
|
630
|
-
// Reactive function child — replace whatever's there with a reactive
|
|
675
|
+
// Reactive function child — replace whatever's there with a reactive wrapper
|
|
631
676
|
if (typeof vnode === 'function') {
|
|
632
|
-
const
|
|
633
|
-
|
|
677
|
+
const wrapper = document.createElement('what-c');
|
|
678
|
+
let mounted = false;
|
|
679
|
+
const dispose = effect(() => {
|
|
634
680
|
const val = vnode();
|
|
635
|
-
|
|
681
|
+
const vnodes = (val == null || val === false || val === true)
|
|
682
|
+
? []
|
|
683
|
+
: Array.isArray(val) ? val : [val];
|
|
684
|
+
if (!mounted) {
|
|
685
|
+
mounted = true;
|
|
686
|
+
for (const v of vnodes) {
|
|
687
|
+
const node = createDOM(v, wrapper);
|
|
688
|
+
if (node) wrapper.appendChild(node);
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
reconcileChildren(wrapper, vnodes);
|
|
692
|
+
}
|
|
636
693
|
});
|
|
694
|
+
wrapper._dispose = dispose;
|
|
637
695
|
if (domNode && domNode.parentNode) {
|
|
638
696
|
disposeTree(domNode);
|
|
639
|
-
parent.replaceChild(
|
|
697
|
+
parent.replaceChild(wrapper, domNode);
|
|
640
698
|
}
|
|
641
|
-
return
|
|
699
|
+
return wrapper;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// DOM node passthrough
|
|
703
|
+
if (isDomNode(vnode)) {
|
|
704
|
+
if (domNode === vnode) return domNode;
|
|
705
|
+
if (domNode && domNode.parentNode) {
|
|
706
|
+
disposeTree(domNode);
|
|
707
|
+
parent.replaceChild(vnode, domNode);
|
|
708
|
+
}
|
|
709
|
+
return vnode;
|
|
642
710
|
}
|
|
643
711
|
|
|
644
712
|
// Text
|
|
@@ -707,6 +775,19 @@ function patchNode(parent, domNode, vnode) {
|
|
|
707
775
|
return startMarker;
|
|
708
776
|
}
|
|
709
777
|
|
|
778
|
+
// Unknown object child fallback
|
|
779
|
+
if (!isVNode(vnode)) {
|
|
780
|
+
const text = String(vnode);
|
|
781
|
+
if (domNode.nodeType === 3) {
|
|
782
|
+
if (domNode.textContent !== text) domNode.textContent = text;
|
|
783
|
+
return domNode;
|
|
784
|
+
}
|
|
785
|
+
const newNode = document.createTextNode(text);
|
|
786
|
+
disposeTree(domNode);
|
|
787
|
+
parent.replaceChild(newNode, domNode);
|
|
788
|
+
return newNode;
|
|
789
|
+
}
|
|
790
|
+
|
|
710
791
|
// Component
|
|
711
792
|
if (typeof vnode.tag === 'function') {
|
|
712
793
|
// Check if old node is a component wrapper for the same component
|
|
@@ -714,6 +795,7 @@ function patchNode(parent, domNode, vnode) {
|
|
|
714
795
|
&& domNode._componentCtx.Component === vnode.tag) {
|
|
715
796
|
// Same component — update props reactively, let its effect re-render
|
|
716
797
|
domNode._componentCtx._propsSignal.set({ ...vnode.props, children: vnode.children });
|
|
798
|
+
domNode._vnode = vnode; // Keep vnode current for keyed reconciliation
|
|
717
799
|
return domNode;
|
|
718
800
|
}
|
|
719
801
|
// Different component or not a component — dispose old, create new
|
|
@@ -726,8 +808,26 @@ function patchNode(parent, domNode, vnode) {
|
|
|
726
808
|
// Element: same tag? Patch props + children
|
|
727
809
|
if (domNode.nodeType === 1 && domNode.tagName.toLowerCase() === vnode.tag) {
|
|
728
810
|
const oldProps = domNode._vnode?.props || {};
|
|
729
|
-
|
|
730
|
-
|
|
811
|
+
const nextProps = vnode.props || {};
|
|
812
|
+
const hadRawHtml = Object.prototype.hasOwnProperty.call(oldProps, 'dangerouslySetInnerHTML')
|
|
813
|
+
|| Object.prototype.hasOwnProperty.call(oldProps, 'innerHTML');
|
|
814
|
+
const hasRawHtml = Object.prototype.hasOwnProperty.call(nextProps, 'dangerouslySetInnerHTML')
|
|
815
|
+
|| Object.prototype.hasOwnProperty.call(nextProps, 'innerHTML');
|
|
816
|
+
|
|
817
|
+
// If switching from normal children to raw HTML, dispose existing child effects first.
|
|
818
|
+
if (hasRawHtml && !hadRawHtml) {
|
|
819
|
+
for (const child of Array.from(domNode.childNodes)) {
|
|
820
|
+
disposeTree(child);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
applyProps(domNode, nextProps, oldProps);
|
|
825
|
+
|
|
826
|
+
// Raw HTML props own the element's children. Skip vnode child reconciliation.
|
|
827
|
+
if (!hasRawHtml) {
|
|
828
|
+
reconcileChildren(domNode, vnode.children);
|
|
829
|
+
}
|
|
830
|
+
|
|
731
831
|
domNode._vnode = vnode;
|
|
732
832
|
return domNode;
|
|
733
833
|
}
|
|
@@ -856,7 +956,17 @@ function setProp(el, key, value, isSvg) {
|
|
|
856
956
|
|
|
857
957
|
// dangerouslySetInnerHTML
|
|
858
958
|
if (key === 'dangerouslySetInnerHTML') {
|
|
859
|
-
el.innerHTML = value
|
|
959
|
+
el.innerHTML = value?.__html ?? '';
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// innerHTML convenience alias
|
|
964
|
+
if (key === 'innerHTML') {
|
|
965
|
+
if (value && typeof value === 'object' && '__html' in value) {
|
|
966
|
+
el.innerHTML = value.__html ?? '';
|
|
967
|
+
} else {
|
|
968
|
+
el.innerHTML = value ?? '';
|
|
969
|
+
}
|
|
860
970
|
return;
|
|
861
971
|
}
|
|
862
972
|
|
|
@@ -912,5 +1022,10 @@ function removeProp(el, key, oldValue) {
|
|
|
912
1022
|
return;
|
|
913
1023
|
}
|
|
914
1024
|
|
|
1025
|
+
if (key === 'dangerouslySetInnerHTML' || key === 'innerHTML') {
|
|
1026
|
+
el.innerHTML = '';
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
915
1030
|
el.removeAttribute(key);
|
|
916
1031
|
}
|
package/src/form.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
// What Framework - Form Utilities
|
|
2
2
|
// Controlled inputs, validation, and form state management
|
|
3
3
|
|
|
4
|
-
import { signal, computed, batch
|
|
4
|
+
import { signal, computed, batch } from './reactive.js';
|
|
5
|
+
import { getCurrentComponent } from './dom.js';
|
|
5
6
|
import { h } from './h.js';
|
|
6
7
|
|
|
7
8
|
// --- useForm Hook ---
|
|
8
9
|
// Complete form state management with validation
|
|
9
10
|
|
|
10
11
|
export function useForm(options = {}) {
|
|
12
|
+
// Hook-stable behavior inside components
|
|
13
|
+
const ctx = getCurrentComponent?.();
|
|
14
|
+
if (ctx) {
|
|
15
|
+
const index = ctx.hookIndex++;
|
|
16
|
+
if (!ctx.hooks[index]) {
|
|
17
|
+
ctx.hooks[index] = createFormController(options);
|
|
18
|
+
}
|
|
19
|
+
return ctx.hooks[index];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Standalone usage outside component scope
|
|
23
|
+
return createFormController(options);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createFormController(options = {}) {
|
|
11
27
|
const {
|
|
12
28
|
defaultValues = {},
|
|
13
29
|
mode = 'onSubmit', // 'onSubmit' | 'onChange' | 'onBlur'
|
|
@@ -19,6 +35,7 @@ export function useForm(options = {}) {
|
|
|
19
35
|
const fieldSignals = {};
|
|
20
36
|
const errorSignals = {};
|
|
21
37
|
const touchedSignals = {};
|
|
38
|
+
const errorsState = signal({});
|
|
22
39
|
|
|
23
40
|
function getFieldSignal(name) {
|
|
24
41
|
if (!fieldSignals[name]) {
|
|
@@ -47,32 +64,55 @@ export function useForm(options = {}) {
|
|
|
47
64
|
const isSubmitted = signal(false);
|
|
48
65
|
const submitCount = signal(0);
|
|
49
66
|
|
|
50
|
-
// Helper: get all current values as a plain object
|
|
51
|
-
|
|
67
|
+
// Helper: get all current values as a plain object.
|
|
68
|
+
// tracked=true subscribes to all known fields; tracked=false is snapshot-only.
|
|
69
|
+
function getAllValues(tracked = false) {
|
|
52
70
|
const result = { ...defaultValues };
|
|
53
71
|
for (const [name, sig] of Object.entries(fieldSignals)) {
|
|
54
|
-
result[name] = sig.peek();
|
|
72
|
+
result[name] = tracked ? sig() : sig.peek();
|
|
55
73
|
}
|
|
56
74
|
return result;
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
// Helper: get all current errors as a plain object
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
// Helper: get all current errors as a plain object.
|
|
78
|
+
// tracked=true subscribes to all known field errors; tracked=false is snapshot-only.
|
|
79
|
+
function getAllErrors(tracked = false) {
|
|
80
|
+
return tracked ? errorsState() : errorsState.peek();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function setFieldError(name, error) {
|
|
84
|
+
const nextError = error ?? null;
|
|
85
|
+
getErrorSignal(name).set(nextError);
|
|
86
|
+
errorsState.set((prev) => {
|
|
87
|
+
const prevError = prev[name];
|
|
88
|
+
if (prevError === nextError) return prev;
|
|
89
|
+
if (nextError == null) {
|
|
90
|
+
if (!Object.prototype.hasOwnProperty.call(prev, name)) return prev;
|
|
91
|
+
const next = { ...prev };
|
|
92
|
+
delete next[name];
|
|
93
|
+
return next;
|
|
94
|
+
}
|
|
95
|
+
return { ...prev, [name]: nextError };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function replaceAllErrors(nextErrors = {}) {
|
|
100
|
+
const normalized = nextErrors || {};
|
|
101
|
+
batch(() => {
|
|
102
|
+
for (const [name, sig] of Object.entries(errorSignals)) {
|
|
103
|
+
if (!Object.prototype.hasOwnProperty.call(normalized, name)) {
|
|
104
|
+
sig.set(null);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const [name, err] of Object.entries(normalized)) {
|
|
108
|
+
getErrorSignal(name).set(err ?? null);
|
|
109
|
+
}
|
|
110
|
+
errorsState.set({ ...normalized });
|
|
111
|
+
});
|
|
67
112
|
}
|
|
68
113
|
|
|
69
114
|
// Computed states
|
|
70
|
-
const isValid = computed(() =>
|
|
71
|
-
for (const sig of Object.values(errorSignals)) {
|
|
72
|
-
if (sig()) return false;
|
|
73
|
-
}
|
|
74
|
-
return true;
|
|
75
|
-
});
|
|
115
|
+
const isValid = computed(() => Object.keys(getAllErrors(true)).length === 0);
|
|
76
116
|
|
|
77
117
|
const dirtyFields = computed(() => {
|
|
78
118
|
const dirty = {};
|
|
@@ -88,31 +128,16 @@ export function useForm(options = {}) {
|
|
|
88
128
|
async function validate(fieldName) {
|
|
89
129
|
if (!resolver) return true;
|
|
90
130
|
|
|
91
|
-
const result = await resolver(getAllValues());
|
|
131
|
+
const result = await resolver(getAllValues(false));
|
|
132
|
+
const nextErrors = result?.errors || {};
|
|
92
133
|
|
|
93
134
|
if (fieldName) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
errSig.set(result.errors[fieldName]);
|
|
98
|
-
return false;
|
|
99
|
-
} else {
|
|
100
|
-
errSig.set(null);
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
135
|
+
const nextError = nextErrors[fieldName] ?? null;
|
|
136
|
+
setFieldError(fieldName, nextError);
|
|
137
|
+
return !nextError;
|
|
103
138
|
} else {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// Clear existing errors
|
|
107
|
-
for (const sig of Object.values(errorSignals)) {
|
|
108
|
-
sig.set(null);
|
|
109
|
-
}
|
|
110
|
-
// Set new errors
|
|
111
|
-
for (const [name, err] of Object.entries(result.errors || {})) {
|
|
112
|
-
getErrorSignal(name).set(err);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
return Object.keys(result.errors || {}).length === 0;
|
|
139
|
+
replaceAllErrors(nextErrors);
|
|
140
|
+
return Object.keys(nextErrors).length === 0;
|
|
116
141
|
}
|
|
117
142
|
}
|
|
118
143
|
|
|
@@ -162,21 +187,17 @@ export function useForm(options = {}) {
|
|
|
162
187
|
|
|
163
188
|
// Set error for a field
|
|
164
189
|
function setError(name, error) {
|
|
165
|
-
|
|
190
|
+
setFieldError(name, error);
|
|
166
191
|
}
|
|
167
192
|
|
|
168
193
|
// Clear error for a field
|
|
169
194
|
function clearError(name) {
|
|
170
|
-
|
|
195
|
+
setFieldError(name, null);
|
|
171
196
|
}
|
|
172
197
|
|
|
173
198
|
// Clear all errors
|
|
174
199
|
function clearErrors() {
|
|
175
|
-
|
|
176
|
-
for (const sig of Object.values(errorSignals)) {
|
|
177
|
-
sig.set(null);
|
|
178
|
-
}
|
|
179
|
-
});
|
|
200
|
+
replaceAllErrors({});
|
|
180
201
|
}
|
|
181
202
|
|
|
182
203
|
// Reset form
|
|
@@ -188,6 +209,7 @@ export function useForm(options = {}) {
|
|
|
188
209
|
for (const sig of Object.values(errorSignals)) {
|
|
189
210
|
sig.set(null);
|
|
190
211
|
}
|
|
212
|
+
errorsState.set({});
|
|
191
213
|
for (const sig of Object.values(touchedSignals)) {
|
|
192
214
|
sig.set(false);
|
|
193
215
|
}
|
|
@@ -210,7 +232,7 @@ export function useForm(options = {}) {
|
|
|
210
232
|
if (isFormValid) {
|
|
211
233
|
await onValid(getAllValues());
|
|
212
234
|
} else if (onInvalid) {
|
|
213
|
-
onInvalid(getAllErrors());
|
|
235
|
+
onInvalid(getAllErrors(false));
|
|
214
236
|
}
|
|
215
237
|
|
|
216
238
|
isSubmitting.set(false);
|
|
@@ -223,7 +245,7 @@ export function useForm(options = {}) {
|
|
|
223
245
|
return computed(() => getFieldSignal(name)());
|
|
224
246
|
}
|
|
225
247
|
// Watch all: return a computed that reads all field signals
|
|
226
|
-
return computed(() => getAllValues());
|
|
248
|
+
return computed(() => getAllValues(true));
|
|
227
249
|
}
|
|
228
250
|
|
|
229
251
|
return {
|
|
@@ -237,10 +259,11 @@ export function useForm(options = {}) {
|
|
|
237
259
|
reset,
|
|
238
260
|
watch,
|
|
239
261
|
validate,
|
|
240
|
-
// Form state
|
|
262
|
+
// Form state
|
|
241
263
|
formState: {
|
|
242
|
-
get values() { return getAllValues(); },
|
|
243
|
-
get errors() { return getAllErrors(); },
|
|
264
|
+
get values() { return getAllValues(true); },
|
|
265
|
+
get errors() { return getAllErrors(true); },
|
|
266
|
+
error: (name) => getErrorSignal(name)(),
|
|
244
267
|
get touched() {
|
|
245
268
|
const result = {};
|
|
246
269
|
for (const [name, sig] of Object.entries(touchedSignals)) {
|
|
@@ -497,8 +520,16 @@ export function Radio(props) {
|
|
|
497
520
|
|
|
498
521
|
// --- Form Error Display ---
|
|
499
522
|
|
|
500
|
-
export function ErrorMessage({ name, errors, render }) {
|
|
501
|
-
const error =
|
|
523
|
+
export function ErrorMessage({ name, formState, errors, render }) {
|
|
524
|
+
const error = formState && typeof formState.error === 'function'
|
|
525
|
+
? formState.error(name)
|
|
526
|
+
: (
|
|
527
|
+
(
|
|
528
|
+
formState?.errors != null
|
|
529
|
+
? formState.errors
|
|
530
|
+
: (typeof errors === 'function' ? errors() : errors)
|
|
531
|
+
)?.[name] || null
|
|
532
|
+
);
|
|
502
533
|
if (!error) return null;
|
|
503
534
|
|
|
504
535
|
if (render) {
|
package/src/helpers.js
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
// What Framework - Helpers & Utilities
|
|
2
2
|
// Commonly needed patterns, zero overhead.
|
|
3
3
|
|
|
4
|
-
import { signal, effect,
|
|
5
|
-
|
|
6
|
-
// --- show(condition, vnode) --- [DEPRECATED: use <Show> component instead]
|
|
7
|
-
// Conditional rendering. More readable than ternary.
|
|
8
|
-
let _showWarned = false;
|
|
9
|
-
export function show(condition, vnode, fallback = null) {
|
|
10
|
-
if (!_showWarned) {
|
|
11
|
-
_showWarned = true;
|
|
12
|
-
console.warn('[what] show() is deprecated. Use the <Show> component or ternary expressions instead.');
|
|
13
|
-
}
|
|
14
|
-
return condition ? vnode : fallback;
|
|
15
|
-
}
|
|
4
|
+
import { signal, effect, __DEV__ } from './reactive.js';
|
|
16
5
|
|
|
17
6
|
// --- each(list, fn) --- [DEPRECATED: use <For> component or .map() instead]
|
|
18
7
|
// Keyed list rendering. Optimized for reconciliation.
|
package/src/hooks.js
CHANGED
|
@@ -20,6 +20,8 @@ function getHook(ctx) {
|
|
|
20
20
|
return { index, exists: index < ctx.hooks.length };
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
let _useMemoNoDepsWarned = false;
|
|
24
|
+
|
|
23
25
|
// --- useState ---
|
|
24
26
|
// Returns [value, setter]. Setter triggers re-render of this component only.
|
|
25
27
|
|
|
@@ -97,6 +99,15 @@ export function useMemo(fn, deps) {
|
|
|
97
99
|
const ctx = getCtx();
|
|
98
100
|
const { index, exists } = getHook(ctx);
|
|
99
101
|
|
|
102
|
+
if (__DEV__ && deps === undefined && !_useMemoNoDepsWarned) {
|
|
103
|
+
_useMemoNoDepsWarned = true;
|
|
104
|
+
console.warn(
|
|
105
|
+
'[what] useMemo() called without a deps array. ' +
|
|
106
|
+
'This recomputes every render. Use useComputed() for signal-derived values, ' +
|
|
107
|
+
'or pass deps to useMemo().'
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
100
111
|
if (!exists) {
|
|
101
112
|
ctx.hooks[index] = { value: undefined, deps: undefined };
|
|
102
113
|
}
|
package/src/index.js
CHANGED
|
@@ -41,7 +41,6 @@ export { Head, clearHead } from './head.js';
|
|
|
41
41
|
|
|
42
42
|
// Utilities
|
|
43
43
|
export {
|
|
44
|
-
show,
|
|
45
44
|
each,
|
|
46
45
|
cls,
|
|
47
46
|
style,
|
|
@@ -87,6 +86,7 @@ export {
|
|
|
87
86
|
// Accessibility utilities
|
|
88
87
|
export {
|
|
89
88
|
useFocus,
|
|
89
|
+
useFocusRestore,
|
|
90
90
|
useFocusTrap,
|
|
91
91
|
FocusTrap,
|
|
92
92
|
announce,
|