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