canvasengine 2.0.0-beta.4 → 2.0.0-beta.40

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