canvasengine 2.0.0-rc.1 → 2.0.0-rc.3

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 (40) hide show
  1. package/dist/components/Container.d.ts +25 -0
  2. package/dist/components/Container.d.ts.map +1 -0
  3. package/dist/components/DOMContainer.d.ts +28 -0
  4. package/dist/components/DOMContainer.d.ts.map +1 -0
  5. package/dist/components/DOMSprite.d.ts.map +1 -1
  6. package/dist/components/DisplayObject.d.ts +18 -0
  7. package/dist/components/DisplayObject.d.ts.map +1 -0
  8. package/dist/components/Graphic.d.ts.map +1 -1
  9. package/dist/components/Mesh.d.ts +138 -0
  10. package/dist/components/Mesh.d.ts.map +1 -0
  11. package/dist/components/Sprite.d.ts +182 -0
  12. package/dist/components/Sprite.d.ts.map +1 -0
  13. package/dist/components/Viewport.d.ts +52 -0
  14. package/dist/components/Viewport.d.ts.map +1 -0
  15. package/dist/components/index.d.ts +1 -1
  16. package/dist/components/index.d.ts.map +1 -1
  17. package/dist/directives/Controls.d.ts +1 -1
  18. package/dist/engine/reactive.d.ts +5 -1
  19. package/dist/engine/reactive.d.ts.map +1 -1
  20. package/dist/engine/signal.d.ts +1 -0
  21. package/dist/engine/signal.d.ts.map +1 -1
  22. package/dist/hooks/useProps.d.ts.map +1 -1
  23. package/dist/index.global.js +10 -10
  24. package/dist/index.global.js.map +1 -1
  25. package/dist/index.js +6843 -6720
  26. package/dist/index.js.map +1 -1
  27. package/dist/utils/RadialGradient.d.ts +1 -1
  28. package/package.json +1 -1
  29. package/src/components/Container.ts +10 -2
  30. package/src/components/DOMContainer.ts +8 -0
  31. package/src/components/DOMSprite.ts +11 -7
  32. package/src/components/DisplayObject.ts +1 -1
  33. package/src/components/Graphic.ts +6 -0
  34. package/src/components/Mesh.ts +38 -21
  35. package/src/components/Viewport.ts +4 -4
  36. package/src/components/index.ts +1 -1
  37. package/src/engine/reactive.ts +273 -53
  38. package/src/engine/signal.ts +74 -4
  39. package/src/hooks/useProps.ts +18 -6
  40. package/tsconfig.json +5 -1
@@ -55,11 +55,19 @@ type FlowResult = {
55
55
  elements: Element[];
56
56
  prev?: Element;
57
57
  fullElements?: Element[];
58
+ reorder?: boolean;
58
59
  };
59
60
 
60
61
  type FlowObservable = Observable<FlowResult>;
61
62
 
63
+ export interface LoopOptions<T> {
64
+ track?: (item: T, index: number | string) => string | number;
65
+ }
66
+
62
67
  const components: { [key: string]: any } = {};
68
+ const HOT_COMPONENT_PROPS = "__canvasEngineHotProps";
69
+ const HOT_COMPONENT_UPDATE_PROPS = "__canvasEngineUpdateHotProps";
70
+ const DEFINE_PROPS_SIGNALS = "__canvasEngineDefinePropsSignals";
63
71
 
64
72
  export const isElement = (value: any): value is Element => {
65
73
  return (
@@ -109,6 +117,24 @@ const DOM_UNSUPPORTED_TAGS = new Set([
109
117
  "FocusContainer",
110
118
  ]);
111
119
 
120
+ const readSignalValue = (value: any) => isSignal(value) ? value() : value;
121
+
122
+ const patchDefinePropsSignals = (target: Element, source: Element) => {
123
+ const targetSignals = (target as any)[DEFINE_PROPS_SIGNALS];
124
+ const sourceSignals = (source as any)[DEFINE_PROPS_SIGNALS];
125
+
126
+ if (!targetSignals || !sourceSignals) {
127
+ return;
128
+ }
129
+
130
+ Object.entries(sourceSignals as Record<string, any>).forEach(([key, sourceSignal]) => {
131
+ const targetSignal = targetSignals[key];
132
+ if (targetSignal && typeof targetSignal.set === "function") {
133
+ targetSignal.set(readSignalValue(sourceSignal));
134
+ }
135
+ });
136
+ };
137
+
112
138
  const hasDomAncestor = (element: Element | null): boolean => {
113
139
  let current = element;
114
140
  while (current) {
@@ -695,10 +721,82 @@ export function createComponent(tag: string, props?: Props): Element {
695
721
  return getNextGroupIndex();
696
722
  };
697
723
 
724
+ const collectMountedInstances = (
725
+ element: Element,
726
+ instances: any[],
727
+ childIndex: Map<any, number>,
728
+ seen = new Set<Element>()
729
+ ) => {
730
+ if (!element || seen.has(element)) return;
731
+ seen.add(element);
732
+
733
+ const instance = element.componentInstance as any;
734
+ if (childIndex.has(instance)) {
735
+ instances.push(instance);
736
+ return;
737
+ }
738
+
739
+ const nestedGroups = ((element as any).__childGroups ?? [])
740
+ .slice()
741
+ .sort((a, b) => a.order - b.order);
742
+ for (const group of nestedGroups) {
743
+ for (const mounted of group.mounted.values()) {
744
+ collectMountedInstances(mounted, instances, childIndex, seen);
745
+ }
746
+ }
747
+ };
748
+
749
+ const reorderMountedChildGroups = () => {
750
+ if (childGroups.length < 2) return;
751
+
752
+ const parentInstance = parent.componentInstance as any;
753
+ const children = parentInstance?.children;
754
+ if (!children || typeof parentInstance.addChildAt !== "function") return;
755
+
756
+ const childIndex = new Map<any, number>();
757
+ children.forEach((child, index) => {
758
+ childIndex.set(child, index);
759
+ });
760
+
761
+ const orderedInstances: any[] = [];
762
+ const orderedGroups = childGroups
763
+ .slice()
764
+ .sort((a, b) => a.order - b.order);
765
+
766
+ for (const group of orderedGroups) {
767
+ for (const mounted of group.mounted.values()) {
768
+ collectMountedInstances(mounted, orderedInstances, childIndex);
769
+ }
770
+ }
771
+
772
+ const mountedIndices = orderedInstances
773
+ .map((instance) => childIndex.get(instance))
774
+ .filter((index): index is number => index !== undefined);
775
+ if (!mountedIndices.length) return;
776
+
777
+ let targetIndex = Math.min(...mountedIndices);
778
+ for (const instance of orderedInstances) {
779
+ if (children[targetIndex] !== instance) {
780
+ parentInstance.addChildAt(instance, targetIndex);
781
+ }
782
+ targetIndex++;
783
+ }
784
+ };
785
+
786
+ const mountElementAtDeclaredOrder = (
787
+ element: Element,
788
+ sourceIndex: number,
789
+ orderedSources: any[]
790
+ ) => {
791
+ const mountResult = onMount(parent, element, getInsertIndex(sourceIndex, orderedSources));
792
+ void Promise.resolve(mountResult).then(reorderMountedChildGroups);
793
+ return mountResult;
794
+ };
795
+
698
796
  if (child instanceof Observable) {
699
797
  const mountedFlowElements = childGroup.mounted;
700
798
  const flowEffectSubscriptions = ((child as any).effectSubscriptions ?? []) as Subscription[];
701
- const flowEffectMounts = ((child as any).effectMounts ?? []) as Array<(element: Element) => any>;
799
+ const flowEffectMounts = ((child as any).effectMounts ?? []) as Array<(element?: Element) => any>;
702
800
 
703
801
  const applyFlowEffects = (element: Element) => {
704
802
  if (!flowEffectMounts.length) {
@@ -730,16 +828,31 @@ export function createComponent(tag: string, props?: Props): Element {
730
828
  const mountFlowElement = (
731
829
  element: Element,
732
830
  sourceIndex: number,
733
- orderedSources: any[]
831
+ orderedSources: any[],
832
+ shouldReorder = false
734
833
  ) => {
735
- if (mountedFlowElements.has(element)) {
834
+ const mounted = mountedFlowElements.get(element);
835
+ if (mounted) {
836
+ if (shouldReorder) {
837
+ const insertIndex = getInsertIndex(sourceIndex, orderedSources);
838
+ const parentInstance = mounted.parent?.componentInstance as any;
839
+ const childInstance = mounted.componentInstance as any;
840
+ if (
841
+ insertIndex !== undefined &&
842
+ parentInstance &&
843
+ typeof parentInstance.addChildAt === "function" &&
844
+ parentInstance.children?.includes(childInstance)
845
+ ) {
846
+ parentInstance.addChildAt(childInstance, insertIndex);
847
+ }
848
+ }
736
849
  return;
737
850
  }
738
851
 
739
852
  const routed = routeDomComponent(parent, element);
740
853
  applyFlowEffects(routed);
741
854
  mountedFlowElements.set(element, routed);
742
- onMount(parent, routed, getInsertIndex(sourceIndex, orderedSources));
855
+ mountElementAtDeclaredOrder(routed, sourceIndex, orderedSources);
743
856
  propagateContext(routed);
744
857
  };
745
858
 
@@ -759,7 +872,8 @@ export function createComponent(tag: string, props?: Props): Element {
759
872
  component: any,
760
873
  nextElements: Set<any>,
761
874
  index: number,
762
- orderedSources: any[]
875
+ orderedSources: any[],
876
+ shouldReorder = false
763
877
  ) => {
764
878
  if (component instanceof Observable) {
765
879
  nextElements.add(component);
@@ -772,7 +886,7 @@ export function createComponent(tag: string, props?: Props): Element {
772
886
  }
773
887
  if (Array.isArray(component)) {
774
888
  component.forEach((comp) =>
775
- processFlowComponent(comp, nextElements, index, orderedSources)
889
+ processFlowComponent(comp, nextElements, index, orderedSources, shouldReorder)
776
890
  );
777
891
  return;
778
892
  }
@@ -781,7 +895,7 @@ export function createComponent(tag: string, props?: Props): Element {
781
895
  }
782
896
 
783
897
  nextElements.add(component);
784
- mountFlowElement(component, index, orderedSources);
898
+ mountFlowElement(component, index, orderedSources, shouldReorder);
785
899
  };
786
900
 
787
901
  // Subscribe to the observable and handle the emitted values
@@ -793,9 +907,11 @@ export function createComponent(tag: string, props?: Props): Element {
793
907
  const {
794
908
  elements: comp,
795
909
  prev,
910
+ reorder,
796
911
  }: {
797
912
  elements: Element[];
798
913
  prev?: Element;
914
+ reorder?: boolean;
799
915
  } = value;
800
916
 
801
917
  const components = comp.filter((c) => c !== null);
@@ -809,7 +925,7 @@ export function createComponent(tag: string, props?: Props): Element {
809
925
  return;
810
926
  }
811
927
  components.forEach((component, index) => {
812
- processFlowComponent(component, nextElements, index, components);
928
+ processFlowComponent(component, nextElements, index, components, reorder);
813
929
  });
814
930
  syncFlowElements(nextElements);
815
931
  } else if (isElement(value)) {
@@ -817,7 +933,7 @@ export function createComponent(tag: string, props?: Props): Element {
817
933
  const routed = routeDomComponent(parent, value);
818
934
  applyFlowEffects(routed);
819
935
  childGroup.mounted.set(value, routed);
820
- onMount(parent, routed, getInsertIndex(0, [value]));
936
+ mountElementAtDeclaredOrder(routed, 0, [value]);
821
937
  propagateContext(routed);
822
938
  } else if (Array.isArray(value)) {
823
939
  // Handle array of elements (which can also be observables)
@@ -844,7 +960,7 @@ export function createComponent(tag: string, props?: Props): Element {
844
960
  } else if (isElement(child)) {
845
961
  const routed = routeDomComponent(parent, child);
846
962
  childGroup.mounted.set(child, routed);
847
- onMount(parent, routed, getInsertIndex(0, [child]));
963
+ mountElementAtDeclaredOrder(routed, 0, [child]);
848
964
  await propagateContext(routed);
849
965
  }
850
966
  }
@@ -862,7 +978,8 @@ export function createComponent(tag: string, props?: Props): Element {
862
978
  */
863
979
  export function loop<T>(
864
980
  itemsSubject: any,
865
- createElementFn: (item: T, index: number | string) => Element | null
981
+ createElementFn: (item: T, index: number | string) => Element | null,
982
+ options: LoopOptions<T> = {}
866
983
  ): FlowObservable {
867
984
 
868
985
  if (isComputed(itemsSubject) && itemsSubject.dependencies.size == 0) {
@@ -876,6 +993,8 @@ export function loop<T>(
876
993
  let elements: Element[] = [];
877
994
  let elementMap = new Map<string | number, Element>();
878
995
  let isFirstSubscription = true;
996
+ const getTrackKey = (item: T, index: number | string) =>
997
+ options.track ? options.track(item, index) : index;
879
998
 
880
999
  const ensureElement = (itemResult: any): Element | null => {
881
1000
  if (!itemResult) return null;
@@ -900,27 +1019,133 @@ export function loop<T>(
900
1019
  const isArraySignal = (signal: any): signal is WritableArraySignal<T[]> =>
901
1020
  Array.isArray(signal());
902
1021
 
1022
+ const cleanupUntrackedElement = (element: Element | null) => {
1023
+ if (!element) return;
1024
+ element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
1025
+ element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
1026
+ element.effectUnmounts?.forEach((fn) => fn?.());
1027
+ };
1028
+
1029
+ const updateTrackedHotChildren = (targetChildren: any, sourceChildren: any) => {
1030
+ const targetList = Array.isArray(targetChildren) ? targetChildren : [targetChildren];
1031
+ const sourceList = Array.isArray(sourceChildren) ? sourceChildren : [sourceChildren];
1032
+ let updated = false;
1033
+
1034
+ targetList.forEach((targetChild, index) => {
1035
+ const sourceChild = sourceList[index];
1036
+ const updateProps = targetChild?.[HOT_COMPONENT_UPDATE_PROPS];
1037
+ const nextProps = sourceChild?.[HOT_COMPONENT_PROPS];
1038
+
1039
+ if (typeof updateProps === "function" && nextProps !== undefined) {
1040
+ updateProps(nextProps);
1041
+ updated = true;
1042
+ }
1043
+ });
1044
+
1045
+ return updated;
1046
+ };
1047
+
1048
+ const patchTrackedElement = (target: Element, source: Element) => {
1049
+ const nextProps = { ...source.props };
1050
+ const nextPropObservables = source.propObservables;
1051
+ const updatedHotChildren = updateTrackedHotChildren(target.props.children, source.props.children);
1052
+
1053
+ patchDefinePropsSignals(target, source);
1054
+
1055
+ if (target.props.context) {
1056
+ nextProps.context = target.props.context;
1057
+ }
1058
+ if (updatedHotChildren || (target.props.children && !source.props.children)) {
1059
+ nextProps.children = target.props.children;
1060
+ }
1061
+
1062
+ target.props = nextProps;
1063
+ target.propObservables = nextPropObservables;
1064
+ target.componentInstance.onUpdate?.(nextProps);
1065
+ Object.entries(target.directives).forEach(([name, directive]) => {
1066
+ if (name in nextProps) {
1067
+ directive.onUpdate?.(nextProps[name], target);
1068
+ }
1069
+ });
1070
+
1071
+ cleanupUntrackedElement(source);
1072
+ };
1073
+
1074
+ const removeElementFromMap = (element: Element) => {
1075
+ for (const [key, mappedElement] of elementMap.entries()) {
1076
+ if (mappedElement === element) {
1077
+ elementMap.delete(key);
1078
+ return;
1079
+ }
1080
+ }
1081
+ };
1082
+
1083
+ const rebuildArrayElements = (items: T[] | undefined | null) => {
1084
+ if (!options.track) {
1085
+ elements.forEach(el => destroyElement(el));
1086
+ elements = [];
1087
+ elementMap.clear();
1088
+
1089
+ if (items) {
1090
+ items.forEach((item, index) => {
1091
+ const element = ensureElement(createElementFn(item, index));
1092
+ if (element) {
1093
+ elements.push(element);
1094
+ elementMap.set(index, element);
1095
+ }
1096
+ });
1097
+ }
1098
+ return;
1099
+ }
1100
+
1101
+ const previousMap = elementMap;
1102
+ const nextElements: Element[] = [];
1103
+ const nextMap = new Map<string | number, Element>();
1104
+ const usedElements = new Set<Element>();
1105
+
1106
+ if (items) {
1107
+ items.forEach((item, index) => {
1108
+ const key = getTrackKey(item, index);
1109
+ const existing = previousMap.get(key);
1110
+ const nextElement = ensureElement(createElementFn(item, index));
1111
+
1112
+ if (existing) {
1113
+ if (nextElement) {
1114
+ patchTrackedElement(existing, nextElement);
1115
+ }
1116
+ nextElements.push(existing);
1117
+ nextMap.set(key, existing);
1118
+ usedElements.add(existing);
1119
+ return;
1120
+ }
1121
+
1122
+ if (nextElement) {
1123
+ nextElements.push(nextElement);
1124
+ nextMap.set(key, nextElement);
1125
+ usedElements.add(nextElement);
1126
+ }
1127
+ });
1128
+ }
1129
+
1130
+ elements.forEach((element) => {
1131
+ if (!usedElements.has(element)) {
1132
+ destroyElement(element);
1133
+ }
1134
+ });
1135
+
1136
+ elements = nextElements;
1137
+ elementMap = nextMap;
1138
+ };
1139
+
903
1140
  return new Observable<FlowResult>(subscriber => {
904
1141
  const subscription = isArraySignal(itemsSubject)
905
1142
  ? itemsSubject.observable.subscribe(change => {
906
1143
  if (isFirstSubscription) {
907
1144
  isFirstSubscription = false;
908
- elements.forEach(el => el.destroy());
909
- elements = [];
910
- elementMap.clear();
911
-
912
- const items = itemsSubject();
913
- if (items) {
914
- items.forEach((item, index) => {
915
- const element = ensureElement(createElementFn(item, index));
916
- if (element) {
917
- elements.push(element);
918
- elementMap.set(index, element);
919
- }
920
- });
921
- }
1145
+ rebuildArrayElements(itemsSubject());
922
1146
  subscriber.next({
923
- elements: [...elements]
1147
+ elements: [...elements],
1148
+ reorder: Boolean(options.track)
924
1149
  });
925
1150
  return;
926
1151
  }
@@ -930,25 +1155,13 @@ export function loop<T>(
930
1155
  const isDirectArrayChange = Array.isArray(change) || (change && typeof change === 'object' && !('type' in change));
931
1156
 
932
1157
  if (change.type === 'init' || change.type === 'reset' || isDirectArrayChange) {
933
- elements.forEach(el => destroyElement(el));
934
- elements = [];
935
- elementMap.clear();
936
-
937
- const items = itemsSubject();
938
- if (items) {
939
- items.forEach((item, index) => {
940
- const element = ensureElement(createElementFn(item, index));
941
- if (element) {
942
- elements.push(element);
943
- elementMap.set(index, element);
944
- }
945
- });
946
- }
1158
+ rebuildArrayElements(itemsSubject());
947
1159
  } else if (change.type === 'add' && change.index !== undefined) {
948
1160
  const newElements = change.items.map((item, i) => {
949
- const element = ensureElement(createElementFn(item as T, change.index! + i));
1161
+ const index = change.index! + i;
1162
+ const element = ensureElement(createElementFn(item as T, index));
950
1163
  if (element) {
951
- elementMap.set(change.index! + i, element);
1164
+ elementMap.set(getTrackKey(item as T, index), element);
952
1165
  }
953
1166
  return element;
954
1167
  }).filter((el): el is Element => el !== null);
@@ -958,19 +1171,20 @@ export function loop<T>(
958
1171
  const removed = elements.splice(change.index, 1);
959
1172
  removed.forEach(el => {
960
1173
  destroyElement(el)
961
- elementMap.delete(change.index!);
1174
+ removeElementFromMap(el);
962
1175
  });
963
1176
  } else if (change.type === 'update' && change.index !== undefined && change.items.length === 1) {
964
1177
  const index = change.index;
965
1178
  const newItem = change.items[0];
1179
+ const key = getTrackKey(newItem as T, index);
966
1180
 
967
1181
  // Check if the previous item at this index was effectively undefined or non-existent
968
- if (index >= elements.length || elements[index] === undefined || !elementMap.has(index)) {
1182
+ if (index >= elements.length || elements[index] === undefined || !elementMap.has(key)) {
969
1183
  // Treat as add operation
970
1184
  const newElement = ensureElement(createElementFn(newItem as T, index));
971
1185
  if (newElement) {
972
1186
  elements.splice(index, 0, newElement); // Insert at the correct index
973
- elementMap.set(index, newElement);
1187
+ elementMap.set(key, newElement);
974
1188
  // Adjust indices in elementMap for subsequent elements might be needed if map relied on exact indices
975
1189
  // This simple implementation assumes keys are stable or createElementFn handles context correctly
976
1190
  } else {
@@ -978,22 +1192,28 @@ export function loop<T>(
978
1192
  }
979
1193
  } else {
980
1194
  // Treat as a standard update operation
981
- const oldElement = elements[index];
982
- destroyElement(oldElement)
1195
+ const oldElement = elementMap.get(key) ?? elements[index];
983
1196
  const newElement = ensureElement(createElementFn(newItem as T, index));
984
- if (newElement) {
1197
+ if (options.track && oldElement && newElement) {
1198
+ patchTrackedElement(oldElement, newElement);
1199
+ elements[index] = oldElement;
1200
+ elementMap.set(key, oldElement);
1201
+ } else if (newElement) {
1202
+ destroyElement(oldElement)
985
1203
  elements[index] = newElement;
986
- elementMap.set(index, newElement);
1204
+ elementMap.set(key, newElement);
987
1205
  } else {
988
1206
  // Handle case where new element creation returns null
1207
+ destroyElement(oldElement)
989
1208
  elements.splice(index, 1);
990
- elementMap.delete(index);
1209
+ elementMap.delete(key);
991
1210
  }
992
1211
  }
993
1212
  }
994
1213
 
995
1214
  subscriber.next({
996
- elements: [...elements] // Create a new array to ensure change detection
1215
+ elements: [...elements], // Create a new array to ensure change detection
1216
+ reorder: Boolean(options.track)
997
1217
  });
998
1218
  })
999
1219
  : (itemsSubject as WritableObjectSignal<T>).observable.subscribe(change => {
@@ -1007,7 +1227,7 @@ export function loop<T>(
1007
1227
  const items = (itemsSubject as WritableObjectSignal<T>)();
1008
1228
  if (items) {
1009
1229
  Object.entries(items).forEach(([key, value]) => {
1010
- const element = ensureElement(createElementFn(value, key));
1230
+ const element = ensureElement(createElementFn(value as T, key));
1011
1231
  if (element) {
1012
1232
  elements.push(element);
1013
1233
  elementMap.set(key, element);
@@ -1028,7 +1248,7 @@ export function loop<T>(
1028
1248
  const items = (itemsSubject as WritableObjectSignal<T>)();
1029
1249
  if (items) {
1030
1250
  Object.entries(items).forEach(([key, value]) => {
1031
- const element = ensureElement(createElementFn(value, key));
1251
+ const element = ensureElement(createElementFn(value as T, key));
1032
1252
  if (element) {
1033
1253
  elements.push(element);
1034
1254
  elementMap.set(key, element);
@@ -22,7 +22,12 @@ type HotComponentRecord = {
22
22
  wrapper?: ComponentFunction<any>;
23
23
  };
24
24
 
25
+ const HOT_COMPONENT_PROPS = "__canvasEngineHotProps";
26
+ const HOT_COMPONENT_UPDATE_PROPS = "__canvasEngineUpdateHotProps";
27
+ const DEFINE_PROPS_SIGNALS = "__canvasEngineDefinePropsSignals";
28
+
25
29
  export let currentSubscriptionsTracker: ((subscription: Subscription) => void) | null = null;
30
+ export let currentDefinePropsTracker: ((signals: Record<string, any>) => void) | null = null;
26
31
  export let mountTracker: MountFunction | null = null;
27
32
 
28
33
  const getHotComponentRegistry = (): Map<string, HotComponentRecord> => {
@@ -163,11 +168,19 @@ function createTrackedComponent<C extends ComponentFunction<any>>(
163
168
  ): ReturnType<C> {
164
169
  const allSubscriptions = new Set<Subscription>();
165
170
  const allMounts = new Set<MountCallback>();
171
+ let allDefinePropSignals: Record<string, any> | null = null;
166
172
 
167
173
  currentSubscriptionsTracker = (subscription) => {
168
174
  allSubscriptions.add(subscription);
169
175
  };
170
176
 
177
+ currentDefinePropsTracker = (signals) => {
178
+ allDefinePropSignals = {
179
+ ...(allDefinePropSignals ?? {}),
180
+ ...signals,
181
+ };
182
+ };
183
+
171
184
  mountTracker = (fn: any) => {
172
185
  allMounts.add(fn);
173
186
  };
@@ -177,6 +190,7 @@ function createTrackedComponent<C extends ComponentFunction<any>>(
177
190
  component = componentFunction(props) as ReturnType<C>;
178
191
  } finally {
179
192
  currentSubscriptionsTracker = null;
193
+ currentDefinePropsTracker = null;
180
194
  mountTracker = null;
181
195
  }
182
196
 
@@ -190,6 +204,9 @@ function createTrackedComponent<C extends ComponentFunction<any>>(
190
204
  ...Array.from(allMounts),
191
205
  ...((element as any).effectMounts ?? [])
192
206
  ];
207
+ if (allDefinePropSignals) {
208
+ (element as any)[DEFINE_PROPS_SIGNALS] = allDefinePropSignals;
209
+ }
193
210
  };
194
211
 
195
212
  if (component instanceof Promise) {
@@ -208,6 +225,9 @@ function createTrackedComponent<C extends ComponentFunction<any>>(
208
225
  ...Array.from(allMounts),
209
226
  ...((component as any).effectMounts ?? [])
210
227
  ];
228
+ if (allDefinePropSignals) {
229
+ (component as any)[DEFINE_PROPS_SIGNALS] = allDefinePropSignals;
230
+ }
211
231
  } else {
212
232
  applyTrackedEffects(component as Element);
213
233
  }
@@ -235,14 +255,55 @@ export function createHotComponent<P>(
235
255
 
236
256
  if (!record.wrapper) {
237
257
  record.wrapper = ((props: P) => {
238
- return new Observable<HotFlowResult>((subscriber) => {
258
+ let currentProps = props;
259
+
260
+ const observable = new Observable<HotFlowResult>((subscriber) => {
239
261
  let disposed = false;
240
262
  let currentElement: Element | null = null;
241
263
 
242
- const emit = () => {
243
- const rendered = createTrackedComponent(record!.component, props);
264
+ const patchElement = (target: Element, source: Element) => {
265
+ if (target.tag !== source.tag) {
266
+ return false;
267
+ }
268
+
269
+ const nextProps = { ...source.props };
270
+ if (target.props.context) {
271
+ nextProps.context = target.props.context;
272
+ }
273
+ if (target.props.children && !source.props.children) {
274
+ nextProps.children = target.props.children;
275
+ }
276
+
277
+ target.props = nextProps;
278
+ target.propObservables = source.propObservables;
279
+ target.componentInstance.onUpdate?.(nextProps);
280
+ Object.entries(target.directives).forEach(([name, directive]) => {
281
+ if (name in nextProps) {
282
+ directive.onUpdate?.(nextProps[name], target);
283
+ }
284
+ });
285
+
286
+ source.propSubscriptions?.forEach((sub) => sub.unsubscribe());
287
+ source.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
288
+ source.effectUnmounts?.forEach((fn) => fn?.());
289
+
290
+ return true;
291
+ };
292
+
293
+ const emit = (preserveCurrentElement = false) => {
294
+ const rendered = createTrackedComponent(record!.component, currentProps);
244
295
  const next = (element: Element | null | undefined) => {
245
296
  if (!disposed) {
297
+ if (
298
+ preserveCurrentElement &&
299
+ currentElement &&
300
+ element &&
301
+ patchElement(currentElement, element)
302
+ ) {
303
+ subscriber.next({ elements: [currentElement] });
304
+ return;
305
+ }
306
+
246
307
  subscriber.next({ elements: element ? [element] : [] });
247
308
  if (currentElement && currentElement !== element) {
248
309
  destroyElement(currentElement);
@@ -259,7 +320,12 @@ export function createHotComponent<P>(
259
320
  };
260
321
 
261
322
  emit();
262
- const subscription = record!.updates.subscribe(emit);
323
+ (observable as any)[HOT_COMPONENT_UPDATE_PROPS] = (nextProps: P) => {
324
+ currentProps = nextProps;
325
+ emit(true);
326
+ };
327
+
328
+ const subscription = record!.updates.subscribe(() => emit());
263
329
 
264
330
  return () => {
265
331
  disposed = true;
@@ -270,6 +336,10 @@ export function createHotComponent<P>(
270
336
  subscription.unsubscribe();
271
337
  };
272
338
  }) as any;
339
+
340
+ (observable as any)[HOT_COMPONENT_PROPS] = props;
341
+
342
+ return observable;
273
343
  }) as ComponentFunction<any>;
274
344
  }
275
345
 
@@ -1,5 +1,6 @@
1
1
  import { isSignal, signal } from "@signe/reactive"
2
2
  import { isPrimitive } from "../engine/reactive"
3
+ import { currentDefinePropsTracker } from "../engine/signal"
3
4
 
4
5
  /**
5
6
  * Converts props into reactive signals if they are primitive values.
@@ -46,6 +47,16 @@ type PropSchema = {
46
47
  [key: string]: PropType | PropType[] | PropConfig;
47
48
  }
48
49
 
50
+ const toPropSignal = (value: any) => isSignal(value) ? value : signal(value)
51
+
52
+ const definePropSignals = (props: any): any => {
53
+ const obj: any = {}
54
+ for (let key in props) {
55
+ obj[key] = toPropSignal(props[key])
56
+ }
57
+ return obj
58
+ }
59
+
49
60
  /**
50
61
  * Validates and defines properties based on a schema.
51
62
  *
@@ -109,15 +120,16 @@ export const useDefineProps = (props: any) => {
109
120
  }
110
121
  }
111
122
 
112
- validatedProps[key] = isSignal(validatedValue)
113
- ? validatedValue
114
- : signal(validatedValue)
123
+ validatedProps[key] = toPropSignal(validatedValue)
115
124
  }
116
125
 
117
- return {
118
- ...useProps(rawProps),
126
+ const definedProps = {
127
+ ...definePropSignals(rawProps),
119
128
  ...validatedProps
120
129
  }
130
+ currentDefinePropsTracker?.(definedProps)
131
+
132
+ return definedProps
121
133
  }
122
134
  }
123
135
 
@@ -152,4 +164,4 @@ function validateType(key: string, value: any, types: any[]) {
152
164
  `Expected ${types.map(t => t.name).join(' or ')}`
153
165
  )
154
166
  }
155
- }
167
+ }