cx 26.1.13 → 26.2.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/build/charts/Marker.d.ts +1 -1
- package/build/charts/Marker.d.ts.map +1 -1
- package/build/charts/Marker.js +16 -6
- package/build/charts/MouseTracker.d.ts +2 -0
- package/build/charts/MouseTracker.d.ts.map +1 -1
- package/build/charts/helpers/PointReducer.d.ts +2 -2
- package/build/charts/helpers/PointReducer.d.ts.map +1 -1
- package/build/data/View.d.ts +5 -3
- package/build/data/View.d.ts.map +1 -1
- package/build/data/View.js +3 -1
- package/build/data/ops/findTreeNode.d.ts +20 -1
- package/build/data/ops/findTreeNode.d.ts.map +1 -1
- package/build/data/ops/findTreeNode.js +19 -0
- package/build/data/ops/findTreePath.d.ts +1 -1
- package/build/data/ops/findTreePath.d.ts.map +1 -1
- package/build/data/ops/findTreePath.js +1 -1
- package/build/data/ops/removeTreeNodes.d.ts +14 -1
- package/build/data/ops/removeTreeNodes.d.ts.map +1 -1
- package/build/data/ops/removeTreeNodes.js +13 -0
- package/build/data/ops/updateArray.d.ts +1 -1
- package/build/data/ops/updateArray.d.ts.map +1 -1
- package/build/data/ops/updateArray.js +1 -1
- package/build/data/ops/updateTree.d.ts +20 -1
- package/build/data/ops/updateTree.d.ts.map +1 -1
- package/build/data/ops/updateTree.js +19 -0
- package/build/jsx-runtime.d.ts +1 -0
- package/build/jsx-runtime.d.ts.map +1 -1
- package/build/jsx-runtime.js +3 -1
- package/build/svg/Rectangle.d.ts +6 -4
- package/build/svg/Rectangle.d.ts.map +1 -1
- package/build/svg/Rectangle.js +9 -7
- package/build/ui/Instance.d.ts +1 -1
- package/build/ui/Instance.d.ts.map +1 -1
- package/build/ui/Instance.js +18 -8
- package/build/ui/IsolatedScope.d.ts +2 -1
- package/build/ui/IsolatedScope.d.ts.map +1 -1
- package/build/ui/Prop.d.ts +1 -1
- package/build/ui/Prop.d.ts.map +1 -1
- package/build/ui/Widget.d.ts +2 -0
- package/build/ui/Widget.d.ts.map +1 -1
- package/build/ui/Widget.js +4 -0
- package/build/ui/adapter/GroupAdapter.d.ts +4 -4
- package/build/ui/adapter/GroupAdapter.d.ts.map +1 -1
- package/build/ui/adapter/GroupAdapter.js +4 -4
- package/build/ui/adapter/TreeAdapter.d.ts +5 -3
- package/build/ui/adapter/TreeAdapter.d.ts.map +1 -1
- package/build/ui/adapter/TreeAdapter.js +12 -5
- package/build/ui/app/startAppLoop.d.ts +2 -2
- package/build/ui/app/startAppLoop.d.ts.map +1 -1
- package/build/ui/app/startHotAppLoop.d.ts +4 -4
- package/build/ui/app/startHotAppLoop.d.ts.map +1 -1
- package/build/ui/app/startHotAppLoop.js +1 -1
- package/build/ui/batchUpdates.d.ts.map +1 -1
- package/build/ui/batchUpdates.js +3 -4
- package/build/widgets/Button.d.ts +0 -7
- package/build/widgets/Button.d.ts.map +1 -1
- package/build/widgets/HtmlElement.d.ts +2 -2
- package/build/widgets/HtmlElement.d.ts.map +1 -1
- package/build/widgets/form/Checkbox.d.ts +3 -3
- package/build/widgets/form/Checkbox.d.ts.map +1 -1
- package/build/widgets/form/Checkbox.js +11 -6
- package/build/widgets/form/DateTimeField.d.ts +4 -0
- package/build/widgets/form/DateTimeField.d.ts.map +1 -1
- package/build/widgets/form/TextField.d.ts +2 -2
- package/build/widgets/form/TextField.d.ts.map +1 -1
- package/build/widgets/grid/Grid.d.ts +20 -16
- package/build/widgets/grid/Grid.d.ts.map +1 -1
- package/build/widgets/grid/Grid.js +200 -86
- package/build/widgets/nav/Menu.d.ts +2 -0
- package/build/widgets/nav/Menu.d.ts.map +1 -1
- package/build/widgets/nav/Route.js +1 -1
- package/build/widgets/overlay/FlyweightTooltipTracker.d.ts +6 -4
- package/build/widgets/overlay/FlyweightTooltipTracker.d.ts.map +1 -1
- package/build/widgets/overlay/FlyweightTooltipTracker.js +3 -0
- package/build/widgets/overlay/Overlay.d.ts +2 -2
- package/build/widgets/overlay/Overlay.d.ts.map +1 -1
- package/dist/data.js +52 -1
- package/dist/jsx-runtime.js +4 -2
- package/dist/manifest.js +910 -904
- package/dist/svg.js +3 -0
- package/dist/ui.js +1548 -1544
- package/dist/widgets.css +1 -1
- package/dist/widgets.js +395 -4
- package/package.json +2 -2
- package/src/charts/Marker.tsx +448 -394
- package/src/charts/MouseTracker.tsx +3 -0
- package/src/charts/helpers/PointReducer.ts +2 -2
- package/src/data/View.ts +76 -19
- package/src/data/ops/findTreeNode.ts +20 -1
- package/src/data/ops/findTreePath.ts +7 -2
- package/src/data/ops/removeTreeNodes.ts +14 -1
- package/src/data/ops/updateArray.ts +4 -4
- package/src/data/ops/updateTree.ts +32 -6
- package/src/index.scss +6 -6
- package/src/jsx-runtime.spec.tsx +40 -0
- package/src/jsx-runtime.ts +87 -84
- package/src/svg/Rectangle.tsx +80 -73
- package/src/ui/DataProxy.ts +55 -55
- package/src/ui/Instance.ts +142 -45
- package/src/ui/IsolatedScope.ts +4 -2
- package/src/ui/Prop.ts +141 -141
- package/src/ui/Rescope.ts +50 -50
- package/src/ui/Widget.tsx +292 -234
- package/src/ui/adapter/ArrayAdapter.ts +229 -229
- package/src/ui/adapter/GroupAdapter.ts +8 -10
- package/src/ui/adapter/TreeAdapter.ts +75 -15
- package/src/ui/app/Url.spec.ts +1 -1
- package/src/ui/app/startAppLoop.tsx +56 -45
- package/src/ui/app/startHotAppLoop.ts +4 -4
- package/src/ui/batchUpdates.ts +16 -21
- package/src/ui/exprHelpers.ts +96 -96
- package/src/widgets/Button.tsx +0 -8
- package/src/widgets/HtmlElement.spec.tsx +100 -72
- package/src/widgets/HtmlElement.tsx +11 -10
- package/src/widgets/Sandbox.ts +104 -104
- package/src/widgets/Section.scss +55 -55
- package/src/widgets/drag-drop/DropZone.scss +74 -74
- package/src/widgets/form/Checkbox.tsx +296 -243
- package/src/widgets/form/DateTimeField.tsx +6 -0
- package/src/widgets/form/TextField.tsx +2 -2
- package/src/widgets/grid/Grid.scss +43 -10
- package/src/widgets/grid/Grid.tsx +4401 -3848
- package/src/widgets/nav/Menu.tsx +3 -0
- package/src/widgets/nav/Route.ts +1 -1
- package/src/widgets/overlay/FlyweightTooltipTracker.ts +15 -4
- package/src/widgets/overlay/Overlay.tsx +2 -1
- package/src/widgets/overlay/index.d.ts +11 -11
|
@@ -6,66 +6,77 @@ import { Store } from "../../data/Store";
|
|
|
6
6
|
import { Cx } from "../Cx";
|
|
7
7
|
import { Instance } from "../Instance";
|
|
8
8
|
import { Create } from "../../util/Component";
|
|
9
|
+
import { View } from "../../data/View";
|
|
9
10
|
|
|
10
11
|
export interface StartAppLoopOptions {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
destroyDelay?: number;
|
|
13
|
+
removeParentDOMElement?: boolean;
|
|
14
|
+
[key: string]: any;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export function startAppLoop(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
parentDOMElement: HTMLElement,
|
|
19
|
+
storeOrInstance?: View | Instance,
|
|
20
|
+
widget?: Create<typeof Widget> | Create<typeof Widget>[],
|
|
21
|
+
options: StartAppLoopOptions = {},
|
|
21
22
|
): () => void {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
if (!parentDOMElement || parentDOMElement.nodeType !== 1)
|
|
24
|
+
throw new Error(
|
|
25
|
+
"First argument to startAppLoop should be a valid DOM element.",
|
|
26
|
+
);
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
let store: Store | undefined;
|
|
29
|
+
let instance: Instance | undefined;
|
|
30
|
+
let parentInstance: Instance | undefined;
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
if (!storeOrInstance) storeOrInstance = new Store();
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
if ((storeOrInstance as any).notify) store = storeOrInstance as Store;
|
|
35
|
+
else if ((storeOrInstance as any).getChild) {
|
|
36
|
+
if ((storeOrInstance as any).widget === widget)
|
|
37
|
+
instance = storeOrInstance as Instance;
|
|
38
|
+
else parentInstance = storeOrInstance as Instance;
|
|
39
|
+
} else
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Second argument to startAppLoop should be either of type Store or Instance",
|
|
42
|
+
);
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
let content = (
|
|
45
|
+
<Cx
|
|
46
|
+
store={store}
|
|
47
|
+
widget={widget}
|
|
48
|
+
instance={instance}
|
|
49
|
+
parentInstance={parentInstance}
|
|
50
|
+
options={options}
|
|
51
|
+
subscribe
|
|
52
|
+
/>
|
|
53
|
+
) as any;
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
let root = VDOM.DOM.createRoot(parentDOMElement);
|
|
56
|
+
root.render(content);
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
let stopped = false;
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
return function () {
|
|
61
|
+
if (stopped) return;
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
stopped = true;
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
if (!options.destroyDelay) destroy(parentDOMElement, options, root);
|
|
66
|
+
else {
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
destroy(parentDOMElement, options, root);
|
|
69
|
+
}, options.destroyDelay);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
function destroy(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
function destroy(
|
|
75
|
+
parentDOMElement: HTMLElement,
|
|
76
|
+
options: StartAppLoopOptions,
|
|
77
|
+
root: Root,
|
|
78
|
+
): void {
|
|
79
|
+
if (root) root.unmount();
|
|
80
|
+
if (options.removeParentDOMElement && parentDOMElement.parentNode)
|
|
81
|
+
parentDOMElement.parentNode.removeChild(parentDOMElement);
|
|
71
82
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Widget, WidgetConfig } from "../Widget";
|
|
3
|
-
import { Store } from "../../data/Store";
|
|
1
|
+
import { View } from "../../data/View";
|
|
4
2
|
import { Create } from "../../util/Component";
|
|
3
|
+
import { Widget } from "../Widget";
|
|
4
|
+
import { startAppLoop, StartAppLoopOptions } from "./startAppLoop";
|
|
5
5
|
|
|
6
6
|
export interface HotModule {
|
|
7
7
|
hot?: {
|
|
@@ -14,7 +14,7 @@ export interface HotModule {
|
|
|
14
14
|
export function startHotAppLoop(
|
|
15
15
|
appModule: HotModule,
|
|
16
16
|
element: HTMLElement,
|
|
17
|
-
store:
|
|
17
|
+
store: View,
|
|
18
18
|
widgets: Create<typeof Widget> | Create<typeof Widget>[],
|
|
19
19
|
options: StartAppLoopOptions = {},
|
|
20
20
|
): () => void {
|
package/src/ui/batchUpdates.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { VDOM } from
|
|
2
|
-
import { SubscriberList } from
|
|
1
|
+
import { VDOM } from "./VDOM";
|
|
2
|
+
import { SubscriberList } from "../util/SubscriberList";
|
|
3
3
|
|
|
4
4
|
interface UpdateCallback {
|
|
5
5
|
pending: number;
|
|
@@ -16,13 +16,11 @@ export function batchUpdates(callback: () => void): void {
|
|
|
16
16
|
isBatching++;
|
|
17
17
|
try {
|
|
18
18
|
callback();
|
|
19
|
-
}
|
|
20
|
-
finally {
|
|
19
|
+
} finally {
|
|
21
20
|
isBatching--;
|
|
22
21
|
}
|
|
23
22
|
});
|
|
24
|
-
else
|
|
25
|
-
callback();
|
|
23
|
+
else callback();
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
export function isBatchingUpdates(): boolean {
|
|
@@ -30,7 +28,7 @@ export function isBatchingUpdates(): boolean {
|
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
export function notifyBatchedUpdateStarting(): void {
|
|
33
|
-
promiseSubscribers.execute((x: any) =>{
|
|
31
|
+
promiseSubscribers.execute((x: any) => {
|
|
34
32
|
(x as UpdateCallback).pending++;
|
|
35
33
|
});
|
|
36
34
|
}
|
|
@@ -39,14 +37,15 @@ export function notifyBatchedUpdateCompleted(): void {
|
|
|
39
37
|
promiseSubscribers.execute((x: any) => {
|
|
40
38
|
let cb = x as UpdateCallback;
|
|
41
39
|
cb.finished++;
|
|
42
|
-
if (cb.finished >= cb.pending)
|
|
43
|
-
cb.complete(true);
|
|
40
|
+
if (cb.finished >= cb.pending) cb.complete(true);
|
|
44
41
|
});
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
export function batchUpdatesAndNotify(
|
|
45
|
+
callback: () => void,
|
|
46
|
+
notifyCallback: (success: boolean) => void,
|
|
47
|
+
timeout: number = 1000,
|
|
48
|
+
): void {
|
|
50
49
|
let done = false;
|
|
51
50
|
let timer: NodeJS.Timeout | undefined;
|
|
52
51
|
let unsubscribe: (() => void) | undefined;
|
|
@@ -57,21 +56,17 @@ export function batchUpdatesAndNotify(callback: () => void, notifyCallback: (suc
|
|
|
57
56
|
complete: (success?: boolean) => {
|
|
58
57
|
if (!done) {
|
|
59
58
|
done = true;
|
|
60
|
-
if (timer)
|
|
61
|
-
|
|
62
|
-
if (unsubscribe)
|
|
63
|
-
unsubscribe();
|
|
59
|
+
if (timer) clearInterval(timer);
|
|
60
|
+
if (unsubscribe) unsubscribe();
|
|
64
61
|
notifyCallback(!!success);
|
|
65
62
|
}
|
|
66
|
-
}
|
|
63
|
+
},
|
|
67
64
|
};
|
|
68
65
|
|
|
69
66
|
unsubscribe = promiseSubscribers.subscribe(update as any);
|
|
70
67
|
|
|
71
68
|
batchUpdates(callback);
|
|
72
69
|
|
|
73
|
-
if (update.pending <= update.finished)
|
|
74
|
-
|
|
75
|
-
else
|
|
76
|
-
timer = setTimeout(update.complete, timeout);
|
|
70
|
+
if (update.pending <= update.finished) update.complete(true);
|
|
71
|
+
else timer = setTimeout(update.complete, timeout);
|
|
77
72
|
}
|
package/src/ui/exprHelpers.ts
CHANGED
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
import { computable } from "../data/computable";
|
|
2
|
-
import { AccessorChain } from "../data/createAccessorModelProxy";
|
|
3
|
-
import { Selector } from "../data/Selector";
|
|
4
|
-
import { Format } from "../util/Format";
|
|
5
|
-
import { expr } from "./expr";
|
|
6
|
-
|
|
7
|
-
/** Returns a selector that converts the value to boolean using !! */
|
|
8
|
-
export function truthy<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
9
|
-
return expr(arg, (x) => !!x);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Returns a selector that checks if the value is falsy using ! */
|
|
13
|
-
export function falsy<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
14
|
-
return expr(arg, (x) => !x);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Returns a selector that checks if the value is strictly true (=== true) */
|
|
18
|
-
export function isTrue(arg: AccessorChain<any>): Selector<boolean> {
|
|
19
|
-
return expr(arg, (x) => x === true);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Returns a selector that checks if the value is strictly false (=== false) */
|
|
23
|
-
export function isFalse(arg: AccessorChain<any>): Selector<boolean> {
|
|
24
|
-
return expr(arg, (x) => x === false);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Returns a selector that checks if the value is not null or undefined (x != null) */
|
|
28
|
-
export function hasValue<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
29
|
-
return expr(arg, (x) => x != null);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Returns a selector that checks if a string or array is empty (null, undefined, or length === 0) */
|
|
33
|
-
export function isEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
|
|
34
|
-
return expr(arg, (x) => x == null || x.length === 0);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Returns a selector that checks if a string or array is non-empty (not null/undefined and length > 0) */
|
|
38
|
-
export function isNonEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
|
|
39
|
-
return expr(arg, (x) => x != null && x.length > 0);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Returns a selector that checks if the value is less than the given value */
|
|
43
|
-
export function lessThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
44
|
-
return expr(arg, (x) => x < value);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Returns a selector that checks if the value is less than or equal to the given value */
|
|
48
|
-
export function lessThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
49
|
-
return expr(arg, (x) => x <= value);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Returns a selector that checks if the value is greater than the given value */
|
|
53
|
-
export function greaterThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
54
|
-
return expr(arg, (x) => x > value);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Returns a selector that checks if the value is greater than or equal to the given value */
|
|
58
|
-
export function greaterThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
59
|
-
return expr(arg, (x) => x >= value);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Returns a selector that checks if the value equals the given value using == */
|
|
63
|
-
export function equal<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
64
|
-
return expr(arg, (x) => x == value);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Returns a selector that checks if the value does not equal the given value using != */
|
|
68
|
-
export function notEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
69
|
-
return expr(arg, (x) => x != value);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Returns a selector that checks if the value strictly equals the given value using === */
|
|
73
|
-
export function strictEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
74
|
-
return expr(arg, (x) => x === value);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Returns a selector that checks if the value strictly does not equal the given value using !== */
|
|
78
|
-
export function strictNotEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
79
|
-
return expr(arg, (x) => x !== value);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Returns a selector that formats the value using the specified format string.
|
|
83
|
-
* Format strings use semicolon-separated syntax: "formatType;param1;param2"
|
|
84
|
-
* @param arg - The accessor chain to the value
|
|
85
|
-
* @param fmt - The format string
|
|
86
|
-
* @param nullText - Optional text to display for null/undefined values
|
|
87
|
-
* @example
|
|
88
|
-
* format(m.price, "n;2") // formats as number with 2 decimal places
|
|
89
|
-
* format(m.date, "d") // formats as date
|
|
90
|
-
* format(m.value, "p;0;2") // formats as percentage with 0-2 decimal places
|
|
91
|
-
* format(m.value, "n;2", "N/A") // shows "N/A" for null values
|
|
92
|
-
*/
|
|
93
|
-
export function format<V>(arg: AccessorChain<V>, fmt: string, nullText?: string): Selector<string> {
|
|
94
|
-
let f = nullText != null ? `${fmt}|${nullText}` : fmt;
|
|
95
|
-
return computable(arg, (x) => Format.value(x, f));
|
|
96
|
-
}
|
|
1
|
+
import { computable } from "../data/computable";
|
|
2
|
+
import { AccessorChain } from "../data/createAccessorModelProxy";
|
|
3
|
+
import { Selector } from "../data/Selector";
|
|
4
|
+
import { Format } from "../util/Format";
|
|
5
|
+
import { expr } from "./expr";
|
|
6
|
+
|
|
7
|
+
/** Returns a selector that converts the value to boolean using !! */
|
|
8
|
+
export function truthy<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
9
|
+
return expr(arg, (x) => !!x);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Returns a selector that checks if the value is falsy using ! */
|
|
13
|
+
export function falsy<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
14
|
+
return expr(arg, (x) => !x);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Returns a selector that checks if the value is strictly true (=== true) */
|
|
18
|
+
export function isTrue(arg: AccessorChain<any>): Selector<boolean> {
|
|
19
|
+
return expr(arg, (x) => x === true);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Returns a selector that checks if the value is strictly false (=== false) */
|
|
23
|
+
export function isFalse(arg: AccessorChain<any>): Selector<boolean> {
|
|
24
|
+
return expr(arg, (x) => x === false);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Returns a selector that checks if the value is not null or undefined (x != null) */
|
|
28
|
+
export function hasValue<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
29
|
+
return expr(arg, (x) => x != null);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Returns a selector that checks if a string or array is empty (null, undefined, or length === 0) */
|
|
33
|
+
export function isEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
|
|
34
|
+
return expr(arg, (x) => x == null || x.length === 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Returns a selector that checks if a string or array is non-empty (not null/undefined and length > 0) */
|
|
38
|
+
export function isNonEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
|
|
39
|
+
return expr(arg, (x) => x != null && x.length > 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Returns a selector that checks if the value is less than the given value */
|
|
43
|
+
export function lessThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
44
|
+
return expr(arg, (x) => x < value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Returns a selector that checks if the value is less than or equal to the given value */
|
|
48
|
+
export function lessThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
49
|
+
return expr(arg, (x) => x <= value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Returns a selector that checks if the value is greater than the given value */
|
|
53
|
+
export function greaterThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
54
|
+
return expr(arg, (x) => x > value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Returns a selector that checks if the value is greater than or equal to the given value */
|
|
58
|
+
export function greaterThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
59
|
+
return expr(arg, (x) => x >= value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Returns a selector that checks if the value equals the given value using == */
|
|
63
|
+
export function equal<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
64
|
+
return expr(arg, (x) => x == value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Returns a selector that checks if the value does not equal the given value using != */
|
|
68
|
+
export function notEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
69
|
+
return expr(arg, (x) => x != value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Returns a selector that checks if the value strictly equals the given value using === */
|
|
73
|
+
export function strictEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
74
|
+
return expr(arg, (x) => x === value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Returns a selector that checks if the value strictly does not equal the given value using !== */
|
|
78
|
+
export function strictNotEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
79
|
+
return expr(arg, (x) => x !== value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Returns a selector that formats the value using the specified format string.
|
|
83
|
+
* Format strings use semicolon-separated syntax: "formatType;param1;param2"
|
|
84
|
+
* @param arg - The accessor chain to the value
|
|
85
|
+
* @param fmt - The format string
|
|
86
|
+
* @param nullText - Optional text to display for null/undefined values
|
|
87
|
+
* @example
|
|
88
|
+
* format(m.price, "n;2") // formats as number with 2 decimal places
|
|
89
|
+
* format(m.date, "d") // formats as date
|
|
90
|
+
* format(m.value, "p;0;2") // formats as percentage with 0-2 decimal places
|
|
91
|
+
* format(m.value, "n;2", "N/A") // shows "N/A" for null values
|
|
92
|
+
*/
|
|
93
|
+
export function format<V>(arg: AccessorChain<V>, fmt: string, nullText?: string): Selector<string> {
|
|
94
|
+
let f = nullText != null ? `${fmt}|${nullText}` : fmt;
|
|
95
|
+
return computable(arg, (x) => Format.value(x, f));
|
|
96
|
+
}
|
package/src/widgets/Button.tsx
CHANGED
|
@@ -41,14 +41,6 @@ export interface ButtonConfig extends Omit<HtmlElementConfig<"button">, "disable
|
|
|
41
41
|
/** Set to `false` to disable the button. */
|
|
42
42
|
enabled?: BooleanProp;
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
* Click handler.
|
|
46
|
-
*
|
|
47
|
-
* @param e - Event.
|
|
48
|
-
* @param instance - Cx widget instance that fired the event.
|
|
49
|
-
*/
|
|
50
|
-
onClick?: string | ((e: React.MouseEvent, instance: Instance) => void);
|
|
51
|
-
|
|
52
44
|
/** Button type. */
|
|
53
45
|
type?: "submit" | "button";
|
|
54
46
|
|
|
@@ -3,87 +3,115 @@ import assert from "assert";
|
|
|
3
3
|
import { createTestRenderer } from "../util/test/createTestRenderer";
|
|
4
4
|
import { bind } from "../ui/bind";
|
|
5
5
|
import { VDOM } from "../ui/Widget";
|
|
6
|
+
import { Instance } from "../ui/Instance";
|
|
6
7
|
|
|
7
8
|
describe("HtmlElement", () => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
it("renders textual content provided through the text property", async () => {
|
|
10
|
+
let widget = (
|
|
11
|
+
<cx>
|
|
12
|
+
<div text={bind("text")} />
|
|
13
|
+
</cx>
|
|
14
|
+
);
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
let store = new Store({
|
|
17
|
+
data: {
|
|
18
|
+
text: "Test",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
const component = await createTestRenderer(store, widget);
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
let tree = component.toJSON();
|
|
25
|
+
assert(tree && !Array.isArray(tree), "Expected single element");
|
|
26
|
+
assert.equal(tree.type, "div");
|
|
27
|
+
assert.deepEqual(tree.children, ["Test"]);
|
|
28
|
+
});
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
it("allows spread bindings", async () => {
|
|
31
|
+
let store = new Store({
|
|
32
|
+
data: {
|
|
33
|
+
title: "title",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
const component = await createTestRenderer(
|
|
38
|
+
store,
|
|
39
|
+
<cx>
|
|
40
|
+
<a href="#" {...{ title: { bind: "title" } }}>
|
|
41
|
+
Link
|
|
42
|
+
</a>
|
|
43
|
+
</cx>,
|
|
44
|
+
);
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
let tree = component.toJSON();
|
|
47
|
+
assert(tree && !Array.isArray(tree), "Expected single element");
|
|
48
|
+
assert.deepEqual(tree, {
|
|
49
|
+
type: "a",
|
|
50
|
+
children: ["Link"],
|
|
51
|
+
props: {
|
|
52
|
+
href: "#",
|
|
53
|
+
title: "title",
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
});
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
it("supports SVG elements with camelCase attributes", async () => {
|
|
59
|
+
let store = new Store();
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
61
|
+
const component = await createTestRenderer(
|
|
62
|
+
store,
|
|
63
|
+
<cx>
|
|
64
|
+
<svg>
|
|
65
|
+
<path
|
|
66
|
+
d="M200,176V64a23.9,23.9,0,0,0-24-24H40"
|
|
67
|
+
fill="none"
|
|
68
|
+
stroke="#343434"
|
|
69
|
+
strokeLinecap="round"
|
|
70
|
+
strokeLinejoin="round"
|
|
71
|
+
strokeWidth="12"
|
|
72
|
+
/>
|
|
73
|
+
</svg>
|
|
74
|
+
</cx>,
|
|
75
|
+
);
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
let tree = component.toJSON();
|
|
78
|
+
assert(tree && !Array.isArray(tree), "Expected single element");
|
|
79
|
+
assert.equal(tree.type, "svg");
|
|
80
|
+
assert(tree.children && tree.children.length === 1, "Expected one child");
|
|
81
|
+
let path = tree.children[0] as any;
|
|
82
|
+
assert.equal(path.type, "path");
|
|
83
|
+
assert.equal(path.props.d, "M200,176V64a23.9,23.9,0,0,0-24-24H40");
|
|
84
|
+
assert.equal(path.props.fill, "none");
|
|
85
|
+
assert.equal(path.props.stroke, "#343434");
|
|
86
|
+
assert.equal(path.props.strokeLinecap, "round");
|
|
87
|
+
assert.equal(path.props.strokeLinejoin, "round");
|
|
88
|
+
assert.equal(path.props.strokeWidth, "12");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("infers event handler parameter types", async () => {
|
|
92
|
+
let clickedWith: {
|
|
93
|
+
event: React.MouseEvent<HTMLButtonElement>;
|
|
94
|
+
instance: Instance;
|
|
95
|
+
} | null = null;
|
|
96
|
+
|
|
97
|
+
let store = new Store();
|
|
98
|
+
|
|
99
|
+
const component = await createTestRenderer(
|
|
100
|
+
store,
|
|
101
|
+
<cx>
|
|
102
|
+
<button
|
|
103
|
+
onClick={(e, instance) => {
|
|
104
|
+
// TypeScript should infer e as React.MouseEvent<HTMLButtonElement> and instance as Instance
|
|
105
|
+
clickedWith = { event: e, instance };
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
Click me
|
|
109
|
+
</button>
|
|
110
|
+
</cx>,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
let tree = component.toJSON();
|
|
114
|
+
assert(tree && !Array.isArray(tree), "Expected single element");
|
|
115
|
+
assert.equal(tree.type, "button");
|
|
116
|
+
});
|
|
89
117
|
});
|
|
@@ -67,18 +67,19 @@ type CxEventHandler<T> = T extends (event: infer E) => any
|
|
|
67
67
|
// Note: For string literal union props (like SVG's strokeLinecap), we also accept `string`
|
|
68
68
|
// because TypeScript widens string literals to `string` in JSX attribute syntax.
|
|
69
69
|
// This is a known TypeScript behavior where `<path strokeLinecap="round"/>` infers "round" as string.
|
|
70
|
+
// We exclude style/className/class as they're handled by StyledContainerConfig with better CxJS types.
|
|
70
71
|
type TransformHtmlElementProps<T> = {
|
|
71
|
-
[K in keyof T
|
|
72
|
+
[K in keyof T as K extends "style" | "className" | "class"
|
|
73
|
+
? never
|
|
74
|
+
: K]: K extends "children"
|
|
72
75
|
? ChildNode | ChildNode[]
|
|
73
|
-
: K extends
|
|
74
|
-
?
|
|
75
|
-
:
|
|
76
|
-
?
|
|
77
|
-
:
|
|
78
|
-
? Prop<T[K]> //
|
|
79
|
-
:
|
|
80
|
-
? Prop<T[K]> | string // String literal unions - accept string for JSX compatibility
|
|
81
|
-
: Prop<T[K]>;
|
|
76
|
+
: IsEventHandler<K, T[K]> extends true
|
|
77
|
+
? CxEventHandler<T[K]>
|
|
78
|
+
: string extends T[K]
|
|
79
|
+
? Prop<T[K]> // Plain string props - no change needed
|
|
80
|
+
: NonNullable<T[K]> extends string
|
|
81
|
+
? Prop<T[K]> | string // String literal unions - accept string for JSX compatibility
|
|
82
|
+
: Prop<T[K]>;
|
|
82
83
|
};
|
|
83
84
|
|
|
84
85
|
/** Base HtmlElement configuration - core CxJS properties for extension by widgets */
|