canvasengine 2.0.0-beta.12 → 2.0.0-beta.14

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.
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "canvasengine",
3
- "version": "2.0.0-beta.12",
3
+ "version": "2.0.0-beta.14",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "dependencies": {
8
8
  "@barvynkoa/particle-emitter": "^0.0.1",
9
- "@signe/reactive": "^1.1.0",
9
+ "@signe/reactive": "^2.3.3",
10
10
  "howler": "^2.2.4",
11
11
  "pixi-filters": "^6.0.5",
12
12
  "pixi-viewport": "^6.0.3",
@@ -1,35 +1,31 @@
1
- import { effect, Signal } from "@signe/reactive";
1
+ import { Effect, effect, Signal } from "@signe/reactive";
2
2
  import { Graphics as PixiGraphics } from "pixi.js";
3
3
  import { createComponent, registerComponent } from "../engine/reactive";
4
4
  import { DisplayObject } from "./DisplayObject";
5
5
  import { DisplayObjectProps } from "./types/DisplayObject";
6
6
  import { useProps } from "../hooks/useProps";
7
+ import { SignalOrPrimitive } from "./types";
7
8
 
8
9
  interface GraphicsProps extends DisplayObjectProps {
9
10
  draw?: (graphics: PixiGraphics) => void;
10
11
  }
11
12
 
12
13
  interface RectProps extends DisplayObjectProps {
13
- width: number;
14
- height: number;
15
- color: string;
14
+ color: SignalOrPrimitive<string>;
16
15
  }
17
16
 
18
17
  interface CircleProps extends DisplayObjectProps {
19
- radius: number;
20
- color: string;
18
+ radius: SignalOrPrimitive<number>;
19
+ color: SignalOrPrimitive<string>;
21
20
  }
22
21
 
23
22
  interface EllipseProps extends DisplayObjectProps {
24
- width: number;
25
- height: number;
26
- color: string;
23
+ color: SignalOrPrimitive<string>;
27
24
  }
28
25
 
29
26
  interface TriangleProps extends DisplayObjectProps {
30
- base: number;
31
- height: number;
32
- color: string;
27
+ base: SignalOrPrimitive<number>;
28
+ color: SignalOrPrimitive<string>;
33
29
  }
34
30
 
35
31
  interface SvgProps extends DisplayObjectProps {
@@ -37,15 +33,21 @@ interface SvgProps extends DisplayObjectProps {
37
33
  }
38
34
 
39
35
  class CanvasGraphics extends DisplayObject(PixiGraphics) {
36
+ clearEffect: Effect;
40
37
  onInit(props) {
41
38
  super.onInit(props);
42
39
  if (props.draw) {
43
- effect(() => {
40
+ this.clearEffect = effect(() => {
44
41
  this.clear();
45
42
  props.draw?.(this);
46
43
  });
47
44
  }
48
45
  }
46
+
47
+ onDestroy() {
48
+ this.clearEffect.subscription.unsubscribe();
49
+ super.onDestroy();
50
+ }
49
51
  }
50
52
 
51
53
  interface CanvasGraphics extends PixiGraphics {}
@@ -48,10 +48,7 @@ export class CanvasViewport extends DisplayObject(PixiViewport) {
48
48
  onInit(props) {
49
49
  super.onInit(props)
50
50
  for (let event of EVENTS) {
51
- const camelCaseEvent = event.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
52
- if (props[camelCaseEvent]) {
53
- this.on(event, props[camelCaseEvent])
54
- }
51
+ if (props[event]) this.on(event, props[event])
55
52
  }
56
53
  }
57
54
 
@@ -69,6 +66,7 @@ export class CanvasViewport extends DisplayObject(PixiViewport) {
69
66
  'wheel',
70
67
  this.input.wheelFunction
71
68
  );
69
+
72
70
  this.options.events = renderer.events
73
71
 
74
72
  this.tickSubscription = tick.observable.subscribe(({ value }) => {
@@ -97,13 +95,9 @@ export class CanvasViewport extends DisplayObject(PixiViewport) {
97
95
  if (props.worldHeight !== undefined) {
98
96
  this.worldHeight = props.worldHeight
99
97
  }
100
- // if (props.drag) {
101
- // if (props.drag === true) {
102
-
103
- // } else {
104
- // this.drag(props.drag)
105
- // }
106
- // }
98
+ if (props.drag) {
99
+ this.drag(props.drag)
100
+ }
107
101
  if (props.clamp) {
108
102
  this.clamp(props.clamp.value ?? props.clamp)
109
103
  }
@@ -1,5 +1,7 @@
1
1
  import * as PIXI from "pixi.js";
2
2
  import { SignalOrPrimitive } from ".";
3
+ import { DragProps } from "../../directives/Drag";
4
+ import { ViewportFollowProps } from "../../directives/ViewportFollow";
3
5
 
4
6
  export type FlexDirection = 'row' | 'column' | 'row-reverse' | 'column-reverse';
5
7
  export type JustifyContent = 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around';
@@ -37,6 +39,11 @@ export interface DisplayObjectProps {
37
39
  blendMode?: SignalOrPrimitive<PIXI.BLEND_MODES>;
38
40
  blur?: SignalOrPrimitive<number>;
39
41
 
42
+ // Directives
43
+ drag?: DragProps;
44
+ viewportFollow?: ViewportFollowProps;
45
+
46
+ // Events
40
47
  click?: PIXI.FederatedEventHandler;
41
48
  mousedown?: PIXI.FederatedEventHandler;
42
49
  mouseenter?: PIXI.FederatedEventHandler;
@@ -1,84 +1,302 @@
1
- import { effect, isSignal } from '@signe/reactive';
2
- import { Container, Rectangle } from 'pixi.js';
1
+ import { effect, isComputed, isSignal, signal } from '@signe/reactive';
2
+ import { Container, Rectangle, Point, FederatedPointerEvent } from 'pixi.js';
3
3
  import { Directive, registerDirective } from '../engine/directive';
4
4
  import { Element } from '../engine/reactive';
5
5
  import { snap } from 'popmotion';
6
6
  import { addContext } from '../hooks/addContext';
7
+ import { Subscription } from 'rxjs';
8
+ import { useProps } from '../hooks/useProps';
9
+ import { SignalOrPrimitive } from '../components/types';
10
+
11
+ export type DragProps = {
12
+ move?: (event: FederatedPointerEvent) => void;
13
+ start?: () => void;
14
+ end?: () => void;
15
+ snap?: SignalOrPrimitive<number>;
16
+ direction?: SignalOrPrimitive<'x' | 'y' | 'all'>;
17
+ viewport?: {
18
+ edgeThreshold?: SignalOrPrimitive<number>;
19
+ maxSpeed?: SignalOrPrimitive<number>;
20
+ };
21
+ }
7
22
 
8
23
  export class Drop extends Directive {
9
- onInit(element: Element<Container>) {}
24
+ private elementRef: Element<Container> | null = null;
25
+
26
+ onInit(element: Element<Container>) {
27
+ this.elementRef = element;
28
+ }
10
29
 
11
30
  onMount(element: Element<Container>) {
12
- addContext(element, 'drop', element)
31
+ addContext(element, 'drop', element);
13
32
  }
14
33
 
15
34
  onUpdate() {}
16
35
 
17
- onDestroy() {}
36
+ onDestroy() {
37
+ this.elementRef = null;
38
+ }
18
39
  }
19
40
 
20
41
  export class Drag extends Directive {
21
- onInit(element: Element<Container>) {}
42
+ private elementRef: Element<Container> | null = null;
43
+ private stageRef: Container | null = null;
44
+ private offsetInParent = new Point();
45
+ private isDragging = false;
46
+ private viewport: any | null = null;
47
+ private animationFrameId: number | null = null;
48
+ private lastPointerPosition: Point = new Point();
49
+
50
+ private onDragMoveHandler: (event: FederatedPointerEvent) => void = () => {};
51
+ private onDragEndHandler: () => void = () => {};
52
+ private onDragStartHandler: (event: FederatedPointerEvent) => void = () => {};
53
+
54
+ private subscriptions: Subscription[] = [];
55
+
56
+ onInit(element: Element<Container>) {
57
+ this.elementRef = element;
58
+ this.onDragMoveHandler = this.onDragMove.bind(this);
59
+ this.onDragEndHandler = this.onDragEnd.bind(this);
60
+ this.onDragStartHandler = this.onPointerDown.bind(this);
61
+ }
22
62
 
23
63
  onMount(element: Element<Container>) {
24
- const { rootElement, canvasSize } = element.props.context
25
- const { propObservables } = element
26
- const { drag } = element.props
27
- const instance = element.componentInstance
28
- const stage = rootElement.componentInstance
29
- instance.eventMode = 'static'
30
- stage.eventMode = 'static'
31
-
32
- const snapTo = snap(drag?.snap ?? 0);
33
-
34
- effect(() => {
35
- stage.hitArea = new Rectangle(0, 0, canvasSize().width, canvasSize().height)
36
- })
37
-
38
- let x = 0
39
- let y = 0
40
-
41
- const onDragMove = (event) => {
42
- drag.move?.(event)
43
- x += event.movementX
44
- y += event.movementY
45
- if (drag?.snap) {
46
- instance.position.x = snapTo(x)
47
- instance.position.y = snapTo(y)
48
- } else {
49
- instance.position.x = x
50
- instance.position.y = y
64
+ const { rootElement, canvasSize, viewport, tick } = element.props.context;
65
+ const instance = element.componentInstance;
66
+ const dragProps = this.dragProps;
67
+ const haveNotProps = Object.keys(dragProps).length === 0;
68
+
69
+ if (haveNotProps) {
70
+ this.onDestroy();
71
+ return;
72
+ }
73
+
74
+ if (!instance) return;
75
+ this.stageRef = rootElement.componentInstance;
76
+ if (!this.stageRef) return;
77
+ this.viewport = viewport;
78
+
79
+ instance.eventMode = 'static';
80
+ this.stageRef.eventMode = 'static';
81
+
82
+ const _effect = effect(() => {
83
+ if (this.stageRef) {
84
+ this.stageRef.hitArea = new Rectangle(0, 0, canvasSize().width, canvasSize().height);
85
+ }
86
+ });
87
+
88
+ instance.on('pointerdown', this.onDragStartHandler);
89
+ this.stageRef.on('pointerup', this.onDragEndHandler);
90
+ this.stageRef.on('pointerupoutside', this.onDragEndHandler);
91
+
92
+ this.subscriptions = [
93
+ tick.observable.subscribe(() => {
94
+ if (this.isDragging && this.viewport) {
95
+ this.updateViewportPosition(this.lastPointerPosition);
96
+ }
97
+ }),
98
+ _effect.subscription
99
+ ]
100
+ }
101
+
102
+ get dragProps() {
103
+ const drag = this.elementRef?.props.drag
104
+ const options = useProps(drag?.value ?? drag, {
105
+ snap: 0,
106
+ viewport: {},
107
+ direction: 'all'
108
+ });
109
+ options.viewport = useProps(options.viewport, {
110
+ edgeThreshold: 300,
111
+ maxSpeed: 40
112
+ });
113
+ return options;
114
+ }
115
+
116
+ get axis() {
117
+ const direction = this.dragProps.direction();
118
+ const axis = {
119
+ x: true,
120
+ y: true,
121
+ }
122
+ if (direction === 'x') {
123
+ axis.y = false;
124
+ }
125
+ if (direction === 'y') {
126
+ axis.x = false;
127
+ }
128
+ return axis;
129
+ }
130
+
131
+ /**
132
+ * Updates element position when dragging and starts continuous viewport movement
133
+ * @param event The pointer event that triggered the drag move
134
+ */
135
+ private onDragMove(event: FederatedPointerEvent) {
136
+ if (!this.isDragging || !this.elementRef?.componentInstance || !this.elementRef.componentInstance.parent) return;
137
+
138
+ const instance = this.elementRef.componentInstance;
139
+ const parent = instance.parent;
140
+ const dragProps = this.dragProps;
141
+ const propObservables = this.elementRef.propObservables;
142
+ const snapTo = snap(dragProps?.snap() ?? 0);
143
+
144
+ dragProps?.move?.(event);
145
+
146
+ const currentParentLocalPointer = parent.toLocal(event.global);
147
+
148
+ const newX = currentParentLocalPointer.x - this.offsetInParent.x;
149
+ const newY = currentParentLocalPointer.y - this.offsetInParent.y;
150
+
151
+ if (dragProps?.snap()) {
152
+ instance.position.x = snapTo(newX);
153
+ instance.position.y = snapTo(newY);
154
+ } else {
155
+ if (this.axis.x) instance.position.x = newX;
156
+ if (this.axis.y) instance.position.y = newY;
157
+ }
158
+
159
+ // Store the last pointer position for continuous viewport movement
160
+ this.lastPointerPosition.copyFrom(event.global);
161
+
162
+ const { x: xProp, y: yProp } = propObservables as any;
163
+
164
+ const updatePosition = (prop: any, value: number) => {
165
+ if (isComputed(prop)) {
166
+ prop.dependencies.forEach(dependency => {
167
+ dependency.set(value)
168
+ })
169
+ } else if (isSignal(prop)) {
170
+ prop.set(value)
51
171
  }
52
- const { x: xProp, y: yProp } = propObservables as any
53
- if (xProp !== undefined && isSignal(xProp)) {
54
- xProp.set(instance.position.x)
172
+ }
173
+
174
+ if (xProp !== undefined) updatePosition(xProp, instance.position.x)
175
+ if (yProp !== undefined) updatePosition(yProp, instance.position.y)
176
+ }
177
+
178
+ /**
179
+ * Moves the viewport if the dragged element is near screen edges
180
+ * @param globalPosition The global pointer position
181
+ */
182
+ private updateViewportPosition(globalPosition: Point) {
183
+ if (!this.viewport || !this.elementRef) return;
184
+
185
+ const dragProps = this.dragProps;
186
+ const edgeThreshold = dragProps?.viewport?.edgeThreshold(); // Distance from edge to trigger viewport movement
187
+ const maxSpeed = dragProps?.viewport?.maxSpeed(); // Maximum speed when element is at the very edge
188
+
189
+ // Calculate screen boundaries
190
+ const screenLeft = 0;
191
+ const screenRight = this.viewport.screenWidth;
192
+ const screenTop = 0;
193
+ const screenBottom = this.viewport.screenHeight;
194
+ const instance = this.elementRef.componentInstance;
195
+
196
+ // Calculate distances from element to screen edges
197
+ const distanceFromLeft = globalPosition.x - screenLeft;
198
+ const distanceFromRight = screenRight - globalPosition.x;
199
+ const distanceFromTop = globalPosition.y - screenTop;
200
+ const distanceFromBottom = screenBottom - globalPosition.y;
201
+
202
+ let moveX = 0;
203
+ let moveY = 0;
204
+
205
+ // Calculate horizontal movement with dynamic velocity
206
+ if (distanceFromLeft < edgeThreshold) {
207
+ // Velocity increases as distance decreases
208
+ // When distance = 0, velocity = maxSpeed
209
+ // When distance = threshold, velocity = 0
210
+ const velocity = maxSpeed * (1 - (distanceFromLeft / edgeThreshold));
211
+ moveX = -velocity;
212
+ } else if (distanceFromRight < edgeThreshold) {
213
+ const velocity = maxSpeed * (1 - (distanceFromRight / edgeThreshold));
214
+ moveX = velocity;
215
+ }
216
+
217
+ // Calculate vertical movement with dynamic velocity
218
+ if (distanceFromTop < edgeThreshold) {
219
+ const velocity = maxSpeed * (1 - (distanceFromTop / edgeThreshold));
220
+ moveY = -velocity;
221
+ } else if (distanceFromBottom < edgeThreshold) {
222
+ const velocity = maxSpeed * (1 - (distanceFromBottom / edgeThreshold));
223
+ moveY = velocity;
224
+ }
225
+
226
+ // Apply movement with velocity-based displacement
227
+ if (moveX !== 0 || moveY !== 0) {
228
+ const lastViewValue = this.viewport.center;
229
+ this.viewport.moveCenter(
230
+ this.viewport.center.x + moveX,
231
+ this.viewport.center.y + moveY
232
+ );
233
+ if (this.axis.x && lastViewValue.x !== this.viewport.center.x) {
234
+ instance.position.x += moveX;
55
235
  }
56
- if (yProp !== undefined && isSignal(yProp)) {
57
- yProp.set(instance.position.y)
236
+ if (this.axis.y && lastViewValue.y !== this.viewport.center.y) {
237
+ instance.position.y += moveY;
58
238
  }
59
239
  }
240
+ }
60
241
 
61
- const onDragEnd = () => {
62
- drag.end?.()
63
- stage.off('pointermove', onDragMove)
64
- console.log(rootElement.allElements)
65
- }
242
+ /**
243
+ * Handles drag end event and stops viewport movement
244
+ */
245
+ private onDragEnd() {
246
+ if (!this.isDragging || !this.elementRef) return;
66
247
 
67
- instance.on('pointerdown', () => {
68
- drag.start?.()
69
- stage.on('pointermove', onDragMove)
70
- });
248
+ const dragProps = this.dragProps;
249
+ this.isDragging = false;
250
+ dragProps?.end?.();
251
+
252
+ if (this.stageRef) {
253
+ this.stageRef.off('pointermove', this.onDragMoveHandler);
254
+ }
255
+ }
256
+
257
+ private onPointerDown(event: FederatedPointerEvent) {
258
+ if (!this.elementRef?.componentInstance || !this.stageRef || !this.elementRef.componentInstance.parent) return;
259
+
260
+ const instance = this.elementRef.componentInstance;
261
+ const parent = instance.parent;
262
+ const dragProps = this.dragProps;
71
263
 
72
- stage.on('pointerup', onDragEnd)
73
- stage.on('pointerupoutside', onDragEnd)
264
+ const parentLocalPointer = parent.toLocal(event.global);
265
+
266
+ this.offsetInParent.x = parentLocalPointer.x - instance.position.x;
267
+ this.offsetInParent.y = parentLocalPointer.y - instance.position.y;
268
+
269
+ this.isDragging = true;
270
+
271
+ // Store initial pointer position
272
+ this.lastPointerPosition.copyFrom(event.global);
273
+
274
+ dragProps?.start?.();
275
+ this.stageRef.on('pointermove', this.onDragMoveHandler);
74
276
  }
75
277
 
76
- onUpdate() {}
278
+ onUpdate(props) {
279
+ if (props.type && props.type === 'reset') {
280
+ this.onDestroy();
281
+ this.onMount(this.elementRef);
282
+ }
283
+ }
77
284
 
78
285
  onDestroy() {
79
-
286
+ this.subscriptions.forEach(subscription => subscription.unsubscribe());
287
+ const instance = this.elementRef?.componentInstance;
288
+ if (instance) {
289
+ instance.off('pointerdown', this.onDragStartHandler);
290
+ }
291
+ if (this.stageRef) {
292
+ this.stageRef.off('pointermove', this.onDragMoveHandler);
293
+ this.stageRef.off('pointerup', this.onDragEndHandler);
294
+ this.stageRef.off('pointerupoutside', this.onDragEndHandler);
295
+ }
296
+ this.stageRef = null;
297
+ this.viewport = null;
80
298
  }
81
299
  }
82
300
 
83
- // registerDirective('drag', Drag)
84
- // registerDirective('drop', Drop)
301
+ registerDirective('drag', Drag);
302
+ registerDirective('drop', Drop);
@@ -366,8 +366,10 @@ export class KeyboardControls extends Directive {
366
366
  };
367
367
 
368
368
  onInit(element: Element) {
369
+ const value = element.props.controls.value ?? element.props.controls
370
+ if (!value) return
369
371
  this.setupListeners();
370
- this.setInputs(element.props.controls.value)
372
+ this.setInputs(value)
371
373
  // The processing is outside the rendering loop because if the FPS are lower (or higher) then the sending to the server would be slower or faster. Here it is constant
372
374
  this.interval = setInterval(() => {
373
375
  this.preStep()
@@ -1,25 +1,53 @@
1
1
  import { ComponentInstance } from '../components/DisplayObject';
2
+ import { SignalOrPrimitive } from '../components/types';
2
3
  import { Directive, registerDirective } from '../engine/directive';
3
4
  import { Element } from '../engine/reactive';
4
5
  import { error } from '../engine/utils';
6
+ import { useProps } from '../hooks/useProps';
7
+
8
+ export type ViewportFollowProps = {
9
+ viewportFollow?: boolean | {
10
+ speed?: SignalOrPrimitive<number>;
11
+ acceleration?: SignalOrPrimitive<number>;
12
+ radius?: SignalOrPrimitive<number>;
13
+ };
14
+ }
5
15
 
6
16
  export class ViewportFollow extends Directive {
7
17
  onInit(element: Element<ComponentInstance>) {
8
18
 
9
19
  }
10
20
  onMount(element: Element) {
11
- const { viewportFollow } = element.props
21
+ this.onUpdate(element.props.viewportFollow, element)
22
+ }
23
+ onUpdate(viewportFollow: any, element: Element) {
12
24
  const { viewport } = element.props.context
13
25
  if (!viewport) {
14
26
  throw error('ViewportFollow directive requires a Viewport component to be mounted in the same context')
15
27
  }
16
- if (viewportFollow) viewport.follow(element.componentInstance)
17
- }
18
- onUpdate(props: any) {
19
-
28
+ if (viewportFollow) {
29
+ if (viewportFollow === true) {
30
+ viewport.follow(element.componentInstance)
31
+ } else {
32
+ const options = useProps(viewportFollow, {
33
+ speed: undefined,
34
+ acceleration: undefined,
35
+ radius: undefined
36
+ })
37
+ viewport.follow(element.componentInstance, {
38
+ speed: options.speed(),
39
+ acceleration: options.acceleration(),
40
+ radius: options.radius()
41
+ })
42
+ }
43
+ } else {
44
+ viewport.plugins.remove('follow')
45
+ }
20
46
  }
21
- onDestroy() {
22
-
47
+ onDestroy(element: Element) {
48
+ const { viewportFollow } = element.props
49
+ const { viewport } = element.props.context
50
+ if (viewportFollow) viewport.plugins.remove('follow')
23
51
  }
24
52
  }
25
53
 
@@ -3,10 +3,10 @@ import { Element } from "./reactive"
3
3
  export const directives: { [key: string]: any } = {}
4
4
 
5
5
  export abstract class Directive {
6
- abstract onDestroy();
6
+ abstract onDestroy(element: Element<any>);
7
7
  abstract onInit(element: Element<any>);
8
8
  abstract onMount(element: Element<any>);
9
- abstract onUpdate(props: any);
9
+ abstract onUpdate(props: any, element: Element<any>);
10
10
  }
11
11
 
12
12
  export function registerDirective(name: string, directive: any) {
@@ -1,4 +1,4 @@
1
- import { Signal, WritableArraySignal, WritableObjectSignal, isSignal } from "@signe/reactive";
1
+ import { ArrayChange, ObjectChange, Signal, WritableArraySignal, WritableObjectSignal, isComputed, isSignal, signal } from "@signe/reactive";
2
2
  import {
3
3
  Observable,
4
4
  Subject,
@@ -17,25 +17,6 @@ export interface Props {
17
17
  [key: string]: any;
18
18
  }
19
19
 
20
- export type ArrayChange<T> = {
21
- type: "add" | "remove" | "update" | "init" | "reset";
22
- index?: number;
23
- items: T[];
24
- };
25
-
26
- export type ObjectChange<T> = {
27
- type: "add" | "remove" | "update" | "init" | "reset";
28
- key?: string;
29
- value?: T;
30
- items: T[];
31
- };
32
-
33
- type ElementObservable<T> = Observable<
34
- (ArrayChange<T> | ObjectChange<T>) & {
35
- value: Element | Element[];
36
- }
37
- >;
38
-
39
20
  type NestedSignalObjects = {
40
21
  [Key in string]: NestedSignalObjects | Signal<any>;
41
22
  };
@@ -104,11 +85,11 @@ function destroyElement(element: Element | Element[]) {
104
85
  }
105
86
  element.propSubscriptions.forEach((sub) => sub.unsubscribe());
106
87
  element.effectSubscriptions.forEach((sub) => sub.unsubscribe());
88
+ element.effectUnmounts.forEach((fn) => fn?.());
107
89
  for (let name in element.directives) {
108
- element.directives[name].onDestroy?.();
90
+ element.directives[name].onDestroy?.(element);
109
91
  }
110
92
  element.componentInstance.onDestroy?.(element.parent as any);
111
- element.effectUnmounts.forEach((fn) => fn?.());
112
93
  }
113
94
 
114
95
  /**
@@ -165,7 +146,7 @@ export function createComponent(tag: string, props?: Props): Element {
165
146
  _value.observable.subscribe((value) => {
166
147
  _set(path, key, value);
167
148
  if (element.directives[key]) {
168
- element.directives[key].onUpdate?.(value);
149
+ element.directives[key].onUpdate?.(value, element);
169
150
  }
170
151
  if (key == "tick") {
171
152
  return
@@ -309,9 +290,17 @@ export function createComponent(tag: string, props?: Props): Element {
309
290
  * @returns {Observable} An observable that emits the list of created child elements.
310
291
  */
311
292
  export function loop<T>(
312
- itemsSubject: WritableArraySignal<T[]> | WritableObjectSignal<T>,
293
+ itemsSubject: any,
313
294
  createElementFn: (item: T, index: number | string) => Element | null
314
295
  ): FlowObservable {
296
+
297
+ if (isComputed(itemsSubject) && itemsSubject.dependencies.size == 0) {
298
+ itemsSubject = signal(itemsSubject());
299
+ }
300
+ else if (!isSignal(itemsSubject)) {
301
+ itemsSubject = signal(itemsSubject);
302
+ }
303
+
315
304
  return defer(() => {
316
305
  let elements: Element[] = [];
317
306
  let elementMap = new Map<string | number, Element>();
@@ -25,7 +25,7 @@ export const useProps = (props, defaults = {}): any => {
25
25
  }
26
26
  for (let key in defaults) {
27
27
  if (!(key in obj)) {
28
- obj[key] = signal(defaults[key])
28
+ obj[key] = isPrimitive(defaults[key]) ? signal(defaults[key]) : defaults[key]
29
29
  }
30
30
  }
31
31
  return obj