canvasengine 2.0.0-beta.5 → 2.0.0-beta.50

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