canvasengine 2.0.0-beta.9 → 2.0.0-rc.2

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