canvasengine 2.0.0-beta.3 → 2.0.0-beta.30

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 (130) hide show
  1. package/dist/DebugRenderer-DcvJLrrD.js +172 -0
  2. package/dist/DebugRenderer-DcvJLrrD.js.map +1 -0
  3. package/dist/components/Button.d.ts +136 -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/Container.d.ts +80 -0
  8. package/dist/components/Container.d.ts.map +1 -0
  9. package/dist/components/DOMContainer.d.ts +77 -0
  10. package/dist/components/DOMContainer.d.ts.map +1 -0
  11. package/dist/components/DOMElement.d.ts +44 -0
  12. package/dist/components/DOMElement.d.ts.map +1 -0
  13. package/dist/components/DisplayObject.d.ts +82 -0
  14. package/dist/components/DisplayObject.d.ts.map +1 -0
  15. package/dist/components/Graphic.d.ts +65 -0
  16. package/dist/components/Graphic.d.ts.map +1 -0
  17. package/dist/components/Mesh.d.ts +202 -0
  18. package/dist/components/Mesh.d.ts.map +1 -0
  19. package/dist/components/NineSliceSprite.d.ts +17 -0
  20. package/dist/components/NineSliceSprite.d.ts.map +1 -0
  21. package/dist/components/ParticleEmitter.d.ts +5 -0
  22. package/dist/components/ParticleEmitter.d.ts.map +1 -0
  23. package/dist/components/Scene.d.ts +2 -0
  24. package/dist/components/Scene.d.ts.map +1 -0
  25. package/dist/components/Sprite.d.ts +174 -0
  26. package/dist/components/Sprite.d.ts.map +1 -0
  27. package/dist/components/Text.d.ts +21 -0
  28. package/dist/components/Text.d.ts.map +1 -0
  29. package/dist/components/TilingSprite.d.ts +18 -0
  30. package/dist/components/TilingSprite.d.ts.map +1 -0
  31. package/dist/components/Video.d.ts +15 -0
  32. package/dist/components/Video.d.ts.map +1 -0
  33. package/dist/components/Viewport.d.ts +106 -0
  34. package/dist/components/Viewport.d.ts.map +1 -0
  35. package/dist/components/index.d.ts +17 -0
  36. package/dist/components/index.d.ts.map +1 -0
  37. package/dist/components/types/DisplayObject.d.ts +106 -0
  38. package/dist/components/types/DisplayObject.d.ts.map +1 -0
  39. package/dist/components/types/MouseEvent.d.ts +4 -0
  40. package/dist/components/types/MouseEvent.d.ts.map +1 -0
  41. package/dist/components/types/Spritesheet.d.ts +366 -0
  42. package/dist/components/types/Spritesheet.d.ts.map +1 -0
  43. package/dist/components/types/index.d.ts +5 -0
  44. package/dist/components/types/index.d.ts.map +1 -0
  45. package/dist/directives/Drag.d.ts +70 -0
  46. package/dist/directives/Drag.d.ts.map +1 -0
  47. package/dist/directives/KeyboardControls.d.ts +530 -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/Sound.d.ts +26 -0
  52. package/dist/directives/Sound.d.ts.map +1 -0
  53. package/dist/directives/Transition.d.ts +11 -0
  54. package/dist/directives/Transition.d.ts.map +1 -0
  55. package/dist/directives/ViewportCull.d.ts +12 -0
  56. package/dist/directives/ViewportCull.d.ts.map +1 -0
  57. package/dist/directives/ViewportFollow.d.ts +19 -0
  58. package/dist/directives/ViewportFollow.d.ts.map +1 -0
  59. package/dist/directives/index.d.ts +2 -0
  60. package/dist/directives/index.d.ts.map +1 -0
  61. package/dist/engine/animation.d.ts +59 -0
  62. package/dist/engine/animation.d.ts.map +1 -0
  63. package/dist/engine/bootstrap.d.ts +16 -0
  64. package/dist/engine/bootstrap.d.ts.map +1 -0
  65. package/dist/engine/directive.d.ts +14 -0
  66. package/dist/engine/directive.d.ts.map +1 -0
  67. package/dist/engine/reactive.d.ts +95 -0
  68. package/dist/engine/reactive.d.ts.map +1 -0
  69. package/dist/engine/signal.d.ts +72 -0
  70. package/dist/engine/signal.d.ts.map +1 -0
  71. package/dist/engine/trigger.d.ts +51 -0
  72. package/dist/engine/trigger.d.ts.map +1 -0
  73. package/dist/engine/utils.d.ts +90 -0
  74. package/dist/engine/utils.d.ts.map +1 -0
  75. package/dist/hooks/addContext.d.ts +2 -0
  76. package/dist/hooks/addContext.d.ts.map +1 -0
  77. package/dist/hooks/useProps.d.ts +42 -0
  78. package/dist/hooks/useProps.d.ts.map +1 -0
  79. package/dist/hooks/useRef.d.ts +5 -0
  80. package/dist/hooks/useRef.d.ts.map +1 -0
  81. package/dist/index-C-iY-lCt.js +11080 -0
  82. package/dist/index-C-iY-lCt.js.map +1 -0
  83. package/dist/index.d.ts +15 -919
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.global.js +29 -0
  86. package/dist/index.global.js.map +1 -0
  87. package/dist/index.js +63 -2950
  88. package/dist/index.js.map +1 -1
  89. package/dist/utils/Ease.d.ts +17 -0
  90. package/dist/utils/Ease.d.ts.map +1 -0
  91. package/dist/utils/RadialGradient.d.ts +58 -0
  92. package/dist/utils/RadialGradient.d.ts.map +1 -0
  93. package/dist/utils/functions.d.ts +2 -0
  94. package/dist/utils/functions.d.ts.map +1 -0
  95. package/index.d.ts +4 -0
  96. package/package.json +12 -7
  97. package/src/components/Button.ts +269 -0
  98. package/src/components/Canvas.ts +53 -45
  99. package/src/components/Container.ts +2 -2
  100. package/src/components/DOMContainer.ts +123 -0
  101. package/src/components/DOMElement.ts +421 -0
  102. package/src/components/DisplayObject.ts +283 -190
  103. package/src/components/Graphic.ts +200 -34
  104. package/src/components/Mesh.ts +222 -0
  105. package/src/components/NineSliceSprite.ts +4 -1
  106. package/src/components/ParticleEmitter.ts +12 -8
  107. package/src/components/Sprite.ts +92 -22
  108. package/src/components/Text.ts +34 -14
  109. package/src/components/Video.ts +110 -0
  110. package/src/components/Viewport.ts +59 -43
  111. package/src/components/index.ts +7 -2
  112. package/src/components/types/DisplayObject.ts +30 -0
  113. package/src/directives/Drag.ts +357 -52
  114. package/src/directives/KeyboardControls.ts +3 -1
  115. package/src/directives/Sound.ts +94 -31
  116. package/src/directives/ViewportFollow.ts +35 -7
  117. package/src/engine/animation.ts +41 -5
  118. package/src/engine/bootstrap.ts +23 -3
  119. package/src/engine/directive.ts +2 -2
  120. package/src/engine/reactive.ts +472 -172
  121. package/src/engine/signal.ts +18 -2
  122. package/src/engine/trigger.ts +65 -9
  123. package/src/engine/utils.ts +97 -9
  124. package/src/hooks/useProps.ts +1 -1
  125. package/src/index.ts +4 -1
  126. package/src/utils/RadialGradient.ts +29 -0
  127. package/src/utils/functions.ts +7 -0
  128. package/testing/index.ts +12 -0
  129. package/tsconfig.json +17 -0
  130. package/vite.config.ts +39 -0
@@ -1,4 +1,4 @@
1
- import { Signal, WritableArraySignal, isSignal } from "@signe/reactive";
1
+ import { ArrayChange, ObjectChange, Signal, WritableArraySignal, WritableObjectSignal, isComputed, isSignal, signal, computed } from "@signe/reactive";
2
2
  import {
3
3
  Observable,
4
4
  Subject,
@@ -7,7 +7,13 @@ import {
7
7
  from,
8
8
  map,
9
9
  of,
10
+ share,
10
11
  switchMap,
12
+ debounceTime,
13
+ distinctUntilChanged,
14
+ bufferTime,
15
+ filter,
16
+ throttleTime,
11
17
  } from "rxjs";
12
18
  import { ComponentInstance } from "../components/DisplayObject";
13
19
  import { Directive, applyDirective } from "./directive";
@@ -17,18 +23,6 @@ export interface Props {
17
23
  [key: string]: any;
18
24
  }
19
25
 
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
26
  type NestedSignalObjects = {
33
27
  [Key in string]: NestedSignalObjects | Signal<any>;
34
28
  };
@@ -53,10 +47,13 @@ export interface Element<T = ComponentInstance> {
53
47
  allElements: Subject<void>;
54
48
  }
55
49
 
56
- type FlowObservable = Observable<{
50
+ type FlowResult = {
57
51
  elements: Element[];
58
52
  prev?: Element;
59
- }>;
53
+ fullElements?: Element[];
54
+ };
55
+
56
+ type FlowObservable = Observable<FlowResult>;
60
57
 
61
58
  const components: { [key: string]: any } = {};
62
59
 
@@ -92,13 +89,26 @@ function destroyElement(element: Element | Element[]) {
92
89
  if (!element) {
93
90
  return;
94
91
  }
95
- element.propSubscriptions.forEach((sub) => sub.unsubscribe());
96
- element.effectSubscriptions.forEach((sub) => sub.unsubscribe());
92
+ if (element.props?.children) {
93
+ for (let child of element.props.children) {
94
+ destroyElement(child)
95
+ }
96
+ }
97
97
  for (let name in element.directives) {
98
- element.directives[name].onDestroy?.();
98
+ element.directives[name].onDestroy?.(element);
99
+ }
100
+ if (element.componentInstance && element.componentInstance.onDestroy) {
101
+ element.componentInstance.onDestroy(element.parent as any, () => {
102
+ element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
103
+ element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
104
+ element.effectUnmounts?.forEach((fn) => fn?.());
105
+ });
106
+ } else {
107
+ // If componentInstance is undefined or doesn't have onDestroy, still clean up subscriptions
108
+ element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
109
+ element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
110
+ element.effectUnmounts?.forEach((fn) => fn?.());
99
111
  }
100
- element.componentInstance.onDestroy?.(element.parent as any);
101
- element.effectUnmounts.forEach((fn) => fn?.());
102
112
  }
103
113
 
104
114
  /**
@@ -155,7 +165,7 @@ export function createComponent(tag: string, props?: Props): Element {
155
165
  _value.observable.subscribe((value) => {
156
166
  _set(path, key, value);
157
167
  if (element.directives[key]) {
158
- element.directives[key].onUpdate?.(value);
168
+ element.directives[key].onUpdate?.(value, element);
159
169
  }
160
170
  if (key == "tick") {
161
171
  return
@@ -182,9 +192,24 @@ export function createComponent(tag: string, props?: Props): Element {
182
192
  }
183
193
 
184
194
  instance.onInit?.(element.props);
185
- instance.onUpdate?.(element.props);
186
195
 
187
- const onMount = (parent: Element, element: Element, index?: number) => {
196
+ const elementsListen = new Subject<any>()
197
+
198
+ if (props?.isRoot) {
199
+ element.allElements = elementsListen
200
+ element.props.context.rootElement = element;
201
+ element.componentInstance.onMount?.(element);
202
+ propagateContext(element);
203
+ }
204
+
205
+ if (props) {
206
+ for (let key in props) {
207
+ const directive = applyDirective(element, key);
208
+ if (directive) element.directives[key] = directive;
209
+ }
210
+ }
211
+
212
+ function onMount(parent: Element, element: Element, index?: number) {
188
213
  element.props.context = parent.props.context;
189
214
  element.parent = parent;
190
215
  element.componentInstance.onMount?.(element, index);
@@ -196,68 +221,79 @@ export function createComponent(tag: string, props?: Props): Element {
196
221
  });
197
222
  };
198
223
 
199
- const elementsListen = new Subject<any>()
200
-
201
- if (props?.isRoot) {
202
- // propagate recrusively context in all children
203
- const propagateContext = async (element) => {
204
- if (!element.props.children) {
205
- return;
224
+ async function propagateContext(element) {
225
+ if (element.props.attach) {
226
+ const isReactiveAttach = isSignal(element.propObservables?.attach)
227
+ if (!isReactiveAttach) {
228
+ element.props.children.push(element.props.attach)
206
229
  }
207
- for (let child of element.props.children) {
208
- if (!child) continue;
209
- if (isPromise(child)) {
210
- child = await child;
211
- }
212
- if (child instanceof Observable) {
213
- child.subscribe(
214
- ({
215
- elements: comp,
216
- prev,
217
- }: {
218
- elements: Element[];
219
- 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
- });
230
+ else {
231
+ await new Promise((resolve) => {
232
+ let lastElement = null
233
+ element.propSubscriptions.push(element.propObservables.attach.observable.subscribe(async (args) => {
234
+ const value = args?.value ?? args
235
+ if (!value) {
236
+ throw new Error(`attach in ${element.tag} is undefined or null, add a component`)
240
237
  }
238
+ if (lastElement) {
239
+ destroyElement(lastElement)
240
+ }
241
+ lastElement = value
242
+ await createElement(element, value)
243
+ resolve(undefined)
244
+ }))
245
+ })
246
+ }
247
+ }
248
+ if (!element.props.children) {
249
+ return;
250
+ }
251
+ for (let child of element.props.children) {
252
+ if (!child) continue;
253
+ await createElement(element, child)
254
+ }
255
+ };
256
+
257
+ async function createElement(parent: Element, child: Element) {
258
+ if (isPromise(child)) {
259
+ child = await child;
260
+ }
261
+ if (child instanceof Observable) {
262
+ child.subscribe(
263
+ ({
264
+ elements: comp,
265
+ prev,
266
+ }: {
267
+ elements: Element[];
268
+ prev?: Element;
269
+ }) => {
270
+ // if prev, insert element after this
271
+ const components = comp.filter((c) => c !== null);
272
+ if (prev) {
273
+ components.forEach((c) => {
274
+ const index = parent.props.children.indexOf(prev.props.key);
275
+ onMount(parent, c, index + 1);
276
+ propagateContext(c);
277
+ });
278
+ return;
279
+ }
280
+ components.forEach((component) => {
281
+ if (!Array.isArray(component)) {
282
+ onMount(parent, component);
283
+ propagateContext(component);
284
+ } else {
285
+ component.forEach((comp) => {
286
+ onMount(parent, comp);
287
+ propagateContext(comp);
241
288
  });
242
- elementsListen.next(undefined)
243
289
  }
244
- );
245
- } else {
246
- onMount(element, child);
247
- await propagateContext(child);
290
+ });
291
+ elementsListen.next(undefined)
248
292
  }
249
- }
250
- };
251
- element.allElements = elementsListen
252
- element.props.context.rootElement = element;
253
- element.componentInstance.onMount?.(element);
254
- propagateContext(element);
255
- }
256
-
257
- if (props) {
258
- for (let key in props) {
259
- const directive = applyDirective(element, key);
260
- if (directive) element.directives[key] = directive;
293
+ );
294
+ } else {
295
+ onMount(parent, child);
296
+ await propagateContext(child);
261
297
  }
262
298
  }
263
299
 
@@ -266,114 +302,378 @@ export function createComponent(tag: string, props?: Props): Element {
266
302
  }
267
303
 
268
304
  /**
269
- * Observes a BehaviorSubject containing an array of items and dynamically creates child elements for each item.
305
+ * Observes a BehaviorSubject containing an array or object of items and dynamically creates child elements for each item.
270
306
  *
271
- * @param {BehaviorSubject<Array>} itemsSubject - A BehaviorSubject that emits an array of items.
307
+ * @param {WritableArraySignal<T> | WritableObjectSignal<T>} itemsSubject - A signal that emits an array or object of items.
272
308
  * @param {Function} createElementFn - A function that takes an item and returns an element representation.
273
309
  * @returns {Observable} An observable that emits the list of created child elements.
274
310
  */
275
- export function loop<T = any>(
276
- itemsSubject: WritableArraySignal<T>,
277
- createElementFn: (item: any, index: number) => Element | Promise<Element>
311
+ export function loop<T>(
312
+ itemsSubject: any,
313
+ createElementFn: (item: T, index: number | string) => Element | null
278
314
  ): FlowObservable {
279
- let elements: Element[] = [];
280
315
 
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
- };
316
+ if (isComputed(itemsSubject) && itemsSubject.dependencies.size == 0) {
317
+ itemsSubject = signal(itemsSubject());
318
+ }
319
+ else if (!isSignal(itemsSubject)) {
320
+ itemsSubject = signal(itemsSubject);
321
+ }
288
322
 
289
323
  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
- };
301
- }
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);
324
+ let elements: Element[] = [];
325
+ let elementMap = new Map<string | number, Element>();
326
+ let isFirstSubscription = true;
327
+
328
+ const isArraySignal = (signal: any): signal is WritableArraySignal<T[]> =>
329
+ Array.isArray(signal());
330
+
331
+ return new Observable<FlowResult>(subscriber => {
332
+ const subscription = isArraySignal(itemsSubject)
333
+ ? itemsSubject.observable.subscribe(change => {
334
+ if (isFirstSubscription) {
335
+ isFirstSubscription = false;
336
+ elements.forEach(el => el.destroy());
337
+ elements = [];
338
+ elementMap.clear();
339
+
340
+ const items = itemsSubject();
341
+ if (items) {
342
+ items.forEach((item, index) => {
343
+ const element = createElementFn(item, index);
344
+ if (element) {
345
+ elements.push(element);
346
+ elementMap.set(index, element);
347
+ }
348
+ });
349
+ }
350
+ subscriber.next({
351
+ elements: [...elements]
352
+ });
353
+ return;
354
+ }
355
+
356
+ if (change.type === 'init' || change.type === 'reset') {
357
+ elements.forEach(el => destroyElement(el));
358
+ elements = [];
359
+ elementMap.clear();
360
+
361
+ const items = itemsSubject();
362
+ if (items) {
363
+ items.forEach((item, index) => {
364
+ const element = createElementFn(item, index);
365
+ if (element) {
366
+ elements.push(element);
367
+ elementMap.set(index, element);
368
+ }
369
+ });
370
+ }
371
+ } else if (change.type === 'add' && change.index !== undefined) {
372
+ const newElements = change.items.map((item, i) => {
373
+ const element = createElementFn(item as T, change.index! + i);
374
+ if (element) {
375
+ elementMap.set(change.index! + i, element);
376
+ }
377
+ return element;
378
+ }).filter((el): el is Element => el !== null);
379
+
380
+ elements.splice(change.index, 0, ...newElements);
381
+ } else if (change.type === 'remove' && change.index !== undefined) {
382
+ const removed = elements.splice(change.index, 1);
383
+ removed.forEach(el => {
384
+ destroyElement(el)
385
+ elementMap.delete(change.index!);
386
+ });
387
+ } else if (change.type === 'update' && change.index !== undefined && change.items.length === 1) {
388
+ const index = change.index;
389
+ const newItem = change.items[0];
390
+
391
+ // Check if the previous item at this index was effectively undefined or non-existent
392
+ if (index >= elements.length || elements[index] === undefined || !elementMap.has(index)) {
393
+ // Treat as add operation
394
+ const newElement = createElementFn(newItem as T, index);
395
+ if (newElement) {
396
+ elements.splice(index, 0, newElement); // Insert at the correct index
397
+ elementMap.set(index, newElement);
398
+ // Adjust indices in elementMap for subsequent elements might be needed if map relied on exact indices
399
+ // This simple implementation assumes keys are stable or createElementFn handles context correctly
400
+ } else {
401
+ console.warn(`Element creation returned null for index ${index} during add-like update.`);
402
+ }
403
+ } else {
404
+ // Treat as a standard update operation
405
+ const oldElement = elements[index];
406
+ destroyElement(oldElement)
407
+ const newElement = createElementFn(newItem as T, index);
408
+ if (newElement) {
409
+ elements[index] = newElement;
410
+ elementMap.set(index, newElement);
411
+ } else {
412
+ // Handle case where new element creation returns null
413
+ elements.splice(index, 1);
414
+ elementMap.delete(index);
415
+ }
416
+ }
417
+ }
418
+
419
+ subscriber.next({
420
+ elements: [...elements] // Create a new array to ensure change detection
313
421
  });
314
- elements = [];
315
- }
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
- );
422
+ })
423
+ : (itemsSubject as WritableObjectSignal<T>).observable.subscribe(change => {
424
+ const key = change.key as string | number
425
+ if (isFirstSubscription) {
426
+ isFirstSubscription = false;
427
+ elements.forEach(el => destroyElement(el));
428
+ elements = [];
429
+ elementMap.clear();
430
+
431
+ const items = (itemsSubject as WritableObjectSignal<T>)();
432
+ if (items) {
433
+ Object.entries(items).forEach(([key, value]) => {
434
+ const element = createElementFn(value, key);
435
+ if (element) {
436
+ elements.push(element);
437
+ elementMap.set(key, element);
438
+ }
439
+ });
440
+ }
441
+ subscriber.next({
442
+ elements: [...elements]
443
+ });
444
+ return;
445
+ }
446
+
447
+ if (change.type === 'init' || change.type === 'reset') {
448
+ elements.forEach(el => destroyElement(el));
449
+ elements = [];
450
+ elementMap.clear();
451
+
452
+ const items = (itemsSubject as WritableObjectSignal<T>)();
453
+ if (items) {
454
+ Object.entries(items).forEach(([key, value]) => {
455
+ const element = createElementFn(value, key);
456
+ if (element) {
457
+ elements.push(element);
458
+ elementMap.set(key, element);
459
+ }
460
+ });
461
+ }
462
+ } else if (change.type === 'add' && change.key && change.value !== undefined) {
463
+ const element = createElementFn(change.value as T, key);
464
+ if (element) {
465
+ elements.push(element);
466
+ elementMap.set(key, element);
467
+ }
468
+ } else if (change.type === 'remove' && change.key) {
469
+ const index = elements.findIndex(el => elementMap.get(key) === el);
470
+ if (index !== -1) {
471
+ const [removed] = elements.splice(index, 1);
472
+ destroyElement(removed)
473
+ elementMap.delete(key);
474
+ }
475
+ } else if (change.type === 'update' && change.key && change.value !== undefined) {
476
+ const index = elements.findIndex(el => elementMap.get(key) === el);
477
+ if (index !== -1) {
478
+ const oldElement = elements[index];
479
+ destroyElement(oldElement)
480
+ const newElement = createElementFn(change.value as T, key);
481
+ if (newElement) {
482
+ elements[index] = newElement;
483
+ elementMap.set(key, newElement);
484
+ }
485
+ }
486
+ }
487
+
488
+ subscriber.next({
489
+ elements: [...elements] // Create a new array to ensure change detection
490
+ });
491
+ });
492
+
493
+ return subscription;
494
+ });
343
495
  });
344
496
  }
345
497
 
498
+ /**
499
+ * Conditionally creates and destroys elements based on condition signals with support for else if and else.
500
+ *
501
+ * @description This function creates conditional rendering with support for multiple conditions (if/else if/else pattern).
502
+ * It evaluates conditions in order and renders the first matching condition's element.
503
+ * The function maintains full reactivity with signals and ensures proper cleanup of elements.
504
+ *
505
+ * @param {Signal<boolean> | boolean | (() => boolean)} condition - A signal, boolean, or function that determines whether to create an element.
506
+ * @param {Function} createElementFn - A function that returns an element or a promise that resolves to an element.
507
+ * @param {...Array} additionalConditions - Additional conditions for else if and else cases.
508
+ * Can be:
509
+ * - A function for else case: `() => Element | Promise<Element>`
510
+ * - An array for else if case: `[Signal<boolean> | boolean | (() => boolean), () => Element | Promise<Element>]`
511
+ * @returns {Observable} An observable that emits the created element based on the matching condition.
512
+ *
513
+ * @example
514
+ * ```typescript
515
+ * // Simple if/else
516
+ * cond(
517
+ * signal(isVisible),
518
+ * () => h(Container),
519
+ * () => h(Text, { text: 'Hidden' }) // else
520
+ * );
521
+ *
522
+ * // Multiple else if + else
523
+ * cond(
524
+ * signal(status === 'loading'),
525
+ * () => h(LoadingSpinner),
526
+ * [signal(status === 'error'), () => h(ErrorMessage)], // else if
527
+ * [signal(status === 'success'), () => h(SuccessMessage)], // else if
528
+ * () => h(DefaultMessage) // else
529
+ * );
530
+ * ```
531
+ */
346
532
  export function cond(
347
- condition: Signal,
348
- createElementFn: () => Element | Promise<Element>
533
+ condition: Signal<boolean> | boolean | (() => boolean),
534
+ createElementFn: () => Element | Promise<Element>,
535
+ ...additionalConditions: Array<
536
+ | (() => Element | Promise<Element>) // else final
537
+ | [Signal<boolean> | boolean | (() => boolean), () => Element | Promise<Element>] // else if
538
+ >
349
539
  ): 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 {
540
+ let currentElement: Element | null = null;
541
+ let currentConditionIndex = -1;
542
+
543
+ // Parse additional conditions
544
+ const elseIfConditions: Array<{
545
+ condition: Signal<boolean>;
546
+ elementFn: () => Element | Promise<Element>;
547
+ }> = [];
548
+ let elseElementFn: (() => Element | Promise<Element>) | null = null;
549
+
550
+ // Convert function conditions to computed signals
551
+ const convertConditionToSignal = (cond: Signal<boolean> | boolean | (() => boolean)): Signal<boolean> => {
552
+ if (isSignal(cond)) {
553
+ return cond as Signal<boolean>;
554
+ } else if (typeof cond === 'function') {
555
+ return computed(cond as () => boolean);
556
+ } else {
557
+ return signal(cond as boolean);
558
+ }
559
+ };
560
+
561
+ // Process additional conditions
562
+ for (const param of additionalConditions) {
563
+ if (Array.isArray(param)) {
564
+ // else if case: [condition, elementFn]
565
+ elseIfConditions.push({
566
+ condition: convertConditionToSignal(param[0]),
567
+ elementFn: param[1],
568
+ });
569
+ } else if (typeof param === 'function') {
570
+ // else case: elementFn (should be the last one)
571
+ elseElementFn = param;
572
+ break; // Stop processing after else
573
+ }
574
+ }
575
+
576
+ // Collect all conditions with their element functions
577
+ const allConditions = [
578
+ { condition: convertConditionToSignal(condition), elementFn: createElementFn },
579
+ ...elseIfConditions,
580
+ ];
581
+
582
+ // All conditions are now signals, so we always use the reactive path
583
+ return new Observable<{elements: Element[], type?: "init" | "remove"}>(subscriber => {
584
+ const subscriptions: Subscription[] = [];
585
+
586
+ const evaluateConditions = () => {
587
+ // Find the first matching condition
588
+ let matchingIndex = -1;
589
+ for (let i = 0; i < allConditions.length; i++) {
590
+ const condition = allConditions[i].condition;
591
+ const conditionValue = condition();
592
+
593
+ if (conditionValue) {
594
+ matchingIndex = i;
595
+ break;
596
+ }
597
+ }
598
+
599
+ // If no condition matches and we have an else, use else
600
+ const shouldUseElse = matchingIndex === -1 && elseElementFn;
601
+ const newConditionIndex = shouldUseElse ? -2 : matchingIndex; // -2 for else, -1 for nothing
602
+
603
+ // Only update if the condition changed
604
+ if (newConditionIndex !== currentConditionIndex) {
605
+ // Destroy current element if it exists
606
+ if (currentElement) {
607
+ destroyElement(currentElement);
608
+ currentElement = null;
609
+ }
610
+
611
+ currentConditionIndex = newConditionIndex;
612
+
613
+ if (shouldUseElse) {
614
+ // Render else element
615
+ let _el = elseElementFn!();
616
+ if (isPromise(_el)) {
617
+ from(_el as Promise<Element>).subscribe(el => {
618
+ currentElement = el;
619
+ subscriber.next({
360
620
  type: "init",
361
621
  elements: [el],
362
- };
363
- })
364
- );
622
+ });
623
+ });
624
+ } else {
625
+ currentElement = _el as Element;
626
+ subscriber.next({
627
+ type: "init",
628
+ elements: [currentElement],
629
+ });
630
+ }
631
+ } else if (matchingIndex >= 0) {
632
+ // Render matching condition element
633
+ let _el = allConditions[matchingIndex].elementFn();
634
+ if (isPromise(_el)) {
635
+ from(_el as Promise<Element>).subscribe(el => {
636
+ currentElement = el;
637
+ subscriber.next({
638
+ type: "init",
639
+ elements: [el],
640
+ });
641
+ });
642
+ } else {
643
+ currentElement = _el as Element;
644
+ subscriber.next({
645
+ type: "init",
646
+ elements: [currentElement],
647
+ });
648
+ }
649
+ } else {
650
+ // No matching condition and no else
651
+ subscriber.next({
652
+ elements: [],
653
+ });
365
654
  }
366
- element = _el as Element;
367
- return of({
368
- type: "init",
369
- elements: [element],
370
- });
371
- } else if (element) {
372
- destroyElement(element);
373
655
  }
374
- return of({
375
- elements: [],
376
- });
377
- })
378
- );
656
+ };
657
+
658
+ // Subscribe to all signal conditions
659
+ allConditions.forEach(({ condition }) => {
660
+ const signalCondition = condition as WritableObjectSignal<boolean>;
661
+ subscriptions.push(
662
+ signalCondition.observable.subscribe(() => {
663
+ evaluateConditions();
664
+ })
665
+ );
666
+ });
667
+
668
+ // Initial evaluation
669
+ evaluateConditions();
670
+
671
+ // Return cleanup function
672
+ return () => {
673
+ subscriptions.forEach(sub => sub.unsubscribe());
674
+ if (currentElement) {
675
+ destroyElement(currentElement);
676
+ }
677
+ };
678
+ }).pipe(share());
379
679
  }