canvasengine 2.0.0-beta.8 → 2.0.0-rc.1

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 (167) hide show
  1. package/dist/DebugRenderer-DkjTAc48.js +1384 -0
  2. package/dist/DebugRenderer-DkjTAc48.js.map +1 -0
  3. package/dist/components/Button.d.ts +185 -0
  4. package/dist/components/Button.d.ts.map +1 -0
  5. package/dist/components/Canvas.d.ts +17 -0
  6. package/dist/components/Canvas.d.ts.map +1 -0
  7. package/dist/components/DOMElement.d.ts +54 -0
  8. package/dist/components/DOMElement.d.ts.map +1 -0
  9. package/dist/components/DOMSprite.d.ts +127 -0
  10. package/dist/components/DOMSprite.d.ts.map +1 -0
  11. package/dist/components/FocusContainer.d.ts +129 -0
  12. package/dist/components/FocusContainer.d.ts.map +1 -0
  13. package/dist/components/Graphic.d.ts +64 -0
  14. package/dist/components/Graphic.d.ts.map +1 -0
  15. package/dist/components/Joystick.d.ts +36 -0
  16. package/dist/components/Joystick.d.ts.map +1 -0
  17. package/dist/components/NineSliceSprite.d.ts +16 -0
  18. package/dist/components/NineSliceSprite.d.ts.map +1 -0
  19. package/dist/components/ParticleEmitter.d.ts +4 -0
  20. package/dist/components/ParticleEmitter.d.ts.map +1 -0
  21. package/dist/components/Scene.d.ts +2 -0
  22. package/dist/components/Scene.d.ts.map +1 -0
  23. package/dist/components/Text.d.ts +24 -0
  24. package/dist/components/Text.d.ts.map +1 -0
  25. package/dist/components/TilingSprite.d.ts +17 -0
  26. package/dist/components/TilingSprite.d.ts.map +1 -0
  27. package/dist/components/Video.d.ts +14 -0
  28. package/dist/components/Video.d.ts.map +1 -0
  29. package/dist/components/index.d.ts +20 -0
  30. package/dist/components/index.d.ts.map +1 -0
  31. package/dist/components/types/DisplayObject.d.ts +122 -0
  32. package/dist/components/types/DisplayObject.d.ts.map +1 -0
  33. package/dist/components/types/MouseEvent.d.ts +4 -0
  34. package/dist/components/types/MouseEvent.d.ts.map +1 -0
  35. package/dist/components/types/Spritesheet.d.ts +248 -0
  36. package/dist/components/types/Spritesheet.d.ts.map +1 -0
  37. package/dist/components/types/index.d.ts +4 -0
  38. package/dist/components/types/index.d.ts.map +1 -0
  39. package/dist/directives/Controls.d.ts +112 -0
  40. package/dist/directives/Controls.d.ts.map +1 -0
  41. package/dist/directives/ControlsBase.d.ts +199 -0
  42. package/dist/directives/ControlsBase.d.ts.map +1 -0
  43. package/dist/directives/Drag.d.ts +69 -0
  44. package/dist/directives/Drag.d.ts.map +1 -0
  45. package/dist/directives/Flash.d.ts +116 -0
  46. package/dist/directives/Flash.d.ts.map +1 -0
  47. package/dist/directives/FocusNavigation.d.ts +52 -0
  48. package/dist/directives/FocusNavigation.d.ts.map +1 -0
  49. package/dist/directives/FogVisibility.d.ts +47 -0
  50. package/dist/directives/FogVisibility.d.ts.map +1 -0
  51. package/dist/directives/GamepadControls.d.ts +224 -0
  52. package/dist/directives/GamepadControls.d.ts.map +1 -0
  53. package/dist/directives/JoystickControls.d.ts +171 -0
  54. package/dist/directives/JoystickControls.d.ts.map +1 -0
  55. package/dist/directives/KeyboardControls.d.ts +219 -0
  56. package/dist/directives/KeyboardControls.d.ts.map +1 -0
  57. package/dist/directives/Scheduler.d.ts +36 -0
  58. package/dist/directives/Scheduler.d.ts.map +1 -0
  59. package/dist/directives/Shake.d.ts +98 -0
  60. package/dist/directives/Shake.d.ts.map +1 -0
  61. package/dist/directives/Sound.d.ts +25 -0
  62. package/dist/directives/Sound.d.ts.map +1 -0
  63. package/dist/directives/SpriteEffects.d.ts +70 -0
  64. package/dist/directives/SpriteEffects.d.ts.map +1 -0
  65. package/dist/directives/Transition.d.ts +10 -0
  66. package/dist/directives/Transition.d.ts.map +1 -0
  67. package/dist/directives/ViewportCull.d.ts +11 -0
  68. package/dist/directives/ViewportCull.d.ts.map +1 -0
  69. package/dist/directives/ViewportFollow.d.ts +18 -0
  70. package/dist/directives/ViewportFollow.d.ts.map +1 -0
  71. package/dist/directives/index.d.ts +15 -0
  72. package/dist/directives/index.d.ts.map +1 -0
  73. package/dist/dist-BOOc43Qm.js +778 -0
  74. package/dist/dist-BOOc43Qm.js.map +1 -0
  75. package/dist/engine/FocusManager.d.ts +174 -0
  76. package/dist/engine/FocusManager.d.ts.map +1 -0
  77. package/dist/engine/animation.d.ts +72 -0
  78. package/dist/engine/animation.d.ts.map +1 -0
  79. package/dist/engine/bootstrap.d.ts +52 -0
  80. package/dist/engine/bootstrap.d.ts.map +1 -0
  81. package/dist/engine/directive.d.ts +13 -0
  82. package/dist/engine/directive.d.ts.map +1 -0
  83. package/dist/engine/reactive.d.ts +135 -0
  84. package/dist/engine/reactive.d.ts.map +1 -0
  85. package/dist/engine/signal.d.ts +73 -0
  86. package/dist/engine/signal.d.ts.map +1 -0
  87. package/dist/engine/trigger.d.ts +54 -0
  88. package/dist/engine/trigger.d.ts.map +1 -0
  89. package/dist/engine/utils.d.ts +89 -0
  90. package/dist/engine/utils.d.ts.map +1 -0
  91. package/dist/hooks/addContext.d.ts +2 -0
  92. package/dist/hooks/addContext.d.ts.map +1 -0
  93. package/dist/hooks/useFocus.d.ts +60 -0
  94. package/dist/hooks/useFocus.d.ts.map +1 -0
  95. package/dist/hooks/useProps.d.ts +42 -0
  96. package/dist/hooks/useProps.d.ts.map +1 -0
  97. package/dist/hooks/useRef.d.ts +4 -0
  98. package/dist/hooks/useRef.d.ts.map +1 -0
  99. package/dist/index.d.ts +19 -1135
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.global.js +189 -0
  102. package/dist/index.global.js.map +1 -0
  103. package/dist/index.js +15001 -3211
  104. package/dist/index.js.map +1 -1
  105. package/dist/utils/Ease.d.ts +17 -0
  106. package/dist/utils/Ease.d.ts.map +1 -0
  107. package/dist/utils/GlobalAssetLoader.d.ts +141 -0
  108. package/dist/utils/GlobalAssetLoader.d.ts.map +1 -0
  109. package/dist/utils/RadialGradient.d.ts +57 -0
  110. package/dist/utils/RadialGradient.d.ts.map +1 -0
  111. package/dist/utils/functions.d.ts +2 -0
  112. package/dist/utils/functions.d.ts.map +1 -0
  113. package/dist/utils/tabindex.d.ts +16 -0
  114. package/dist/utils/tabindex.d.ts.map +1 -0
  115. package/package.json +16 -9
  116. package/src/components/Button.ts +399 -0
  117. package/src/components/Canvas.ts +82 -51
  118. package/src/components/Container.ts +21 -2
  119. package/src/components/DOMContainer.ts +379 -0
  120. package/src/components/DOMElement.ts +556 -0
  121. package/src/components/DOMSprite.ts +1040 -0
  122. package/src/components/DisplayObject.ts +422 -201
  123. package/src/components/FocusContainer.ts +368 -0
  124. package/src/components/Graphic.ts +239 -73
  125. package/src/components/Joystick.ts +363 -0
  126. package/src/components/Mesh.ts +222 -0
  127. package/src/components/NineSliceSprite.ts +4 -1
  128. package/src/components/ParticleEmitter.ts +12 -8
  129. package/src/components/Sprite.ts +418 -52
  130. package/src/components/Text.ts +268 -24
  131. package/src/components/Viewport.ts +122 -63
  132. package/src/components/index.ts +9 -2
  133. package/src/components/types/DisplayObject.ts +57 -5
  134. package/src/components/types/Spritesheet.ts +0 -118
  135. package/src/directives/Controls.ts +254 -0
  136. package/src/directives/ControlsBase.ts +267 -0
  137. package/src/directives/Drag.ts +357 -52
  138. package/src/directives/Flash.ts +419 -0
  139. package/src/directives/FocusNavigation.ts +113 -0
  140. package/src/directives/FogVisibility.ts +273 -0
  141. package/src/directives/GamepadControls.ts +537 -0
  142. package/src/directives/JoystickControls.ts +396 -0
  143. package/src/directives/KeyboardControls.ts +85 -430
  144. package/src/directives/Scheduler.ts +21 -5
  145. package/src/directives/Shake.ts +298 -0
  146. package/src/directives/Sound.ts +94 -31
  147. package/src/directives/SpriteEffects.ts +461 -0
  148. package/src/directives/ViewportFollow.ts +40 -9
  149. package/src/directives/index.ts +14 -6
  150. package/src/engine/FocusManager.ts +510 -0
  151. package/src/engine/animation.ts +137 -19
  152. package/src/engine/bootstrap.ts +140 -6
  153. package/src/engine/directive.ts +4 -4
  154. package/src/engine/reactive.ts +979 -236
  155. package/src/engine/signal.ts +241 -47
  156. package/src/engine/trigger.ts +34 -7
  157. package/src/engine/utils.ts +19 -3
  158. package/src/hooks/useFocus.ts +91 -0
  159. package/src/hooks/useProps.ts +1 -1
  160. package/src/index.ts +8 -2
  161. package/src/types/pixi-cull.d.ts +7 -0
  162. package/src/utils/GlobalAssetLoader.ts +257 -0
  163. package/src/utils/functions.ts +7 -0
  164. package/src/utils/tabindex.ts +70 -0
  165. package/testing/index.ts +35 -4
  166. package/tsconfig.json +18 -0
  167. package/vite.config.ts +39 -0
@@ -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,6 +48,7 @@ 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 = {
@@ -90,11 +81,220 @@ export const isPrimitive = (value) => {
90
81
  );
91
82
  };
92
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
+
93
150
  export function registerComponent(name, component) {
94
151
  components[name] = component;
95
152
  }
96
153
 
97
- function destroyElement(element: Element | Element[]) {
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
+
297
+ export function destroyElement(element: Element | Element[]) {
98
298
  if (Array.isArray(element)) {
99
299
  element.forEach((e) => destroyElement(e));
100
300
  return;
@@ -102,13 +302,34 @@ function destroyElement(element: Element | Element[]) {
102
302
  if (!element) {
103
303
  return;
104
304
  }
105
- element.propSubscriptions.forEach((sub) => sub.unsubscribe());
106
- element.effectSubscriptions.forEach((sub) => sub.unsubscribe());
305
+ if (element.props?.children) {
306
+ for (let child of element.props.children) {
307
+ destroyElement(child)
308
+ }
309
+ }
107
310
  for (let name in element.directives) {
108
- 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?.());
109
332
  }
110
- element.componentInstance.onDestroy?.(element.parent as any);
111
- element.effectUnmounts.forEach((fn) => fn?.());
112
333
  }
113
334
 
114
335
  /**
@@ -141,6 +362,7 @@ export function createComponent(tag: string, props?: Props): Element {
141
362
  destroyElement(this);
142
363
  },
143
364
  allElements: new Subject(),
365
+ isFrozen: false,
144
366
  };
145
367
 
146
368
  // Iterate over each property in the props object
@@ -159,27 +381,78 @@ export function createComponent(tag: string, props?: Props): Element {
159
381
  const _value = value as Signal<any>;
160
382
  if ("dependencies" in _value && _value.dependencies.size == 0) {
161
383
  _set(path, key, _value());
384
+ // Handle freeze prop initialization
385
+ if (key === "freeze") {
386
+ element.isFrozen = _value() === true;
387
+ }
162
388
  return;
163
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
+ );
409
+ return;
410
+ }
411
+
164
412
  element.propSubscriptions.push(
165
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
+
166
428
  _set(path, key, value);
167
429
  if (element.directives[key]) {
168
- element.directives[key].onUpdate?.(value);
430
+ element.directives[key].onUpdate?.(value, element);
169
431
  }
170
432
  if (key == "tick") {
433
+ // Block tick updates if element is frozen
434
+ if (isElementFrozen(element)) {
435
+ return;
436
+ }
171
437
  return
172
438
  }
173
439
  instance.onUpdate?.(
174
440
  path == ""
175
441
  ? {
176
- [key]: value,
177
- }
442
+ [key]: value,
443
+ }
178
444
  : set({}, path + "." + key, value)
179
445
  );
180
446
  })
181
447
  );
182
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
+ }
183
456
  if (isObject(value) && key != "context" && !isElement(value)) {
184
457
  recursiveProps(value, (path ? path + "." : "") + key);
185
458
  } else {
@@ -209,9 +482,69 @@ export function createComponent(tag: string, props?: Props): Element {
209
482
  }
210
483
  }
211
484
 
212
- function onMount(parent: Element, element: Element, index?: number) {
213
- element.props.context = parent.props.context;
214
- 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) {
215
548
  element.componentInstance.onMount?.(element, index);
216
549
  for (let name in element.directives) {
217
550
  element.directives[name].onMount?.(element);
@@ -219,6 +552,34 @@ export function createComponent(tag: string, props?: Props): Element {
219
552
  element.effectMounts.forEach((fn: any) => {
220
553
  element.effectUnmounts.push(fn(element));
221
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);
222
583
  };
223
584
 
224
585
  async function propagateContext(element) {
@@ -229,71 +590,262 @@ export function createComponent(tag: string, props?: Props): Element {
229
590
  }
230
591
  else {
231
592
  await new Promise((resolve) => {
232
- let lastElement = null
233
- element.propSubscriptions.push(element.propObservables.attach.observable.subscribe(async (args) => {
234
- const value = args?.value ?? args
235
- if (!value) {
236
- throw new Error(`attach in ${element.tag} is undefined or null, add a component`)
237
- }
238
- if (lastElement) {
239
- destroyElement(lastElement)
240
- }
241
- lastElement = value
242
- await createElement(element, value)
243
- resolve(undefined)
244
- }))
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
+ }))
245
606
  })
246
607
  }
247
608
  }
248
609
  if (!element.props.children) {
249
610
  return;
250
611
  }
251
- for (let child of element.props.children) {
612
+ for (let i = 0; i < element.props.children.length; i++) {
613
+ const child = element.props.children[i];
252
614
  if (!child) continue;
253
- await createElement(element, child)
615
+ await createElement(element, child, i)
254
616
  }
255
617
  };
256
618
 
257
- async function createElement(parent: Element, child: Element) {
619
+ /**
620
+ * Creates and mounts a child element to a parent element.
621
+ * Handles different types of children: Elements, Promises resolving to Elements, and Observables.
622
+ *
623
+ * @description This function is designed to handle reactive child components that can be:
624
+ * - Direct Element instances
625
+ * - Promises that resolve to Elements (for async components)
626
+ * - Observables that emit Elements, arrays of Elements, or FlowObservable results
627
+ * - Nested observables within arrays or FlowObservable results (handled recursively)
628
+ *
629
+ * For Observables, it subscribes to the stream and automatically mounts/unmounts elements
630
+ * as they are emitted. The function handles nested observables recursively, ensuring that
631
+ * observables within arrays or FlowObservable results are also properly subscribed to.
632
+ * All subscriptions are stored in the parent's effectSubscriptions for automatic cleanup.
633
+ *
634
+ * @param {Element} parent - The parent element to mount the child to
635
+ * @param {Element | Observable<any> | Promise<Element>} child - The child to create and mount
636
+ *
637
+ * @example
638
+ * ```typescript
639
+ * // Direct element
640
+ * await createElement(parent, childElement);
641
+ *
642
+ * // Observable of elements (from cond, loop, etc.)
643
+ * await createElement(parent, cond(signal(visible), () => h(Container)));
644
+ *
645
+ * // Observable that emits arrays containing other observables
646
+ * await createElement(parent, observableOfObservables);
647
+ *
648
+ * // Promise resolving to element
649
+ * await createElement(parent, import('./MyComponent').then(mod => h(mod.default)));
650
+ * ```
651
+ */
652
+ async function createElement(parent: Element, child: Element | Observable<any> | Promise<Element>, childOrder?: number) {
258
653
  if (isPromise(child)) {
259
654
  child = await child;
260
655
  }
656
+
657
+ const childGroups = ((parent as any).__childGroups ??= []);
658
+ const resolvedOrder =
659
+ childOrder ??
660
+ (parent.props.children ? parent.props.children.indexOf(child as any) : -1);
661
+ const childGroup = {
662
+ order: resolvedOrder >= 0 ? resolvedOrder : childGroups.length,
663
+ mounted: new Map<any, Element>(),
664
+ };
665
+ childGroups.push(childGroup);
666
+
667
+ const getMountedIndex = (element?: Element): number | undefined => {
668
+ const children = (parent.componentInstance as any)?.children;
669
+ if (!element || !children) return;
670
+ const index = children.indexOf(element.componentInstance);
671
+ return index >= 0 ? index : undefined;
672
+ };
673
+
674
+ const getNextGroupIndex = (): number | undefined => {
675
+ const nextGroups = childGroups
676
+ .filter((group) => group !== childGroup && group.order > childGroup.order)
677
+ .sort((a, b) => a.order - b.order);
678
+
679
+ for (const group of nextGroups) {
680
+ for (const mounted of group.mounted.values()) {
681
+ const index = getMountedIndex(mounted);
682
+ if (index !== undefined) return index;
683
+ }
684
+ }
685
+ };
686
+
687
+ const getInsertIndex = (
688
+ sourceIndex: number,
689
+ orderedSources: any[]
690
+ ): number | undefined => {
691
+ for (let i = sourceIndex + 1; i < orderedSources.length; i++) {
692
+ const index = getMountedIndex(childGroup.mounted.get(orderedSources[i]));
693
+ if (index !== undefined) return index;
694
+ }
695
+ return getNextGroupIndex();
696
+ };
697
+
261
698
  if (child instanceof Observable) {
262
- child.subscribe(
263
- ({
264
- elements: comp,
265
- prev,
266
- }: {
267
- elements: Element[];
268
- prev?: Element;
269
- }) => {
270
- // if prev, insert element after this
271
- const components = comp.filter((c) => c !== null);
272
- if (prev) {
273
- components.forEach((c) => {
274
- const index = parent.props.children.indexOf(prev.props.key);
275
- onMount(parent, c, index + 1);
276
- propagateContext(c);
277
- });
699
+ const mountedFlowElements = childGroup.mounted;
700
+ const flowEffectSubscriptions = ((child as any).effectSubscriptions ?? []) as Subscription[];
701
+ const flowEffectMounts = ((child as any).effectMounts ?? []) as Array<(element: Element) => any>;
702
+
703
+ const applyFlowEffects = (element: Element) => {
704
+ if (!flowEffectMounts.length) {
705
+ return;
706
+ }
707
+
708
+ element.effectMounts = [
709
+ ...flowEffectMounts,
710
+ ...(element.effectMounts ?? []),
711
+ ];
712
+ };
713
+
714
+ const createFragmentOwner = (): Element => ({
715
+ tag: 'fragment',
716
+ props: { children: [] },
717
+ componentInstance: {} as any,
718
+ propSubscriptions: [],
719
+ effectSubscriptions: [],
720
+ effectMounts: [],
721
+ effectUnmounts: [],
722
+ propObservables: {},
723
+ parent,
724
+ directives: {},
725
+ destroy() { destroyElement(this) },
726
+ allElements: new Subject(),
727
+ isFrozen: false
728
+ });
729
+
730
+ const mountFlowElement = (
731
+ element: Element,
732
+ sourceIndex: number,
733
+ orderedSources: any[]
734
+ ) => {
735
+ if (mountedFlowElements.has(element)) {
736
+ return;
737
+ }
738
+
739
+ const routed = routeDomComponent(parent, element);
740
+ applyFlowEffects(routed);
741
+ mountedFlowElements.set(element, routed);
742
+ onMount(parent, routed, getInsertIndex(sourceIndex, orderedSources));
743
+ propagateContext(routed);
744
+ };
745
+
746
+ const syncFlowElements = (nextElements: Set<any>) => {
747
+ mountedFlowElements.forEach((mounted, source) => {
748
+ if (nextElements.has(source)) {
278
749
  return;
279
750
  }
280
- components.forEach((component) => {
281
- if (!Array.isArray(component)) {
282
- onMount(parent, component);
283
- propagateContext(component);
284
- } else {
285
- component.forEach((comp) => {
286
- onMount(parent, comp);
287
- propagateContext(comp);
751
+ mountedFlowElements.delete(source);
752
+ if (mounted !== source) {
753
+ destroyElement(mounted);
754
+ }
755
+ });
756
+ };
757
+
758
+ const processFlowComponent = (
759
+ component: any,
760
+ nextElements: Set<any>,
761
+ index: number,
762
+ orderedSources: any[]
763
+ ) => {
764
+ if (component instanceof Observable) {
765
+ nextElements.add(component);
766
+ if (!mountedFlowElements.has(component)) {
767
+ const owner = createFragmentOwner();
768
+ mountedFlowElements.set(component, owner);
769
+ void createElement(owner, component);
770
+ }
771
+ return;
772
+ }
773
+ if (Array.isArray(component)) {
774
+ component.forEach((comp) =>
775
+ processFlowComponent(comp, nextElements, index, orderedSources)
776
+ );
777
+ return;
778
+ }
779
+ if (!isElement(component)) {
780
+ return;
781
+ }
782
+
783
+ nextElements.add(component);
784
+ mountFlowElement(component, index, orderedSources);
785
+ };
786
+
787
+ // Subscribe to the observable and handle the emitted values
788
+ const subscription = child.subscribe(
789
+ (value: any) => {
790
+ // Handle different types of observable emissions
791
+ if (value && typeof value === 'object' && 'elements' in value) {
792
+ // Handle FlowObservable result (from loop, cond, etc.)
793
+ const {
794
+ elements: comp,
795
+ prev,
796
+ }: {
797
+ elements: Element[];
798
+ prev?: Element;
799
+ } = value;
800
+
801
+ const components = comp.filter((c) => c !== null);
802
+ const nextElements = new Set<any>();
803
+ if (prev) {
804
+ components.forEach((c) => {
805
+ const index = parent.props.children.indexOf(prev.props.key);
806
+ processFlowComponent(c, nextElements, index + 1, components);
288
807
  });
808
+ syncFlowElements(nextElements);
809
+ return;
289
810
  }
290
- });
291
- elementsListen.next(undefined)
811
+ components.forEach((component, index) => {
812
+ processFlowComponent(component, nextElements, index, components);
813
+ });
814
+ syncFlowElements(nextElements);
815
+ } else if (isElement(value)) {
816
+ // Handle direct Element emission
817
+ const routed = routeDomComponent(parent, value);
818
+ applyFlowEffects(routed);
819
+ childGroup.mounted.set(value, routed);
820
+ onMount(parent, routed, getInsertIndex(0, [value]));
821
+ propagateContext(routed);
822
+ } else if (Array.isArray(value)) {
823
+ // Handle array of elements (which can also be observables)
824
+ const nextElements = new Set<any>();
825
+ value.forEach((element, index) => {
826
+ processFlowComponent(element, nextElements, index, value);
827
+ });
828
+ syncFlowElements(nextElements);
829
+ }
830
+ elementsListen.next(undefined);
292
831
  }
293
832
  );
294
- } else {
295
- onMount(parent, child);
296
- await propagateContext(child);
833
+
834
+ subscription.add(() => {
835
+ mountedFlowElements.forEach((mounted) => {
836
+ destroyElement(mounted);
837
+ });
838
+ mountedFlowElements.clear();
839
+ flowEffectSubscriptions.forEach((sub) => sub.unsubscribe());
840
+ });
841
+
842
+ // Store subscription for cleanup
843
+ parent.effectSubscriptions.push(subscription);
844
+ } else if (isElement(child)) {
845
+ const routed = routeDomComponent(parent, child);
846
+ childGroup.mounted.set(child, routed);
847
+ onMount(parent, routed, getInsertIndex(0, [child]));
848
+ await propagateContext(routed);
297
849
  }
298
850
  }
299
851
 
@@ -309,207 +861,398 @@ export function createComponent(tag: string, props?: Props): Element {
309
861
  * @returns {Observable} An observable that emits the list of created child elements.
310
862
  */
311
863
  export function loop<T>(
312
- itemsSubject: WritableArraySignal<T[]> | WritableObjectSignal<T>,
864
+ itemsSubject: any,
313
865
  createElementFn: (item: T, index: number | string) => Element | null
314
866
  ): FlowObservable {
315
- let elements: Element[] = [];
316
867
 
317
- const isArraySignal = '_subject' in itemsSubject && 'items' in (itemsSubject as any)._subject;
868
+ if (isComputed(itemsSubject) && itemsSubject.dependencies.size == 0) {
869
+ itemsSubject = signal(itemsSubject());
870
+ }
871
+ else if (!isSignal(itemsSubject)) {
872
+ itemsSubject = signal(itemsSubject);
873
+ }
318
874
 
319
- const addAt = (items: T[], insertIndex: number | string, keys?: string[]): Element[] => {
320
- return items.map((item, index) => {
321
- const key = keys ? keys[index] : (typeof insertIndex === 'number' ? insertIndex + index : insertIndex);
322
- const element = createElementFn(item, key);
323
- if (typeof insertIndex === 'number') {
324
- elements.splice(insertIndex + index, 0, element);
325
- } else {
326
- elements.push(element);
327
- }
328
- return element;
329
- });
330
- };
875
+ return defer(() => {
876
+ let elements: Element[] = [];
877
+ let elementMap = new Map<string | number, Element>();
878
+ let isFirstSubscription = true;
331
879
 
332
- const getInitialItems = () => {
333
- if (isArraySignal) {
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][];
880
+ const ensureElement = (itemResult: any): Element | null => {
881
+ if (!itemResult) return null;
882
+ if (isElement(itemResult)) return itemResult;
340
883
  return {
341
- items: entries.map(([_, value]) => value),
342
- keys: entries.map(([key]) => key)
884
+ tag: 'fragment',
885
+ props: { children: Array.isArray(itemResult) ? itemResult : [itemResult] },
886
+ componentInstance: {} as any,
887
+ propSubscriptions: [],
888
+ effectSubscriptions: [],
889
+ effectMounts: [],
890
+ effectUnmounts: [],
891
+ propObservables: {},
892
+ parent: null,
893
+ directives: {},
894
+ destroy() { destroyElement(this) },
895
+ allElements: new Subject(),
896
+ isFrozen: false
343
897
  };
344
898
  }
345
- };
346
899
 
347
- return defer(() => {
348
- const { items, keys } = getInitialItems();
349
- let initialItems = [...items];
350
- let initialKeys = keys ? [...keys] : undefined;
351
- let init = true;
352
- return (itemsSubject.observable as Observable<ArrayChange<T> | ObjectChange<T>>).pipe(
353
- map((event: ArrayChange<T> | ObjectChange<T>): FlowResult => {
354
- const { type, items } = event;
355
- const index = 'index' in event ? event.index : (event as ObjectChange<T>).key;
356
-
357
- if (init) {
358
- if (elements.length > 0) {
359
- return {
360
- elements: elements,
361
- fullElements: elements,
362
- };
363
- }
364
- const newElements = addAt(initialItems, 0, initialKeys);
365
- initialItems = [];
366
- initialKeys = undefined;
367
- init = false;
368
- return {
369
- elements: newElements,
370
- fullElements: elements,
371
- };
372
- } else if (type == "reset") {
373
- if (elements.length != 0) {
374
- elements.forEach((element) => {
375
- destroyElement(element);
376
- });
900
+ const isArraySignal = (signal: any): signal is WritableArraySignal<T[]> =>
901
+ Array.isArray(signal());
902
+
903
+ return new Observable<FlowResult>(subscriber => {
904
+ const subscription = isArraySignal(itemsSubject)
905
+ ? itemsSubject.observable.subscribe(change => {
906
+ if (isFirstSubscription) {
907
+ isFirstSubscription = false;
908
+ elements.forEach(el => el.destroy());
377
909
  elements = [];
910
+ elementMap.clear();
911
+
912
+ const items = itemsSubject();
913
+ if (items) {
914
+ items.forEach((item, index) => {
915
+ const element = ensureElement(createElementFn(item, index));
916
+ if (element) {
917
+ elements.push(element);
918
+ elementMap.set(index, element);
919
+ }
920
+ });
921
+ }
922
+ subscriber.next({
923
+ elements: [...elements]
924
+ });
925
+ return;
378
926
  }
379
- if (!isArraySignal) {
380
- const entries = Object.entries((itemsSubject as any)._subject.value.value) as [string, T][];
381
- const newElements = addAt(
382
- entries.map(([_, value]) => value),
383
- 0,
384
- entries.map(([key]) => key)
385
- );
386
- return {
387
- elements: newElements,
388
- fullElements: elements,
389
- };
390
- }
391
- const newElements = addAt(items as T[], 0);
392
- return {
393
- elements: newElements,
394
- fullElements: elements,
395
- };
396
- } else if (type == "add" && index != undefined) {
397
- const lastElement = typeof index === 'number' ? elements[index - 1] : elements[elements.length - 1];
398
- let newElements: Element[];
399
- if (!isArraySignal && typeof index === 'string') {
400
- // For object updates, create a single element with the new value
401
- const value = (event as ObjectChange<T>).value;
402
- if (value !== undefined) {
403
- newElements = [createElementFn(value, index)];
404
- elements.push(newElements[0]);
927
+
928
+ // Handle computed signals that emit array values directly (not ArrayChange objects)
929
+ // When a computed emits, `change` is the array itself, not an object with `type`
930
+ const isDirectArrayChange = Array.isArray(change) || (change && typeof change === 'object' && !('type' in change));
931
+
932
+ if (change.type === 'init' || change.type === 'reset' || isDirectArrayChange) {
933
+ elements.forEach(el => destroyElement(el));
934
+ elements = [];
935
+ elementMap.clear();
936
+
937
+ const items = itemsSubject();
938
+ if (items) {
939
+ items.forEach((item, index) => {
940
+ const element = ensureElement(createElementFn(item, index));
941
+ if (element) {
942
+ elements.push(element);
943
+ elementMap.set(index, element);
944
+ }
945
+ });
946
+ }
947
+ } else if (change.type === 'add' && change.index !== undefined) {
948
+ const newElements = change.items.map((item, i) => {
949
+ const element = ensureElement(createElementFn(item as T, change.index! + i));
950
+ if (element) {
951
+ elementMap.set(change.index! + i, element);
952
+ }
953
+ return element;
954
+ }).filter((el): el is Element => el !== null);
955
+
956
+ elements.splice(change.index, 0, ...newElements);
957
+ } else if (change.type === 'remove' && change.index !== undefined) {
958
+ const removed = elements.splice(change.index, 1);
959
+ removed.forEach(el => {
960
+ destroyElement(el)
961
+ elementMap.delete(change.index!);
962
+ });
963
+ } else if (change.type === 'update' && change.index !== undefined && change.items.length === 1) {
964
+ const index = change.index;
965
+ const newItem = change.items[0];
966
+
967
+ // Check if the previous item at this index was effectively undefined or non-existent
968
+ if (index >= elements.length || elements[index] === undefined || !elementMap.has(index)) {
969
+ // Treat as add operation
970
+ const newElement = ensureElement(createElementFn(newItem as T, index));
971
+ if (newElement) {
972
+ elements.splice(index, 0, newElement); // Insert at the correct index
973
+ elementMap.set(index, newElement);
974
+ // Adjust indices in elementMap for subsequent elements might be needed if map relied on exact indices
975
+ // This simple implementation assumes keys are stable or createElementFn handles context correctly
976
+ } else {
977
+ console.warn(`Element creation returned null for index ${index} during add-like update.`);
978
+ }
405
979
  } else {
406
- newElements = [];
980
+ // Treat as a standard update operation
981
+ const oldElement = elements[index];
982
+ destroyElement(oldElement)
983
+ const newElement = ensureElement(createElementFn(newItem as T, index));
984
+ if (newElement) {
985
+ elements[index] = newElement;
986
+ elementMap.set(index, newElement);
987
+ } else {
988
+ // Handle case where new element creation returns null
989
+ elements.splice(index, 1);
990
+ elementMap.delete(index);
991
+ }
407
992
  }
408
- } else {
409
- // For array updates, use addAt with the items array
410
- newElements = addAt(items as T[], index);
411
993
  }
412
- return {
413
- prev: lastElement,
414
- elements: newElements,
415
- fullElements: elements,
416
- };
417
- } else if (type == "remove") {
418
- if (!isArraySignal && typeof index === 'string') {
419
- // For object property deletion
420
- const elementIndex = elements.findIndex(el => {
421
- return el.props.text === index || el.props.key === index;
994
+
995
+ subscriber.next({
996
+ elements: [...elements] // Create a new array to ensure change detection
997
+ });
998
+ })
999
+ : (itemsSubject as WritableObjectSignal<T>).observable.subscribe(change => {
1000
+ const key = change.key as string | number
1001
+ if (isFirstSubscription) {
1002
+ isFirstSubscription = false;
1003
+ elements.forEach(el => destroyElement(el));
1004
+ elements = [];
1005
+ elementMap.clear();
1006
+
1007
+ const items = (itemsSubject as WritableObjectSignal<T>)();
1008
+ if (items) {
1009
+ Object.entries(items).forEach(([key, value]) => {
1010
+ const element = ensureElement(createElementFn(value, key));
1011
+ if (element) {
1012
+ elements.push(element);
1013
+ elementMap.set(key, element);
1014
+ }
1015
+ });
1016
+ }
1017
+ subscriber.next({
1018
+ elements: [...elements]
422
1019
  });
423
- if (elementIndex !== -1) {
424
- const currentElement = elements[elementIndex];
425
- destroyElement(currentElement);
426
- elements.splice(elementIndex, 1);
1020
+ return;
1021
+ }
1022
+
1023
+ if (change.type === 'init' || change.type === 'reset') {
1024
+ elements.forEach(el => destroyElement(el));
1025
+ elements = [];
1026
+ elementMap.clear();
1027
+
1028
+ const items = (itemsSubject as WritableObjectSignal<T>)();
1029
+ if (items) {
1030
+ Object.entries(items).forEach(([key, value]) => {
1031
+ const element = ensureElement(createElementFn(value, key));
1032
+ if (element) {
1033
+ elements.push(element);
1034
+ elementMap.set(key, element);
1035
+ }
1036
+ });
1037
+ }
1038
+ } else if (change.type === 'add' && change.key && change.value !== undefined) {
1039
+ const element = ensureElement(createElementFn(change.value as T, key));
1040
+ if (element) {
1041
+ elements.push(element);
1042
+ elementMap.set(key, element);
1043
+ }
1044
+ } else if (change.type === 'remove' && change.key) {
1045
+ const index = elements.findIndex(el => elementMap.get(key) === el);
1046
+ if (index !== -1) {
1047
+ const [removed] = elements.splice(index, 1);
1048
+ destroyElement(removed)
1049
+ elementMap.delete(key);
1050
+ }
1051
+ } else if (change.type === 'update' && change.key && change.value !== undefined) {
1052
+ const index = elements.findIndex(el => elementMap.get(key) === el);
1053
+ if (index !== -1) {
1054
+ const oldElement = elements[index];
1055
+ destroyElement(oldElement)
1056
+ const newElement = ensureElement(createElementFn(change.value as T, key));
1057
+ if (newElement) {
1058
+ elements[index] = newElement;
1059
+ elementMap.set(key, newElement);
1060
+ }
427
1061
  }
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
1062
  }
434
- return {
435
- elements: [],
436
- fullElements: elements,
437
- };
438
- }
439
- return {
440
- elements: [],
441
- fullElements: elements,
442
- };
443
- })
444
- );
445
- });
1063
+
1064
+ subscriber.next({
1065
+ elements: [...elements] // Create a new array to ensure change detection
1066
+ });
1067
+ });
1068
+
1069
+ return () => {
1070
+ subscription.unsubscribe();
1071
+ elements.forEach(el => destroyElement(el));
1072
+ };
1073
+ });
1074
+ }).pipe(shareReplay({ bufferSize: 1, refCount: true }));
446
1075
  }
447
1076
 
448
1077
  /**
449
- * Conditionally creates and destroys elements based on a condition signal.
1078
+ * Conditionally creates and destroys elements based on condition signals with support for else if and else.
450
1079
  *
451
- * @param {Signal<boolean> | boolean} condition - A signal or boolean that determines whether to create an element.
1080
+ * @description This function creates conditional rendering with support for multiple conditions (if/else if/else pattern).
1081
+ * It evaluates conditions in order and renders the first matching condition's element.
1082
+ * The function maintains full reactivity with signals and ensures proper cleanup of elements.
1083
+ *
1084
+ * @param {Signal<boolean> | boolean | (() => boolean)} condition - A signal, boolean, or function that determines whether to create an element.
452
1085
  * @param {Function} createElementFn - A function that returns an element or a promise that resolves to an element.
453
- * @returns {Observable} An observable that emits the created or destroyed element.
1086
+ * @param {...Array} additionalConditions - Additional conditions for else if and else cases.
1087
+ * Can be:
1088
+ * - A function for else case: `() => Element | Promise<Element>`
1089
+ * - An array for else if case: `[Signal<boolean> | boolean | (() => boolean), () => Element | Promise<Element>]`
1090
+ * @returns {Observable} An observable that emits the created element based on the matching condition.
1091
+ *
1092
+ * @example
1093
+ * ```typescript
1094
+ * // Simple if/else
1095
+ * cond(
1096
+ * signal(isVisible),
1097
+ * () => h(Container),
1098
+ * () => h(Text, { text: 'Hidden' }) // else
1099
+ * );
1100
+ *
1101
+ * // Multiple else if + else
1102
+ * cond(
1103
+ * signal(status === 'loading'),
1104
+ * () => h(LoadingSpinner),
1105
+ * [signal(status === 'error'), () => h(ErrorMessage)], // else if
1106
+ * [signal(status === 'success'), () => h(SuccessMessage)], // else if
1107
+ * () => h(DefaultMessage) // else
1108
+ * );
1109
+ * ```
454
1110
  */
455
1111
  export function cond(
456
- condition: Signal<boolean> | boolean,
457
- createElementFn: () => Element | Promise<Element>
1112
+ condition: Signal<boolean> | boolean | (() => boolean),
1113
+ createElementFn: () => Element | Promise<Element>,
1114
+ ...additionalConditions: Array<
1115
+ | (() => Element | Promise<Element>) // else final
1116
+ | [Signal<boolean> | boolean | (() => boolean), () => Element | Promise<Element>] // else if
1117
+ >
458
1118
  ): FlowObservable {
459
- let element: Element | null = null;
460
-
461
- if (isSignal(condition)) {
462
- const signalCondition = condition as WritableObjectSignal<boolean>;
463
- return new Observable<{elements: Element[], type?: "init" | "remove"}>(subscriber => {
464
- return signalCondition.observable.subscribe(bool => {
465
- if (bool) {
466
- let _el = createElementFn();
1119
+ let currentElement: Element | null = null;
1120
+ let currentConditionIndex = -1;
1121
+
1122
+ // Parse additional conditions
1123
+ const elseIfConditions: Array<{
1124
+ condition: Signal<boolean>;
1125
+ elementFn: () => Element | Promise<Element>;
1126
+ }> = [];
1127
+ let elseElementFn: (() => Element | Promise<Element>) | null = null;
1128
+
1129
+ // Convert function conditions to computed signals
1130
+ const convertConditionToSignal = (cond: Signal<boolean> | boolean | (() => boolean)): Signal<boolean> => {
1131
+ if (isSignal(cond)) {
1132
+ return cond as Signal<boolean>;
1133
+ } else if (typeof cond === 'function') {
1134
+ return computed(cond as () => boolean);
1135
+ } else {
1136
+ return signal(cond as boolean);
1137
+ }
1138
+ };
1139
+
1140
+ // Process additional conditions
1141
+ for (const param of additionalConditions) {
1142
+ if (Array.isArray(param)) {
1143
+ // else if case: [condition, elementFn]
1144
+ elseIfConditions.push({
1145
+ condition: convertConditionToSignal(param[0]),
1146
+ elementFn: param[1],
1147
+ });
1148
+ } else if (typeof param === 'function') {
1149
+ // else case: elementFn (should be the last one)
1150
+ elseElementFn = param;
1151
+ break; // Stop processing after else
1152
+ }
1153
+ }
1154
+
1155
+ // Collect all conditions with their element functions
1156
+ const allConditions = [
1157
+ { condition: convertConditionToSignal(condition), elementFn: createElementFn },
1158
+ ...elseIfConditions,
1159
+ ];
1160
+
1161
+ // All conditions are now signals, so we always use the reactive path
1162
+ return new Observable<{ elements: Element[], type?: "init" | "remove" }>(subscriber => {
1163
+ const subscriptions: Subscription[] = [];
1164
+
1165
+ const evaluateConditions = () => {
1166
+ // Find the first matching condition
1167
+ let matchingIndex = -1;
1168
+ for (let i = 0; i < allConditions.length; i++) {
1169
+ const condition = allConditions[i].condition;
1170
+ const conditionValue = condition();
1171
+
1172
+ if (conditionValue) {
1173
+ matchingIndex = i;
1174
+ break;
1175
+ }
1176
+ }
1177
+
1178
+ // If no condition matches and we have an else, use else
1179
+ const shouldUseElse = matchingIndex === -1 && elseElementFn;
1180
+ const newConditionIndex = shouldUseElse ? -2 : matchingIndex; // -2 for else, -1 for nothing
1181
+
1182
+ // Only update if the condition changed
1183
+ if (newConditionIndex !== currentConditionIndex) {
1184
+ // Destroy current element if it exists
1185
+ if (currentElement) {
1186
+ destroyElement(currentElement);
1187
+ currentElement = null;
1188
+ }
1189
+
1190
+ currentConditionIndex = newConditionIndex;
1191
+
1192
+ if (shouldUseElse) {
1193
+ // Render else element
1194
+ let _el = elseElementFn!();
467
1195
  if (isPromise(_el)) {
468
1196
  from(_el as Promise<Element>).subscribe(el => {
469
- element = el;
1197
+ currentElement = el;
470
1198
  subscriber.next({
471
1199
  type: "init",
472
1200
  elements: [el],
473
1201
  });
474
1202
  });
475
1203
  } else {
476
- element = _el as Element;
1204
+ currentElement = _el as Element;
477
1205
  subscriber.next({
478
1206
  type: "init",
479
- elements: [element],
1207
+ elements: [currentElement],
1208
+ });
1209
+ }
1210
+ } else if (matchingIndex >= 0) {
1211
+ // Render matching condition element
1212
+ let _el = allConditions[matchingIndex].elementFn();
1213
+ if (isPromise(_el)) {
1214
+ from(_el as Promise<Element>).subscribe(el => {
1215
+ currentElement = el;
1216
+ subscriber.next({
1217
+ type: "init",
1218
+ elements: [el],
1219
+ });
1220
+ });
1221
+ } else {
1222
+ currentElement = _el as Element;
1223
+ subscriber.next({
1224
+ type: "init",
1225
+ elements: [currentElement],
480
1226
  });
481
1227
  }
482
- } else if (element) {
483
- destroyElement(element);
484
- subscriber.next({
485
- elements: [],
486
- });
487
1228
  } else {
1229
+ // No matching condition and no else
488
1230
  subscriber.next({
489
1231
  elements: [],
490
1232
  });
491
1233
  }
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
1234
  }
506
- return of({
507
- type: "init",
508
- elements: [_el as Element],
509
- });
510
- }
511
- return of({
512
- elements: [],
1235
+ };
1236
+
1237
+ // Subscribe to all signal conditions
1238
+ allConditions.forEach(({ condition }) => {
1239
+ const signalCondition = condition as WritableObjectSignal<boolean>;
1240
+ subscriptions.push(
1241
+ signalCondition.observable.subscribe(() => {
1242
+ evaluateConditions();
1243
+ })
1244
+ );
513
1245
  });
514
- }
1246
+
1247
+ // Initial evaluation
1248
+ evaluateConditions();
1249
+
1250
+ // Return cleanup function
1251
+ return () => {
1252
+ subscriptions.forEach(sub => sub.unsubscribe());
1253
+ if (currentElement) {
1254
+ destroyElement(currentElement);
1255
+ }
1256
+ };
1257
+ }).pipe(share());
515
1258
  }