canvasengine 2.0.0-beta.9 → 2.0.0-rc.2
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/DebugRenderer-DkjTAc48.js +1384 -0
- package/dist/DebugRenderer-DkjTAc48.js.map +1 -0
- package/dist/components/Button.d.ts +185 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Canvas.d.ts +17 -0
- package/dist/components/Canvas.d.ts.map +1 -0
- package/dist/components/Container.d.ts +16 -0
- package/dist/components/Container.d.ts.map +1 -0
- package/dist/components/DOMContainer.d.ts +28 -0
- package/dist/components/DOMContainer.d.ts.map +1 -0
- package/dist/components/DOMElement.d.ts +54 -0
- package/dist/components/DOMElement.d.ts.map +1 -0
- package/dist/components/DOMSprite.d.ts +127 -0
- package/dist/components/DOMSprite.d.ts.map +1 -0
- package/dist/components/DisplayObject.d.ts +18 -0
- package/dist/components/DisplayObject.d.ts.map +1 -0
- package/dist/components/FocusContainer.d.ts +129 -0
- package/dist/components/FocusContainer.d.ts.map +1 -0
- package/dist/components/Graphic.d.ts +64 -0
- package/dist/components/Graphic.d.ts.map +1 -0
- package/dist/components/Joystick.d.ts +36 -0
- package/dist/components/Joystick.d.ts.map +1 -0
- package/dist/components/Mesh.d.ts +138 -0
- package/dist/components/Mesh.d.ts.map +1 -0
- package/dist/components/NineSliceSprite.d.ts +16 -0
- package/dist/components/NineSliceSprite.d.ts.map +1 -0
- package/dist/components/ParticleEmitter.d.ts +4 -0
- package/dist/components/ParticleEmitter.d.ts.map +1 -0
- package/dist/components/Scene.d.ts +2 -0
- package/dist/components/Scene.d.ts.map +1 -0
- package/dist/components/Sprite.d.ts +182 -0
- package/dist/components/Sprite.d.ts.map +1 -0
- package/dist/components/Text.d.ts +24 -0
- package/dist/components/Text.d.ts.map +1 -0
- package/dist/components/TilingSprite.d.ts +17 -0
- package/dist/components/TilingSprite.d.ts.map +1 -0
- package/dist/components/Video.d.ts +14 -0
- package/dist/components/Video.d.ts.map +1 -0
- package/dist/components/Viewport.d.ts +52 -0
- package/dist/components/Viewport.d.ts.map +1 -0
- package/dist/components/index.d.ts +20 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/types/DisplayObject.d.ts +122 -0
- package/dist/components/types/DisplayObject.d.ts.map +1 -0
- package/dist/components/types/MouseEvent.d.ts +4 -0
- package/dist/components/types/MouseEvent.d.ts.map +1 -0
- package/dist/components/types/Spritesheet.d.ts +248 -0
- package/dist/components/types/Spritesheet.d.ts.map +1 -0
- package/dist/components/types/index.d.ts +4 -0
- package/dist/components/types/index.d.ts.map +1 -0
- package/dist/directives/Controls.d.ts +112 -0
- package/dist/directives/Controls.d.ts.map +1 -0
- package/dist/directives/ControlsBase.d.ts +199 -0
- package/dist/directives/ControlsBase.d.ts.map +1 -0
- package/dist/directives/Drag.d.ts +69 -0
- package/dist/directives/Drag.d.ts.map +1 -0
- package/dist/directives/Flash.d.ts +116 -0
- package/dist/directives/Flash.d.ts.map +1 -0
- package/dist/directives/FocusNavigation.d.ts +52 -0
- package/dist/directives/FocusNavigation.d.ts.map +1 -0
- package/dist/directives/FogVisibility.d.ts +47 -0
- package/dist/directives/FogVisibility.d.ts.map +1 -0
- package/dist/directives/GamepadControls.d.ts +224 -0
- package/dist/directives/GamepadControls.d.ts.map +1 -0
- package/dist/directives/JoystickControls.d.ts +171 -0
- package/dist/directives/JoystickControls.d.ts.map +1 -0
- package/dist/directives/KeyboardControls.d.ts +219 -0
- package/dist/directives/KeyboardControls.d.ts.map +1 -0
- package/dist/directives/Scheduler.d.ts +36 -0
- package/dist/directives/Scheduler.d.ts.map +1 -0
- package/dist/directives/Shake.d.ts +98 -0
- package/dist/directives/Shake.d.ts.map +1 -0
- package/dist/directives/Sound.d.ts +25 -0
- package/dist/directives/Sound.d.ts.map +1 -0
- package/dist/directives/SpriteEffects.d.ts +70 -0
- package/dist/directives/SpriteEffects.d.ts.map +1 -0
- package/dist/directives/Transition.d.ts +10 -0
- package/dist/directives/Transition.d.ts.map +1 -0
- package/dist/directives/ViewportCull.d.ts +11 -0
- package/dist/directives/ViewportCull.d.ts.map +1 -0
- package/dist/directives/ViewportFollow.d.ts +18 -0
- package/dist/directives/ViewportFollow.d.ts.map +1 -0
- package/dist/directives/index.d.ts +15 -0
- package/dist/directives/index.d.ts.map +1 -0
- package/dist/dist-BOOc43Qm.js +778 -0
- package/dist/dist-BOOc43Qm.js.map +1 -0
- package/dist/engine/FocusManager.d.ts +174 -0
- package/dist/engine/FocusManager.d.ts.map +1 -0
- package/dist/engine/animation.d.ts +72 -0
- package/dist/engine/animation.d.ts.map +1 -0
- package/dist/engine/bootstrap.d.ts +52 -0
- package/dist/engine/bootstrap.d.ts.map +1 -0
- package/dist/engine/directive.d.ts +13 -0
- package/dist/engine/directive.d.ts.map +1 -0
- package/dist/engine/reactive.d.ts +139 -0
- package/dist/engine/reactive.d.ts.map +1 -0
- package/dist/engine/signal.d.ts +73 -0
- package/dist/engine/signal.d.ts.map +1 -0
- package/dist/engine/trigger.d.ts +54 -0
- package/dist/engine/trigger.d.ts.map +1 -0
- package/dist/engine/utils.d.ts +89 -0
- package/dist/engine/utils.d.ts.map +1 -0
- package/dist/hooks/addContext.d.ts +2 -0
- package/dist/hooks/addContext.d.ts.map +1 -0
- package/dist/hooks/useFocus.d.ts +60 -0
- package/dist/hooks/useFocus.d.ts.map +1 -0
- package/dist/hooks/useProps.d.ts +42 -0
- package/dist/hooks/useProps.d.ts.map +1 -0
- package/dist/hooks/useRef.d.ts +4 -0
- package/dist/hooks/useRef.d.ts.map +1 -0
- package/dist/index.d.ts +19 -1135
- package/dist/index.d.ts.map +1 -0
- package/dist/index.global.js +189 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +15038 -3212
- package/dist/index.js.map +1 -1
- package/dist/utils/Ease.d.ts +17 -0
- package/dist/utils/Ease.d.ts.map +1 -0
- package/dist/utils/GlobalAssetLoader.d.ts +141 -0
- package/dist/utils/GlobalAssetLoader.d.ts.map +1 -0
- package/dist/utils/RadialGradient.d.ts +57 -0
- package/dist/utils/RadialGradient.d.ts.map +1 -0
- package/dist/utils/functions.d.ts +2 -0
- package/dist/utils/functions.d.ts.map +1 -0
- package/dist/utils/tabindex.d.ts +16 -0
- package/dist/utils/tabindex.d.ts.map +1 -0
- package/package.json +16 -9
- package/src/components/Button.ts +399 -0
- package/src/components/Canvas.ts +82 -51
- package/src/components/Container.ts +21 -2
- package/src/components/DOMContainer.ts +379 -0
- package/src/components/DOMElement.ts +556 -0
- package/src/components/DOMSprite.ts +1044 -0
- package/src/components/DisplayObject.ts +423 -202
- package/src/components/FocusContainer.ts +368 -0
- package/src/components/Graphic.ts +239 -73
- package/src/components/Joystick.ts +363 -0
- package/src/components/Mesh.ts +222 -0
- package/src/components/NineSliceSprite.ts +4 -1
- package/src/components/ParticleEmitter.ts +12 -8
- package/src/components/Sprite.ts +418 -52
- package/src/components/Text.ts +268 -24
- package/src/components/Viewport.ts +122 -63
- package/src/components/index.ts +9 -2
- package/src/components/types/DisplayObject.ts +57 -5
- package/src/components/types/Spritesheet.ts +0 -118
- package/src/directives/Controls.ts +254 -0
- package/src/directives/ControlsBase.ts +267 -0
- package/src/directives/Drag.ts +357 -52
- package/src/directives/Flash.ts +419 -0
- package/src/directives/FocusNavigation.ts +113 -0
- package/src/directives/FogVisibility.ts +273 -0
- package/src/directives/GamepadControls.ts +537 -0
- package/src/directives/JoystickControls.ts +396 -0
- package/src/directives/KeyboardControls.ts +85 -430
- package/src/directives/Scheduler.ts +21 -5
- package/src/directives/Shake.ts +298 -0
- package/src/directives/Sound.ts +94 -31
- package/src/directives/SpriteEffects.ts +461 -0
- package/src/directives/ViewportFollow.ts +40 -9
- package/src/directives/index.ts +14 -6
- package/src/engine/FocusManager.ts +510 -0
- package/src/engine/animation.ts +137 -19
- package/src/engine/bootstrap.ts +140 -6
- package/src/engine/directive.ts +4 -4
- package/src/engine/reactive.ts +1084 -236
- package/src/engine/signal.ts +241 -47
- package/src/engine/trigger.ts +34 -7
- package/src/engine/utils.ts +19 -3
- package/src/hooks/useFocus.ts +91 -0
- package/src/hooks/useProps.ts +14 -6
- package/src/index.ts +8 -2
- package/src/types/pixi-cull.d.ts +7 -0
- package/src/utils/GlobalAssetLoader.ts +257 -0
- package/src/utils/functions.ts +7 -0
- package/src/utils/tabindex.ts +70 -0
- package/testing/index.ts +35 -4
- package/tsconfig.json +22 -0
- package/vite.config.ts +39 -0
package/src/engine/reactive.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Signal, WritableArraySignal, WritableObjectSignal, isSignal } from "@signe/reactive";
|
|
1
|
+
import { ArrayChange, ObjectChange, Signal, WritableArraySignal, WritableObjectSignal, isComputed, isSignal, signal, computed } from "@signe/reactive";
|
|
2
|
+
import { isAnimatedSignal, AnimatedSignal } from "./animation";
|
|
2
3
|
import {
|
|
3
4
|
Observable,
|
|
4
5
|
Subject,
|
|
@@ -7,7 +8,15 @@ import {
|
|
|
7
8
|
from,
|
|
8
9
|
map,
|
|
9
10
|
of,
|
|
11
|
+
share,
|
|
12
|
+
shareReplay,
|
|
10
13
|
switchMap,
|
|
14
|
+
debounceTime,
|
|
15
|
+
distinctUntilChanged,
|
|
16
|
+
bufferTime,
|
|
17
|
+
filter,
|
|
18
|
+
throttleTime,
|
|
19
|
+
combineLatest,
|
|
11
20
|
} from "rxjs";
|
|
12
21
|
import { ComponentInstance } from "../components/DisplayObject";
|
|
13
22
|
import { Directive, applyDirective } from "./directive";
|
|
@@ -17,25 +26,6 @@ export interface Props {
|
|
|
17
26
|
[key: string]: any;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
|
-
export type ArrayChange<T> = {
|
|
21
|
-
type: "add" | "remove" | "update" | "init" | "reset";
|
|
22
|
-
index?: number;
|
|
23
|
-
items: T[];
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type ObjectChange<T> = {
|
|
27
|
-
type: "add" | "remove" | "update" | "init" | "reset";
|
|
28
|
-
key?: string;
|
|
29
|
-
value?: T;
|
|
30
|
-
items: T[];
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
type ElementObservable<T> = Observable<
|
|
34
|
-
(ArrayChange<T> | ObjectChange<T>) & {
|
|
35
|
-
value: Element | Element[];
|
|
36
|
-
}
|
|
37
|
-
>;
|
|
38
|
-
|
|
39
29
|
type NestedSignalObjects = {
|
|
40
30
|
[Key in string]: NestedSignalObjects | Signal<any>;
|
|
41
31
|
};
|
|
@@ -58,16 +48,22 @@ export interface Element<T = ComponentInstance> {
|
|
|
58
48
|
};
|
|
59
49
|
destroy: () => void;
|
|
60
50
|
allElements: Subject<void>;
|
|
51
|
+
isFrozen: boolean;
|
|
61
52
|
}
|
|
62
53
|
|
|
63
54
|
type FlowResult = {
|
|
64
55
|
elements: Element[];
|
|
65
56
|
prev?: Element;
|
|
66
57
|
fullElements?: Element[];
|
|
58
|
+
reorder?: boolean;
|
|
67
59
|
};
|
|
68
60
|
|
|
69
61
|
type FlowObservable = Observable<FlowResult>;
|
|
70
62
|
|
|
63
|
+
export interface LoopOptions<T> {
|
|
64
|
+
track?: (item: T, index: number | string) => string | number;
|
|
65
|
+
}
|
|
66
|
+
|
|
71
67
|
const components: { [key: string]: any } = {};
|
|
72
68
|
|
|
73
69
|
export const isElement = (value: any): value is Element => {
|
|
@@ -90,11 +86,220 @@ export const isPrimitive = (value) => {
|
|
|
90
86
|
);
|
|
91
87
|
};
|
|
92
88
|
|
|
89
|
+
const DOM_ROUTING_MAP: Record<string, string> = {
|
|
90
|
+
Sprite: "DOMSprite",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const DOM_ALLOWED_TAGS = new Set(["DOMContainer", "DOMElement", "DOMSprite"]);
|
|
94
|
+
const DOM_UNSUPPORTED_TAGS = new Set([
|
|
95
|
+
"Canvas",
|
|
96
|
+
"Container",
|
|
97
|
+
"Graphics",
|
|
98
|
+
"Rect",
|
|
99
|
+
"Circle",
|
|
100
|
+
"Ellipse",
|
|
101
|
+
"Triangle",
|
|
102
|
+
"Svg",
|
|
103
|
+
"Mesh",
|
|
104
|
+
"Scene",
|
|
105
|
+
"ParticlesEmitter",
|
|
106
|
+
"Sprite",
|
|
107
|
+
"Video",
|
|
108
|
+
"Text",
|
|
109
|
+
"TilingSprite",
|
|
110
|
+
"Viewport",
|
|
111
|
+
"NineSliceSprite",
|
|
112
|
+
"Button",
|
|
113
|
+
"Joystick",
|
|
114
|
+
"FocusContainer",
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const hasDomAncestor = (element: Element | null): boolean => {
|
|
118
|
+
let current = element;
|
|
119
|
+
while (current) {
|
|
120
|
+
if (current.tag === "DOMContainer" || current.tag === "DOMElement") {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
current = current.parent;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const cleanupElementForRouting = (element: Element) => {
|
|
129
|
+
element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
130
|
+
element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
131
|
+
element.effectUnmounts?.forEach((fn) => fn?.());
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const routeDomComponent = (parent: Element, child: Element): Element => {
|
|
135
|
+
if (!hasDomAncestor(parent)) {
|
|
136
|
+
return child;
|
|
137
|
+
}
|
|
138
|
+
if (DOM_ALLOWED_TAGS.has(child.tag)) {
|
|
139
|
+
return child;
|
|
140
|
+
}
|
|
141
|
+
const routedTag = DOM_ROUTING_MAP[child.tag];
|
|
142
|
+
if (routedTag) {
|
|
143
|
+
cleanupElementForRouting(child);
|
|
144
|
+
const routedProps = child.propObservables ?? child.props;
|
|
145
|
+
return createComponent(routedTag, routedProps);
|
|
146
|
+
}
|
|
147
|
+
if (DOM_UNSUPPORTED_TAGS.has(child.tag)) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Component ${child.tag} is not implemented for DOMContainer context yet. Only Sprite is supported.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return child;
|
|
153
|
+
};
|
|
154
|
+
|
|
93
155
|
export function registerComponent(name, component) {
|
|
94
156
|
components[name] = component;
|
|
95
157
|
}
|
|
96
158
|
|
|
97
|
-
|
|
159
|
+
// Track if components have been registered to avoid duplicate imports
|
|
160
|
+
let componentsRegistered = false;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Registers all default CanvasEngine components.
|
|
164
|
+
*
|
|
165
|
+
* This function imports and registers all core components that are available by default.
|
|
166
|
+
* It's called automatically by bootstrapCanvas() if no custom component configuration is provided.
|
|
167
|
+
*
|
|
168
|
+
* Components register themselves when their modules are imported, so this function ensures
|
|
169
|
+
* all component modules are loaded. Since components call registerComponent() at module load time,
|
|
170
|
+
* importing them will automatically register them synchronously.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* // Register all default components manually
|
|
175
|
+
* registerAllComponents();
|
|
176
|
+
*
|
|
177
|
+
* // Now you can use any component
|
|
178
|
+
* const sprite = createComponent('Sprite', { image: 'hero.png' });
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export function registerAllComponents() {
|
|
182
|
+
if (componentsRegistered) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Components are registered when their modules are imported
|
|
187
|
+
// Since bootstrap.ts imports all components, they should already be registered
|
|
188
|
+
// when bootstrapCanvas() is called. This function just marks that registration
|
|
189
|
+
// has been attempted. If components aren't registered yet, they will be when
|
|
190
|
+
// bootstrap.ts imports them (which happens before bootstrapCanvas() is called).
|
|
191
|
+
componentsRegistered = true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Checks if all dependencies are ready (not undefined).
|
|
196
|
+
* Handles signals synchronously and promises asynchronously.
|
|
197
|
+
* For reactive signals, sets up subscriptions to mount when all become ready.
|
|
198
|
+
*
|
|
199
|
+
* @param deps - Array of signals, promises, or direct values
|
|
200
|
+
* @returns Promise<boolean> - true if all dependencies are ready
|
|
201
|
+
*/
|
|
202
|
+
export async function checkDependencies(
|
|
203
|
+
deps: any[]
|
|
204
|
+
): Promise<boolean> {
|
|
205
|
+
const values = await Promise.all(
|
|
206
|
+
deps.map(async (dep) => {
|
|
207
|
+
if (isSignal(dep)) {
|
|
208
|
+
return dep(); // Read current signal value
|
|
209
|
+
} else if (isPromise(dep)) {
|
|
210
|
+
return await dep; // Await promise resolution
|
|
211
|
+
}
|
|
212
|
+
return dep; // Direct value
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
return values.every((v) => v !== undefined);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function waitForDependencies(deps: any[]): Promise<void> {
|
|
219
|
+
return new Promise(async (resolve) => {
|
|
220
|
+
const ready = await checkDependencies(deps);
|
|
221
|
+
if (ready) {
|
|
222
|
+
resolve();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const signalDeps = deps.filter((dep) => isSignal(dep));
|
|
227
|
+
if (signalDeps.length === 0) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const signalObservables = signalDeps.map((sig) => sig.observable);
|
|
232
|
+
const subscription = combineLatest(signalObservables).subscribe(async () => {
|
|
233
|
+
const allReady = await checkDependencies(deps);
|
|
234
|
+
if (allReady) {
|
|
235
|
+
subscription.unsubscribe();
|
|
236
|
+
resolve();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Checks if an element is currently frozen.
|
|
244
|
+
* An element is frozen when the `freeze` prop is set to `true` (either as a boolean or Signal<boolean>),
|
|
245
|
+
* or when any of its parent elements are frozen (recursive freeze propagation).
|
|
246
|
+
*
|
|
247
|
+
* @param element - The element to check
|
|
248
|
+
* @returns `true` if the element is frozen, `false` otherwise
|
|
249
|
+
*/
|
|
250
|
+
export function isElementFrozen(element: Element): boolean {
|
|
251
|
+
if (!element) return false;
|
|
252
|
+
|
|
253
|
+
// Check if this element itself is frozen
|
|
254
|
+
const freezeProp = element.propObservables?.freeze ?? element.props?.freeze;
|
|
255
|
+
|
|
256
|
+
if (freezeProp !== undefined && freezeProp !== null) {
|
|
257
|
+
// Handle Signal<boolean>
|
|
258
|
+
if (isSignal(freezeProp)) {
|
|
259
|
+
if (freezeProp() === true) {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
} else if (freezeProp === true) {
|
|
263
|
+
// Handle direct boolean
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check if any parent is frozen (recursive check)
|
|
269
|
+
if (element.parent) {
|
|
270
|
+
return isElementFrozen(element.parent);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Pauses or resumes all animatedSignals in an element based on freeze state.
|
|
278
|
+
*
|
|
279
|
+
* @param element - The element containing animatedSignals
|
|
280
|
+
* @param shouldPause - Whether to pause (true) or resume (false) animations
|
|
281
|
+
*/
|
|
282
|
+
function handleAnimatedSignalsFreeze(element: Element, shouldPause: boolean) {
|
|
283
|
+
if (!element.propObservables) return;
|
|
284
|
+
|
|
285
|
+
const processValue = (value: any) => {
|
|
286
|
+
if (isSignal(value) && isAnimatedSignal(value as any)) {
|
|
287
|
+
const animatedSig = value as unknown as AnimatedSignal<any>;
|
|
288
|
+
if (shouldPause) {
|
|
289
|
+
animatedSig.pause();
|
|
290
|
+
} else {
|
|
291
|
+
animatedSig.resume();
|
|
292
|
+
}
|
|
293
|
+
} else if (isObject(value) && !isElement(value)) {
|
|
294
|
+
// Recursively process nested objects
|
|
295
|
+
Object.values(value).forEach(processValue);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
Object.values(element.propObservables).forEach(processValue);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function destroyElement(element: Element | Element[]) {
|
|
98
303
|
if (Array.isArray(element)) {
|
|
99
304
|
element.forEach((e) => destroyElement(e));
|
|
100
305
|
return;
|
|
@@ -102,13 +307,34 @@ function destroyElement(element: Element | Element[]) {
|
|
|
102
307
|
if (!element) {
|
|
103
308
|
return;
|
|
104
309
|
}
|
|
105
|
-
element.
|
|
106
|
-
|
|
310
|
+
if (element.props?.children) {
|
|
311
|
+
for (let child of element.props.children) {
|
|
312
|
+
destroyElement(child)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
107
315
|
for (let name in element.directives) {
|
|
108
|
-
element.directives[name].onDestroy?.();
|
|
316
|
+
element.directives[name].onDestroy?.(element);
|
|
317
|
+
}
|
|
318
|
+
if (element.componentInstance && element.componentInstance.onDestroy) {
|
|
319
|
+
element.componentInstance.onDestroy(element.parent as any, () => {
|
|
320
|
+
element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
321
|
+
element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
322
|
+
element.effectUnmounts?.forEach((fn) => {
|
|
323
|
+
if (isPromise(fn)) {
|
|
324
|
+
(fn as unknown as Promise<any>).then((retFn) => {
|
|
325
|
+
retFn?.();
|
|
326
|
+
});
|
|
327
|
+
} else {
|
|
328
|
+
fn?.();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
} else {
|
|
333
|
+
// If componentInstance is undefined or doesn't have onDestroy, still clean up subscriptions
|
|
334
|
+
element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
335
|
+
element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
336
|
+
element.effectUnmounts?.forEach((fn) => fn?.());
|
|
109
337
|
}
|
|
110
|
-
element.componentInstance.onDestroy?.(element.parent as any);
|
|
111
|
-
element.effectUnmounts.forEach((fn) => fn?.());
|
|
112
338
|
}
|
|
113
339
|
|
|
114
340
|
/**
|
|
@@ -141,6 +367,7 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
141
367
|
destroyElement(this);
|
|
142
368
|
},
|
|
143
369
|
allElements: new Subject(),
|
|
370
|
+
isFrozen: false,
|
|
144
371
|
};
|
|
145
372
|
|
|
146
373
|
// Iterate over each property in the props object
|
|
@@ -159,27 +386,78 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
159
386
|
const _value = value as Signal<any>;
|
|
160
387
|
if ("dependencies" in _value && _value.dependencies.size == 0) {
|
|
161
388
|
_set(path, key, _value());
|
|
389
|
+
// Handle freeze prop initialization
|
|
390
|
+
if (key === "freeze") {
|
|
391
|
+
element.isFrozen = _value() === true;
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Handle freeze prop as signal
|
|
397
|
+
if (key === "freeze") {
|
|
398
|
+
element.isFrozen = _value() === true;
|
|
399
|
+
|
|
400
|
+
// Pause/resume animatedSignals based on initial freeze state
|
|
401
|
+
handleAnimatedSignalsFreeze(element, element.isFrozen);
|
|
402
|
+
|
|
403
|
+
element.propSubscriptions.push(
|
|
404
|
+
_value.observable.subscribe((freezeValue) => {
|
|
405
|
+
const wasFrozen = element.isFrozen;
|
|
406
|
+
element.isFrozen = freezeValue === true;
|
|
407
|
+
|
|
408
|
+
// Handle animatedSignal pause/resume when freeze state changes
|
|
409
|
+
if (wasFrozen !== element.isFrozen) {
|
|
410
|
+
handleAnimatedSignalsFreeze(element, element.isFrozen);
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
);
|
|
162
414
|
return;
|
|
163
415
|
}
|
|
416
|
+
|
|
164
417
|
element.propSubscriptions.push(
|
|
165
418
|
_value.observable.subscribe((value) => {
|
|
419
|
+
// Block updates if element is frozen
|
|
420
|
+
if (isElementFrozen(element)) {
|
|
421
|
+
// Pause animatedSignal if it's an animated signal
|
|
422
|
+
if (isAnimatedSignal(_value as any)) {
|
|
423
|
+
(_value as unknown as AnimatedSignal<any>).pause();
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Resume animatedSignal if it was paused
|
|
429
|
+
if (isAnimatedSignal(_value as any)) {
|
|
430
|
+
(_value as unknown as AnimatedSignal<any>).resume();
|
|
431
|
+
}
|
|
432
|
+
|
|
166
433
|
_set(path, key, value);
|
|
167
434
|
if (element.directives[key]) {
|
|
168
|
-
element.directives[key].onUpdate?.(value);
|
|
435
|
+
element.directives[key].onUpdate?.(value, element);
|
|
169
436
|
}
|
|
170
437
|
if (key == "tick") {
|
|
438
|
+
// Block tick updates if element is frozen
|
|
439
|
+
if (isElementFrozen(element)) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
171
442
|
return
|
|
172
443
|
}
|
|
173
444
|
instance.onUpdate?.(
|
|
174
445
|
path == ""
|
|
175
446
|
? {
|
|
176
|
-
|
|
177
|
-
|
|
447
|
+
[key]: value,
|
|
448
|
+
}
|
|
178
449
|
: set({}, path + "." + key, value)
|
|
179
450
|
);
|
|
180
451
|
})
|
|
181
452
|
);
|
|
182
453
|
} else {
|
|
454
|
+
// Handle freeze prop as direct boolean
|
|
455
|
+
if (key === "freeze") {
|
|
456
|
+
element.isFrozen = value === true;
|
|
457
|
+
|
|
458
|
+
// Pause/resume animatedSignals based on freeze state
|
|
459
|
+
handleAnimatedSignalsFreeze(element, element.isFrozen);
|
|
460
|
+
}
|
|
183
461
|
if (isObject(value) && key != "context" && !isElement(value)) {
|
|
184
462
|
recursiveProps(value, (path ? path + "." : "") + key);
|
|
185
463
|
} else {
|
|
@@ -209,9 +487,69 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
209
487
|
}
|
|
210
488
|
}
|
|
211
489
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
490
|
+
/**
|
|
491
|
+
* Checks if all dependencies are ready (not undefined).
|
|
492
|
+
* Handles signals synchronously and promises asynchronously.
|
|
493
|
+
* For reactive signals, sets up subscriptions to mount when all become ready.
|
|
494
|
+
*
|
|
495
|
+
* @param deps - Array of signals, promises, or direct values
|
|
496
|
+
* @returns Promise<boolean> - true if all dependencies are ready
|
|
497
|
+
*/
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Sets up subscriptions to reactive signal dependencies.
|
|
502
|
+
* When all signals become defined, mounts the component.
|
|
503
|
+
*/
|
|
504
|
+
/**
|
|
505
|
+
* Sets up subscriptions to reactive signal dependencies.
|
|
506
|
+
* When all signals become defined, mounts the component.
|
|
507
|
+
*/
|
|
508
|
+
function setupDependencySubscriptions(
|
|
509
|
+
parent: Element,
|
|
510
|
+
element: Element,
|
|
511
|
+
deps: any[],
|
|
512
|
+
index?: number
|
|
513
|
+
) {
|
|
514
|
+
const signalDeps = deps.filter((dep) => isSignal(dep));
|
|
515
|
+
const promiseDeps = deps.filter((dep) => isPromise(dep));
|
|
516
|
+
|
|
517
|
+
if (signalDeps.length === 0) {
|
|
518
|
+
// No reactive signals, nothing to subscribe to
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Create observables from signals
|
|
523
|
+
const signalObservables = signalDeps.map((sig) => sig.observable);
|
|
524
|
+
|
|
525
|
+
// Combine all signal observables
|
|
526
|
+
const subscription = combineLatest(signalObservables).subscribe(
|
|
527
|
+
async () => {
|
|
528
|
+
// Check if all dependencies are now ready
|
|
529
|
+
const allReady = await checkDependencies(deps);
|
|
530
|
+
if (allReady) {
|
|
531
|
+
// Unsubscribe - we only need to mount once
|
|
532
|
+
subscription.unsubscribe();
|
|
533
|
+
// Remove from subscriptions
|
|
534
|
+
const idx = element.propSubscriptions.indexOf(subscription);
|
|
535
|
+
if (idx > -1) {
|
|
536
|
+
element.propSubscriptions.splice(idx, 1);
|
|
537
|
+
}
|
|
538
|
+
// Now mount the component
|
|
539
|
+
performMount(parent, element, index);
|
|
540
|
+
propagateContext(element);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
// Store subscription for cleanup
|
|
546
|
+
element.propSubscriptions.push(subscription);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Performs the actual mounting of the component.
|
|
551
|
+
*/
|
|
552
|
+
function performMount(parent: Element, element: Element, index?: number) {
|
|
215
553
|
element.componentInstance.onMount?.(element, index);
|
|
216
554
|
for (let name in element.directives) {
|
|
217
555
|
element.directives[name].onMount?.(element);
|
|
@@ -219,6 +557,34 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
219
557
|
element.effectMounts.forEach((fn: any) => {
|
|
220
558
|
element.effectUnmounts.push(fn(element));
|
|
221
559
|
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function onMount(parent: Element, element: Element, index?: number) {
|
|
563
|
+
let actualParent = parent;
|
|
564
|
+
while (actualParent?.tag === 'fragment') {
|
|
565
|
+
actualParent = actualParent.parent;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
element.props.context = actualParent.props.context;
|
|
569
|
+
element.parent = actualParent;
|
|
570
|
+
|
|
571
|
+
// Inherit freeze state from parent if element doesn't have its own freeze prop
|
|
572
|
+
if (!element.propObservables?.freeze && !element.props?.freeze && isElementFrozen(actualParent)) {
|
|
573
|
+
element.isFrozen = true;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Check dependencies before mounting
|
|
577
|
+
if (element.props.dependencies && Array.isArray(element.props.dependencies)) {
|
|
578
|
+
const deps = element.props.dependencies;
|
|
579
|
+
const ready = await checkDependencies(deps);
|
|
580
|
+
if (!ready) {
|
|
581
|
+
// Set up subscriptions for reactive signals to trigger mount later
|
|
582
|
+
setupDependencySubscriptions(actualParent, element, deps, index);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
performMount(actualParent, element, index);
|
|
222
588
|
};
|
|
223
589
|
|
|
224
590
|
async function propagateContext(element) {
|
|
@@ -229,71 +595,280 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
229
595
|
}
|
|
230
596
|
else {
|
|
231
597
|
await new Promise((resolve) => {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
598
|
+
let lastElement = null
|
|
599
|
+
element.propSubscriptions.push(element.propObservables.attach.observable.subscribe(async (args) => {
|
|
600
|
+
const value = args?.value ?? args
|
|
601
|
+
if (!value) {
|
|
602
|
+
throw new Error(`attach in ${element.tag} is undefined or null, add a component`)
|
|
603
|
+
}
|
|
604
|
+
if (lastElement) {
|
|
605
|
+
destroyElement(lastElement)
|
|
606
|
+
}
|
|
607
|
+
lastElement = value
|
|
608
|
+
await createElement(element, value)
|
|
609
|
+
resolve(undefined)
|
|
610
|
+
}))
|
|
245
611
|
})
|
|
246
612
|
}
|
|
247
613
|
}
|
|
248
614
|
if (!element.props.children) {
|
|
249
615
|
return;
|
|
250
616
|
}
|
|
251
|
-
for (let
|
|
617
|
+
for (let i = 0; i < element.props.children.length; i++) {
|
|
618
|
+
const child = element.props.children[i];
|
|
252
619
|
if (!child) continue;
|
|
253
|
-
await createElement(element, child)
|
|
620
|
+
await createElement(element, child, i)
|
|
254
621
|
}
|
|
255
622
|
};
|
|
256
623
|
|
|
257
|
-
|
|
624
|
+
/**
|
|
625
|
+
* Creates and mounts a child element to a parent element.
|
|
626
|
+
* Handles different types of children: Elements, Promises resolving to Elements, and Observables.
|
|
627
|
+
*
|
|
628
|
+
* @description This function is designed to handle reactive child components that can be:
|
|
629
|
+
* - Direct Element instances
|
|
630
|
+
* - Promises that resolve to Elements (for async components)
|
|
631
|
+
* - Observables that emit Elements, arrays of Elements, or FlowObservable results
|
|
632
|
+
* - Nested observables within arrays or FlowObservable results (handled recursively)
|
|
633
|
+
*
|
|
634
|
+
* For Observables, it subscribes to the stream and automatically mounts/unmounts elements
|
|
635
|
+
* as they are emitted. The function handles nested observables recursively, ensuring that
|
|
636
|
+
* observables within arrays or FlowObservable results are also properly subscribed to.
|
|
637
|
+
* All subscriptions are stored in the parent's effectSubscriptions for automatic cleanup.
|
|
638
|
+
*
|
|
639
|
+
* @param {Element} parent - The parent element to mount the child to
|
|
640
|
+
* @param {Element | Observable<any> | Promise<Element>} child - The child to create and mount
|
|
641
|
+
*
|
|
642
|
+
* @example
|
|
643
|
+
* ```typescript
|
|
644
|
+
* // Direct element
|
|
645
|
+
* await createElement(parent, childElement);
|
|
646
|
+
*
|
|
647
|
+
* // Observable of elements (from cond, loop, etc.)
|
|
648
|
+
* await createElement(parent, cond(signal(visible), () => h(Container)));
|
|
649
|
+
*
|
|
650
|
+
* // Observable that emits arrays containing other observables
|
|
651
|
+
* await createElement(parent, observableOfObservables);
|
|
652
|
+
*
|
|
653
|
+
* // Promise resolving to element
|
|
654
|
+
* await createElement(parent, import('./MyComponent').then(mod => h(mod.default)));
|
|
655
|
+
* ```
|
|
656
|
+
*/
|
|
657
|
+
async function createElement(parent: Element, child: Element | Observable<any> | Promise<Element>, childOrder?: number) {
|
|
258
658
|
if (isPromise(child)) {
|
|
259
659
|
child = await child;
|
|
260
660
|
}
|
|
661
|
+
|
|
662
|
+
const childGroups = ((parent as any).__childGroups ??= []);
|
|
663
|
+
const resolvedOrder =
|
|
664
|
+
childOrder ??
|
|
665
|
+
(parent.props.children ? parent.props.children.indexOf(child as any) : -1);
|
|
666
|
+
const childGroup = {
|
|
667
|
+
order: resolvedOrder >= 0 ? resolvedOrder : childGroups.length,
|
|
668
|
+
mounted: new Map<any, Element>(),
|
|
669
|
+
};
|
|
670
|
+
childGroups.push(childGroup);
|
|
671
|
+
|
|
672
|
+
const getMountedIndex = (element?: Element): number | undefined => {
|
|
673
|
+
const children = (parent.componentInstance as any)?.children;
|
|
674
|
+
if (!element || !children) return;
|
|
675
|
+
const index = children.indexOf(element.componentInstance);
|
|
676
|
+
return index >= 0 ? index : undefined;
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const getNextGroupIndex = (): number | undefined => {
|
|
680
|
+
const nextGroups = childGroups
|
|
681
|
+
.filter((group) => group !== childGroup && group.order > childGroup.order)
|
|
682
|
+
.sort((a, b) => a.order - b.order);
|
|
683
|
+
|
|
684
|
+
for (const group of nextGroups) {
|
|
685
|
+
for (const mounted of group.mounted.values()) {
|
|
686
|
+
const index = getMountedIndex(mounted);
|
|
687
|
+
if (index !== undefined) return index;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
const getInsertIndex = (
|
|
693
|
+
sourceIndex: number,
|
|
694
|
+
orderedSources: any[]
|
|
695
|
+
): number | undefined => {
|
|
696
|
+
for (let i = sourceIndex + 1; i < orderedSources.length; i++) {
|
|
697
|
+
const index = getMountedIndex(childGroup.mounted.get(orderedSources[i]));
|
|
698
|
+
if (index !== undefined) return index;
|
|
699
|
+
}
|
|
700
|
+
return getNextGroupIndex();
|
|
701
|
+
};
|
|
702
|
+
|
|
261
703
|
if (child instanceof Observable) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
704
|
+
const mountedFlowElements = childGroup.mounted;
|
|
705
|
+
const flowEffectSubscriptions = ((child as any).effectSubscriptions ?? []) as Subscription[];
|
|
706
|
+
const flowEffectMounts = ((child as any).effectMounts ?? []) as Array<(element?: Element) => any>;
|
|
707
|
+
|
|
708
|
+
const applyFlowEffects = (element: Element) => {
|
|
709
|
+
if (!flowEffectMounts.length) {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
element.effectMounts = [
|
|
714
|
+
...flowEffectMounts,
|
|
715
|
+
...(element.effectMounts ?? []),
|
|
716
|
+
];
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const createFragmentOwner = (): Element => ({
|
|
720
|
+
tag: 'fragment',
|
|
721
|
+
props: { children: [] },
|
|
722
|
+
componentInstance: {} as any,
|
|
723
|
+
propSubscriptions: [],
|
|
724
|
+
effectSubscriptions: [],
|
|
725
|
+
effectMounts: [],
|
|
726
|
+
effectUnmounts: [],
|
|
727
|
+
propObservables: {},
|
|
728
|
+
parent,
|
|
729
|
+
directives: {},
|
|
730
|
+
destroy() { destroyElement(this) },
|
|
731
|
+
allElements: new Subject(),
|
|
732
|
+
isFrozen: false
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
const mountFlowElement = (
|
|
736
|
+
element: Element,
|
|
737
|
+
sourceIndex: number,
|
|
738
|
+
orderedSources: any[],
|
|
739
|
+
shouldReorder = false
|
|
740
|
+
) => {
|
|
741
|
+
const mounted = mountedFlowElements.get(element);
|
|
742
|
+
if (mounted) {
|
|
743
|
+
if (shouldReorder) {
|
|
744
|
+
const insertIndex = getInsertIndex(sourceIndex, orderedSources);
|
|
745
|
+
const parentInstance = mounted.parent?.componentInstance as any;
|
|
746
|
+
const childInstance = mounted.componentInstance as any;
|
|
747
|
+
if (
|
|
748
|
+
insertIndex !== undefined &&
|
|
749
|
+
parentInstance &&
|
|
750
|
+
typeof parentInstance.addChildAt === "function" &&
|
|
751
|
+
parentInstance.children?.includes(childInstance)
|
|
752
|
+
) {
|
|
753
|
+
parentInstance.addChildAt(childInstance, insertIndex);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const routed = routeDomComponent(parent, element);
|
|
760
|
+
applyFlowEffects(routed);
|
|
761
|
+
mountedFlowElements.set(element, routed);
|
|
762
|
+
onMount(parent, routed, getInsertIndex(sourceIndex, orderedSources));
|
|
763
|
+
propagateContext(routed);
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const syncFlowElements = (nextElements: Set<any>) => {
|
|
767
|
+
mountedFlowElements.forEach((mounted, source) => {
|
|
768
|
+
if (nextElements.has(source)) {
|
|
278
769
|
return;
|
|
279
770
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
771
|
+
mountedFlowElements.delete(source);
|
|
772
|
+
if (mounted !== source) {
|
|
773
|
+
destroyElement(mounted);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
const processFlowComponent = (
|
|
779
|
+
component: any,
|
|
780
|
+
nextElements: Set<any>,
|
|
781
|
+
index: number,
|
|
782
|
+
orderedSources: any[],
|
|
783
|
+
shouldReorder = false
|
|
784
|
+
) => {
|
|
785
|
+
if (component instanceof Observable) {
|
|
786
|
+
nextElements.add(component);
|
|
787
|
+
if (!mountedFlowElements.has(component)) {
|
|
788
|
+
const owner = createFragmentOwner();
|
|
789
|
+
mountedFlowElements.set(component, owner);
|
|
790
|
+
void createElement(owner, component);
|
|
791
|
+
}
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (Array.isArray(component)) {
|
|
795
|
+
component.forEach((comp) =>
|
|
796
|
+
processFlowComponent(comp, nextElements, index, orderedSources, shouldReorder)
|
|
797
|
+
);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (!isElement(component)) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
nextElements.add(component);
|
|
805
|
+
mountFlowElement(component, index, orderedSources, shouldReorder);
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// Subscribe to the observable and handle the emitted values
|
|
809
|
+
const subscription = child.subscribe(
|
|
810
|
+
(value: any) => {
|
|
811
|
+
// Handle different types of observable emissions
|
|
812
|
+
if (value && typeof value === 'object' && 'elements' in value) {
|
|
813
|
+
// Handle FlowObservable result (from loop, cond, etc.)
|
|
814
|
+
const {
|
|
815
|
+
elements: comp,
|
|
816
|
+
prev,
|
|
817
|
+
reorder,
|
|
818
|
+
}: {
|
|
819
|
+
elements: Element[];
|
|
820
|
+
prev?: Element;
|
|
821
|
+
reorder?: boolean;
|
|
822
|
+
} = value;
|
|
823
|
+
|
|
824
|
+
const components = comp.filter((c) => c !== null);
|
|
825
|
+
const nextElements = new Set<any>();
|
|
826
|
+
if (prev) {
|
|
827
|
+
components.forEach((c) => {
|
|
828
|
+
const index = parent.props.children.indexOf(prev.props.key);
|
|
829
|
+
processFlowComponent(c, nextElements, index + 1, components);
|
|
288
830
|
});
|
|
831
|
+
syncFlowElements(nextElements);
|
|
832
|
+
return;
|
|
289
833
|
}
|
|
290
|
-
|
|
291
|
-
|
|
834
|
+
components.forEach((component, index) => {
|
|
835
|
+
processFlowComponent(component, nextElements, index, components, reorder);
|
|
836
|
+
});
|
|
837
|
+
syncFlowElements(nextElements);
|
|
838
|
+
} else if (isElement(value)) {
|
|
839
|
+
// Handle direct Element emission
|
|
840
|
+
const routed = routeDomComponent(parent, value);
|
|
841
|
+
applyFlowEffects(routed);
|
|
842
|
+
childGroup.mounted.set(value, routed);
|
|
843
|
+
onMount(parent, routed, getInsertIndex(0, [value]));
|
|
844
|
+
propagateContext(routed);
|
|
845
|
+
} else if (Array.isArray(value)) {
|
|
846
|
+
// Handle array of elements (which can also be observables)
|
|
847
|
+
const nextElements = new Set<any>();
|
|
848
|
+
value.forEach((element, index) => {
|
|
849
|
+
processFlowComponent(element, nextElements, index, value);
|
|
850
|
+
});
|
|
851
|
+
syncFlowElements(nextElements);
|
|
852
|
+
}
|
|
853
|
+
elementsListen.next(undefined);
|
|
292
854
|
}
|
|
293
855
|
);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
856
|
+
|
|
857
|
+
subscription.add(() => {
|
|
858
|
+
mountedFlowElements.forEach((mounted) => {
|
|
859
|
+
destroyElement(mounted);
|
|
860
|
+
});
|
|
861
|
+
mountedFlowElements.clear();
|
|
862
|
+
flowEffectSubscriptions.forEach((sub) => sub.unsubscribe());
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// Store subscription for cleanup
|
|
866
|
+
parent.effectSubscriptions.push(subscription);
|
|
867
|
+
} else if (isElement(child)) {
|
|
868
|
+
const routed = routeDomComponent(parent, child);
|
|
869
|
+
childGroup.mounted.set(child, routed);
|
|
870
|
+
onMount(parent, routed, getInsertIndex(0, [child]));
|
|
871
|
+
await propagateContext(routed);
|
|
297
872
|
}
|
|
298
873
|
}
|
|
299
874
|
|
|
@@ -309,207 +884,480 @@ export function createComponent(tag: string, props?: Props): Element {
|
|
|
309
884
|
* @returns {Observable} An observable that emits the list of created child elements.
|
|
310
885
|
*/
|
|
311
886
|
export function loop<T>(
|
|
312
|
-
itemsSubject:
|
|
313
|
-
createElementFn: (item: T, index: number | string) => Element | null
|
|
887
|
+
itemsSubject: any,
|
|
888
|
+
createElementFn: (item: T, index: number | string) => Element | null,
|
|
889
|
+
options: LoopOptions<T> = {}
|
|
314
890
|
): FlowObservable {
|
|
315
|
-
let elements: Element[] = [];
|
|
316
891
|
|
|
317
|
-
|
|
892
|
+
if (isComputed(itemsSubject) && itemsSubject.dependencies.size == 0) {
|
|
893
|
+
itemsSubject = signal(itemsSubject());
|
|
894
|
+
}
|
|
895
|
+
else if (!isSignal(itemsSubject)) {
|
|
896
|
+
itemsSubject = signal(itemsSubject);
|
|
897
|
+
}
|
|
318
898
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
} else {
|
|
326
|
-
elements.push(element);
|
|
327
|
-
}
|
|
328
|
-
return element;
|
|
329
|
-
});
|
|
330
|
-
};
|
|
899
|
+
return defer(() => {
|
|
900
|
+
let elements: Element[] = [];
|
|
901
|
+
let elementMap = new Map<string | number, Element>();
|
|
902
|
+
let isFirstSubscription = true;
|
|
903
|
+
const getTrackKey = (item: T, index: number | string) =>
|
|
904
|
+
options.track ? options.track(item, index) : index;
|
|
331
905
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return
|
|
335
|
-
items: (itemsSubject as any)._subject.items as T[],
|
|
336
|
-
keys: undefined as string[] | undefined
|
|
337
|
-
};
|
|
338
|
-
} else {
|
|
339
|
-
const entries = Object.entries((itemsSubject as any)._subject.value.value) as [string, T][];
|
|
906
|
+
const ensureElement = (itemResult: any): Element | null => {
|
|
907
|
+
if (!itemResult) return null;
|
|
908
|
+
if (isElement(itemResult)) return itemResult;
|
|
340
909
|
return {
|
|
341
|
-
|
|
342
|
-
|
|
910
|
+
tag: 'fragment',
|
|
911
|
+
props: { children: Array.isArray(itemResult) ? itemResult : [itemResult] },
|
|
912
|
+
componentInstance: {} as any,
|
|
913
|
+
propSubscriptions: [],
|
|
914
|
+
effectSubscriptions: [],
|
|
915
|
+
effectMounts: [],
|
|
916
|
+
effectUnmounts: [],
|
|
917
|
+
propObservables: {},
|
|
918
|
+
parent: null,
|
|
919
|
+
directives: {},
|
|
920
|
+
destroy() { destroyElement(this) },
|
|
921
|
+
allElements: new Subject(),
|
|
922
|
+
isFrozen: false
|
|
343
923
|
};
|
|
344
924
|
}
|
|
345
|
-
};
|
|
346
925
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
926
|
+
const isArraySignal = (signal: any): signal is WritableArraySignal<T[]> =>
|
|
927
|
+
Array.isArray(signal());
|
|
928
|
+
|
|
929
|
+
const cleanupUntrackedElement = (element: Element | null) => {
|
|
930
|
+
if (!element) return;
|
|
931
|
+
element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
932
|
+
element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
|
|
933
|
+
element.effectUnmounts?.forEach((fn) => fn?.());
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
const patchTrackedElement = (target: Element, source: Element) => {
|
|
937
|
+
const nextProps = { ...source.props };
|
|
938
|
+
const nextPropObservables = source.propObservables;
|
|
939
|
+
|
|
940
|
+
if (target.props.context) {
|
|
941
|
+
nextProps.context = target.props.context;
|
|
942
|
+
}
|
|
943
|
+
if (target.props.children && !source.props.children) {
|
|
944
|
+
nextProps.children = target.props.children;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
target.props = nextProps;
|
|
948
|
+
target.propObservables = nextPropObservables;
|
|
949
|
+
target.componentInstance.onUpdate?.(nextProps);
|
|
950
|
+
Object.entries(target.directives).forEach(([name, directive]) => {
|
|
951
|
+
if (name in nextProps) {
|
|
952
|
+
directive.onUpdate?.(nextProps[name], target);
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
cleanupUntrackedElement(source);
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
const removeElementFromMap = (element: Element) => {
|
|
960
|
+
for (const [key, mappedElement] of elementMap.entries()) {
|
|
961
|
+
if (mappedElement === element) {
|
|
962
|
+
elementMap.delete(key);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
const rebuildArrayElements = (items: T[] | undefined | null) => {
|
|
969
|
+
if (!options.track) {
|
|
970
|
+
elements.forEach(el => destroyElement(el));
|
|
971
|
+
elements = [];
|
|
972
|
+
elementMap.clear();
|
|
973
|
+
|
|
974
|
+
if (items) {
|
|
975
|
+
items.forEach((item, index) => {
|
|
976
|
+
const element = ensureElement(createElementFn(item, index));
|
|
977
|
+
if (element) {
|
|
978
|
+
elements.push(element);
|
|
979
|
+
elementMap.set(index, element);
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const previousMap = elementMap;
|
|
987
|
+
const nextElements: Element[] = [];
|
|
988
|
+
const nextMap = new Map<string | number, Element>();
|
|
989
|
+
const usedElements = new Set<Element>();
|
|
990
|
+
|
|
991
|
+
if (items) {
|
|
992
|
+
items.forEach((item, index) => {
|
|
993
|
+
const key = getTrackKey(item, index);
|
|
994
|
+
const existing = previousMap.get(key);
|
|
995
|
+
const nextElement = ensureElement(createElementFn(item, index));
|
|
996
|
+
|
|
997
|
+
if (existing) {
|
|
998
|
+
if (nextElement) {
|
|
999
|
+
patchTrackedElement(existing, nextElement);
|
|
1000
|
+
}
|
|
1001
|
+
nextElements.push(existing);
|
|
1002
|
+
nextMap.set(key, existing);
|
|
1003
|
+
usedElements.add(existing);
|
|
1004
|
+
return;
|
|
362
1005
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
elements: newElements,
|
|
369
|
-
fullElements: elements,
|
|
370
|
-
};
|
|
371
|
-
} else if (type == "reset") {
|
|
372
|
-
if (elements.length != 0) {
|
|
373
|
-
elements.forEach((element) => {
|
|
374
|
-
destroyElement(element);
|
|
375
|
-
});
|
|
376
|
-
elements = [];
|
|
1006
|
+
|
|
1007
|
+
if (nextElement) {
|
|
1008
|
+
nextElements.push(nextElement);
|
|
1009
|
+
nextMap.set(key, nextElement);
|
|
1010
|
+
usedElements.add(nextElement);
|
|
377
1011
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
elements.forEach((element) => {
|
|
1016
|
+
if (!usedElements.has(element)) {
|
|
1017
|
+
destroyElement(element);
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
elements = nextElements;
|
|
1022
|
+
elementMap = nextMap;
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
return new Observable<FlowResult>(subscriber => {
|
|
1026
|
+
const subscription = isArraySignal(itemsSubject)
|
|
1027
|
+
? itemsSubject.observable.subscribe(change => {
|
|
1028
|
+
if (isFirstSubscription) {
|
|
1029
|
+
isFirstSubscription = false;
|
|
1030
|
+
rebuildArrayElements(itemsSubject());
|
|
1031
|
+
subscriber.next({
|
|
1032
|
+
elements: [...elements],
|
|
1033
|
+
reorder: Boolean(options.track)
|
|
1034
|
+
});
|
|
1035
|
+
return;
|
|
389
1036
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
1037
|
+
|
|
1038
|
+
// Handle computed signals that emit array values directly (not ArrayChange objects)
|
|
1039
|
+
// When a computed emits, `change` is the array itself, not an object with `type`
|
|
1040
|
+
const isDirectArrayChange = Array.isArray(change) || (change && typeof change === 'object' && !('type' in change));
|
|
1041
|
+
|
|
1042
|
+
if (change.type === 'init' || change.type === 'reset' || isDirectArrayChange) {
|
|
1043
|
+
rebuildArrayElements(itemsSubject());
|
|
1044
|
+
} else if (change.type === 'add' && change.index !== undefined) {
|
|
1045
|
+
const newElements = change.items.map((item, i) => {
|
|
1046
|
+
const index = change.index! + i;
|
|
1047
|
+
const element = ensureElement(createElementFn(item as T, index));
|
|
1048
|
+
if (element) {
|
|
1049
|
+
elementMap.set(getTrackKey(item as T, index), element);
|
|
1050
|
+
}
|
|
1051
|
+
return element;
|
|
1052
|
+
}).filter((el): el is Element => el !== null);
|
|
1053
|
+
|
|
1054
|
+
elements.splice(change.index, 0, ...newElements);
|
|
1055
|
+
} else if (change.type === 'remove' && change.index !== undefined) {
|
|
1056
|
+
const removed = elements.splice(change.index, 1);
|
|
1057
|
+
removed.forEach(el => {
|
|
1058
|
+
destroyElement(el)
|
|
1059
|
+
removeElementFromMap(el);
|
|
1060
|
+
});
|
|
1061
|
+
} else if (change.type === 'update' && change.index !== undefined && change.items.length === 1) {
|
|
1062
|
+
const index = change.index;
|
|
1063
|
+
const newItem = change.items[0];
|
|
1064
|
+
const key = getTrackKey(newItem as T, index);
|
|
1065
|
+
|
|
1066
|
+
// Check if the previous item at this index was effectively undefined or non-existent
|
|
1067
|
+
if (index >= elements.length || elements[index] === undefined || !elementMap.has(key)) {
|
|
1068
|
+
// Treat as add operation
|
|
1069
|
+
const newElement = ensureElement(createElementFn(newItem as T, index));
|
|
1070
|
+
if (newElement) {
|
|
1071
|
+
elements.splice(index, 0, newElement); // Insert at the correct index
|
|
1072
|
+
elementMap.set(key, newElement);
|
|
1073
|
+
// Adjust indices in elementMap for subsequent elements might be needed if map relied on exact indices
|
|
1074
|
+
// This simple implementation assumes keys are stable or createElementFn handles context correctly
|
|
1075
|
+
} else {
|
|
1076
|
+
console.warn(`Element creation returned null for index ${index} during add-like update.`);
|
|
1077
|
+
}
|
|
404
1078
|
} else {
|
|
405
|
-
|
|
1079
|
+
// Treat as a standard update operation
|
|
1080
|
+
const oldElement = elementMap.get(key) ?? elements[index];
|
|
1081
|
+
const newElement = ensureElement(createElementFn(newItem as T, index));
|
|
1082
|
+
if (options.track && oldElement && newElement) {
|
|
1083
|
+
patchTrackedElement(oldElement, newElement);
|
|
1084
|
+
elements[index] = oldElement;
|
|
1085
|
+
elementMap.set(key, oldElement);
|
|
1086
|
+
} else if (newElement) {
|
|
1087
|
+
destroyElement(oldElement)
|
|
1088
|
+
elements[index] = newElement;
|
|
1089
|
+
elementMap.set(key, newElement);
|
|
1090
|
+
} else {
|
|
1091
|
+
// Handle case where new element creation returns null
|
|
1092
|
+
destroyElement(oldElement)
|
|
1093
|
+
elements.splice(index, 1);
|
|
1094
|
+
elementMap.delete(key);
|
|
1095
|
+
}
|
|
406
1096
|
}
|
|
407
|
-
} else {
|
|
408
|
-
// For array updates, use addAt with the items array
|
|
409
|
-
newElements = addAt(items as T[], index);
|
|
410
1097
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
1098
|
+
|
|
1099
|
+
subscriber.next({
|
|
1100
|
+
elements: [...elements], // Create a new array to ensure change detection
|
|
1101
|
+
reorder: Boolean(options.track)
|
|
1102
|
+
});
|
|
1103
|
+
})
|
|
1104
|
+
: (itemsSubject as WritableObjectSignal<T>).observable.subscribe(change => {
|
|
1105
|
+
const key = change.key as string | number
|
|
1106
|
+
if (isFirstSubscription) {
|
|
1107
|
+
isFirstSubscription = false;
|
|
1108
|
+
elements.forEach(el => destroyElement(el));
|
|
1109
|
+
elements = [];
|
|
1110
|
+
elementMap.clear();
|
|
1111
|
+
|
|
1112
|
+
const items = (itemsSubject as WritableObjectSignal<T>)();
|
|
1113
|
+
if (items) {
|
|
1114
|
+
Object.entries(items).forEach(([key, value]) => {
|
|
1115
|
+
const element = ensureElement(createElementFn(value as T, key));
|
|
1116
|
+
if (element) {
|
|
1117
|
+
elements.push(element);
|
|
1118
|
+
elementMap.set(key, element);
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
subscriber.next({
|
|
1123
|
+
elements: [...elements]
|
|
422
1124
|
});
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (change.type === 'init' || change.type === 'reset') {
|
|
1129
|
+
elements.forEach(el => destroyElement(el));
|
|
1130
|
+
elements = [];
|
|
1131
|
+
elementMap.clear();
|
|
1132
|
+
|
|
1133
|
+
const items = (itemsSubject as WritableObjectSignal<T>)();
|
|
1134
|
+
if (items) {
|
|
1135
|
+
Object.entries(items).forEach(([key, value]) => {
|
|
1136
|
+
const element = ensureElement(createElementFn(value as T, key));
|
|
1137
|
+
if (element) {
|
|
1138
|
+
elements.push(element);
|
|
1139
|
+
elementMap.set(key, element);
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
} else if (change.type === 'add' && change.key && change.value !== undefined) {
|
|
1144
|
+
const element = ensureElement(createElementFn(change.value as T, key));
|
|
1145
|
+
if (element) {
|
|
1146
|
+
elements.push(element);
|
|
1147
|
+
elementMap.set(key, element);
|
|
1148
|
+
}
|
|
1149
|
+
} else if (change.type === 'remove' && change.key) {
|
|
1150
|
+
const index = elements.findIndex(el => elementMap.get(key) === el);
|
|
1151
|
+
if (index !== -1) {
|
|
1152
|
+
const [removed] = elements.splice(index, 1);
|
|
1153
|
+
destroyElement(removed)
|
|
1154
|
+
elementMap.delete(key);
|
|
1155
|
+
}
|
|
1156
|
+
} else if (change.type === 'update' && change.key && change.value !== undefined) {
|
|
1157
|
+
const index = elements.findIndex(el => elementMap.get(key) === el);
|
|
1158
|
+
if (index !== -1) {
|
|
1159
|
+
const oldElement = elements[index];
|
|
1160
|
+
destroyElement(oldElement)
|
|
1161
|
+
const newElement = ensureElement(createElementFn(change.value as T, key));
|
|
1162
|
+
if (newElement) {
|
|
1163
|
+
elements[index] = newElement;
|
|
1164
|
+
elementMap.set(key, newElement);
|
|
1165
|
+
}
|
|
427
1166
|
}
|
|
428
|
-
} else if (typeof index === 'number') {
|
|
429
|
-
// For array element deletion
|
|
430
|
-
const currentElement = elements[index];
|
|
431
|
-
destroyElement(currentElement);
|
|
432
|
-
elements.splice(index, 1);
|
|
433
1167
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
);
|
|
445
|
-
});
|
|
1168
|
+
|
|
1169
|
+
subscriber.next({
|
|
1170
|
+
elements: [...elements] // Create a new array to ensure change detection
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
return () => {
|
|
1175
|
+
subscription.unsubscribe();
|
|
1176
|
+
elements.forEach(el => destroyElement(el));
|
|
1177
|
+
};
|
|
1178
|
+
});
|
|
1179
|
+
}).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
|
446
1180
|
}
|
|
447
1181
|
|
|
448
1182
|
/**
|
|
449
|
-
* Conditionally creates and destroys elements based on
|
|
1183
|
+
* Conditionally creates and destroys elements based on condition signals with support for else if and else.
|
|
1184
|
+
*
|
|
1185
|
+
* @description This function creates conditional rendering with support for multiple conditions (if/else if/else pattern).
|
|
1186
|
+
* It evaluates conditions in order and renders the first matching condition's element.
|
|
1187
|
+
* The function maintains full reactivity with signals and ensures proper cleanup of elements.
|
|
450
1188
|
*
|
|
451
|
-
* @param {Signal<boolean> | boolean} condition - A signal or
|
|
1189
|
+
* @param {Signal<boolean> | boolean | (() => boolean)} condition - A signal, boolean, or function that determines whether to create an element.
|
|
452
1190
|
* @param {Function} createElementFn - A function that returns an element or a promise that resolves to an element.
|
|
453
|
-
* @
|
|
1191
|
+
* @param {...Array} additionalConditions - Additional conditions for else if and else cases.
|
|
1192
|
+
* Can be:
|
|
1193
|
+
* - A function for else case: `() => Element | Promise<Element>`
|
|
1194
|
+
* - An array for else if case: `[Signal<boolean> | boolean | (() => boolean), () => Element | Promise<Element>]`
|
|
1195
|
+
* @returns {Observable} An observable that emits the created element based on the matching condition.
|
|
1196
|
+
*
|
|
1197
|
+
* @example
|
|
1198
|
+
* ```typescript
|
|
1199
|
+
* // Simple if/else
|
|
1200
|
+
* cond(
|
|
1201
|
+
* signal(isVisible),
|
|
1202
|
+
* () => h(Container),
|
|
1203
|
+
* () => h(Text, { text: 'Hidden' }) // else
|
|
1204
|
+
* );
|
|
1205
|
+
*
|
|
1206
|
+
* // Multiple else if + else
|
|
1207
|
+
* cond(
|
|
1208
|
+
* signal(status === 'loading'),
|
|
1209
|
+
* () => h(LoadingSpinner),
|
|
1210
|
+
* [signal(status === 'error'), () => h(ErrorMessage)], // else if
|
|
1211
|
+
* [signal(status === 'success'), () => h(SuccessMessage)], // else if
|
|
1212
|
+
* () => h(DefaultMessage) // else
|
|
1213
|
+
* );
|
|
1214
|
+
* ```
|
|
454
1215
|
*/
|
|
455
1216
|
export function cond(
|
|
456
|
-
condition: Signal<boolean> | boolean,
|
|
457
|
-
createElementFn: () => Element | Promise<Element
|
|
1217
|
+
condition: Signal<boolean> | boolean | (() => boolean),
|
|
1218
|
+
createElementFn: () => Element | Promise<Element>,
|
|
1219
|
+
...additionalConditions: Array<
|
|
1220
|
+
| (() => Element | Promise<Element>) // else final
|
|
1221
|
+
| [Signal<boolean> | boolean | (() => boolean), () => Element | Promise<Element>] // else if
|
|
1222
|
+
>
|
|
458
1223
|
): FlowObservable {
|
|
459
|
-
let
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1224
|
+
let currentElement: Element | null = null;
|
|
1225
|
+
let currentConditionIndex = -1;
|
|
1226
|
+
|
|
1227
|
+
// Parse additional conditions
|
|
1228
|
+
const elseIfConditions: Array<{
|
|
1229
|
+
condition: Signal<boolean>;
|
|
1230
|
+
elementFn: () => Element | Promise<Element>;
|
|
1231
|
+
}> = [];
|
|
1232
|
+
let elseElementFn: (() => Element | Promise<Element>) | null = null;
|
|
1233
|
+
|
|
1234
|
+
// Convert function conditions to computed signals
|
|
1235
|
+
const convertConditionToSignal = (cond: Signal<boolean> | boolean | (() => boolean)): Signal<boolean> => {
|
|
1236
|
+
if (isSignal(cond)) {
|
|
1237
|
+
return cond as Signal<boolean>;
|
|
1238
|
+
} else if (typeof cond === 'function') {
|
|
1239
|
+
return computed(cond as () => boolean);
|
|
1240
|
+
} else {
|
|
1241
|
+
return signal(cond as boolean);
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
// Process additional conditions
|
|
1246
|
+
for (const param of additionalConditions) {
|
|
1247
|
+
if (Array.isArray(param)) {
|
|
1248
|
+
// else if case: [condition, elementFn]
|
|
1249
|
+
elseIfConditions.push({
|
|
1250
|
+
condition: convertConditionToSignal(param[0]),
|
|
1251
|
+
elementFn: param[1],
|
|
1252
|
+
});
|
|
1253
|
+
} else if (typeof param === 'function') {
|
|
1254
|
+
// else case: elementFn (should be the last one)
|
|
1255
|
+
elseElementFn = param;
|
|
1256
|
+
break; // Stop processing after else
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Collect all conditions with their element functions
|
|
1261
|
+
const allConditions = [
|
|
1262
|
+
{ condition: convertConditionToSignal(condition), elementFn: createElementFn },
|
|
1263
|
+
...elseIfConditions,
|
|
1264
|
+
];
|
|
1265
|
+
|
|
1266
|
+
// All conditions are now signals, so we always use the reactive path
|
|
1267
|
+
return new Observable<{ elements: Element[], type?: "init" | "remove" }>(subscriber => {
|
|
1268
|
+
const subscriptions: Subscription[] = [];
|
|
1269
|
+
|
|
1270
|
+
const evaluateConditions = () => {
|
|
1271
|
+
// Find the first matching condition
|
|
1272
|
+
let matchingIndex = -1;
|
|
1273
|
+
for (let i = 0; i < allConditions.length; i++) {
|
|
1274
|
+
const condition = allConditions[i].condition;
|
|
1275
|
+
const conditionValue = condition();
|
|
1276
|
+
|
|
1277
|
+
if (conditionValue) {
|
|
1278
|
+
matchingIndex = i;
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// If no condition matches and we have an else, use else
|
|
1284
|
+
const shouldUseElse = matchingIndex === -1 && elseElementFn;
|
|
1285
|
+
const newConditionIndex = shouldUseElse ? -2 : matchingIndex; // -2 for else, -1 for nothing
|
|
1286
|
+
|
|
1287
|
+
// Only update if the condition changed
|
|
1288
|
+
if (newConditionIndex !== currentConditionIndex) {
|
|
1289
|
+
// Destroy current element if it exists
|
|
1290
|
+
if (currentElement) {
|
|
1291
|
+
destroyElement(currentElement);
|
|
1292
|
+
currentElement = null;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
currentConditionIndex = newConditionIndex;
|
|
1296
|
+
|
|
1297
|
+
if (shouldUseElse) {
|
|
1298
|
+
// Render else element
|
|
1299
|
+
let _el = elseElementFn!();
|
|
467
1300
|
if (isPromise(_el)) {
|
|
468
1301
|
from(_el as Promise<Element>).subscribe(el => {
|
|
469
|
-
|
|
1302
|
+
currentElement = el;
|
|
470
1303
|
subscriber.next({
|
|
471
1304
|
type: "init",
|
|
472
1305
|
elements: [el],
|
|
473
1306
|
});
|
|
474
1307
|
});
|
|
475
1308
|
} else {
|
|
476
|
-
|
|
1309
|
+
currentElement = _el as Element;
|
|
477
1310
|
subscriber.next({
|
|
478
1311
|
type: "init",
|
|
479
|
-
elements: [
|
|
1312
|
+
elements: [currentElement],
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
} else if (matchingIndex >= 0) {
|
|
1316
|
+
// Render matching condition element
|
|
1317
|
+
let _el = allConditions[matchingIndex].elementFn();
|
|
1318
|
+
if (isPromise(_el)) {
|
|
1319
|
+
from(_el as Promise<Element>).subscribe(el => {
|
|
1320
|
+
currentElement = el;
|
|
1321
|
+
subscriber.next({
|
|
1322
|
+
type: "init",
|
|
1323
|
+
elements: [el],
|
|
1324
|
+
});
|
|
1325
|
+
});
|
|
1326
|
+
} else {
|
|
1327
|
+
currentElement = _el as Element;
|
|
1328
|
+
subscriber.next({
|
|
1329
|
+
type: "init",
|
|
1330
|
+
elements: [currentElement],
|
|
480
1331
|
});
|
|
481
1332
|
}
|
|
482
|
-
} else if (element) {
|
|
483
|
-
destroyElement(element);
|
|
484
|
-
subscriber.next({
|
|
485
|
-
elements: [],
|
|
486
|
-
});
|
|
487
1333
|
} else {
|
|
1334
|
+
// No matching condition and no else
|
|
488
1335
|
subscriber.next({
|
|
489
1336
|
elements: [],
|
|
490
1337
|
});
|
|
491
1338
|
}
|
|
492
|
-
});
|
|
493
|
-
});
|
|
494
|
-
} else {
|
|
495
|
-
// Handle boolean case
|
|
496
|
-
if (condition) {
|
|
497
|
-
let _el = createElementFn();
|
|
498
|
-
if (isPromise(_el)) {
|
|
499
|
-
return from(_el as Promise<Element>).pipe(
|
|
500
|
-
map((el) => ({
|
|
501
|
-
type: "init",
|
|
502
|
-
elements: [el],
|
|
503
|
-
}))
|
|
504
|
-
);
|
|
505
1339
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
// Subscribe to all signal conditions
|
|
1343
|
+
allConditions.forEach(({ condition }) => {
|
|
1344
|
+
const signalCondition = condition as WritableObjectSignal<boolean>;
|
|
1345
|
+
subscriptions.push(
|
|
1346
|
+
signalCondition.observable.subscribe(() => {
|
|
1347
|
+
evaluateConditions();
|
|
1348
|
+
})
|
|
1349
|
+
);
|
|
513
1350
|
});
|
|
514
|
-
|
|
1351
|
+
|
|
1352
|
+
// Initial evaluation
|
|
1353
|
+
evaluateConditions();
|
|
1354
|
+
|
|
1355
|
+
// Return cleanup function
|
|
1356
|
+
return () => {
|
|
1357
|
+
subscriptions.forEach(sub => sub.unsubscribe());
|
|
1358
|
+
if (currentElement) {
|
|
1359
|
+
destroyElement(currentElement);
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
}).pipe(share());
|
|
515
1363
|
}
|