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.
- package/dist/components/Container.d.ts +25 -0
- package/dist/components/Container.d.ts.map +1 -0
- package/dist/components/DOMContainer.d.ts +28 -0
- package/dist/components/DOMContainer.d.ts.map +1 -0
- package/dist/components/DOMSprite.d.ts.map +1 -1
- package/dist/components/DisplayObject.d.ts +18 -0
- package/dist/components/DisplayObject.d.ts.map +1 -0
- package/dist/components/Graphic.d.ts.map +1 -1
- package/dist/components/Mesh.d.ts +138 -0
- package/dist/components/Mesh.d.ts.map +1 -0
- package/dist/components/Sprite.d.ts +182 -0
- package/dist/components/Sprite.d.ts.map +1 -0
- package/dist/components/Viewport.d.ts +52 -0
- package/dist/components/Viewport.d.ts.map +1 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/directives/Controls.d.ts +1 -1
- package/dist/engine/reactive.d.ts +5 -1
- package/dist/engine/reactive.d.ts.map +1 -1
- package/dist/engine/signal.d.ts +1 -0
- package/dist/engine/signal.d.ts.map +1 -1
- package/dist/hooks/useProps.d.ts.map +1 -1
- package/dist/index.global.js +10 -10
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +6843 -6720
- package/dist/index.js.map +1 -1
- package/dist/utils/RadialGradient.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Container.ts +10 -2
- package/src/components/DOMContainer.ts +8 -0
- package/src/components/DOMSprite.ts +11 -7
- package/src/components/DisplayObject.ts +1 -1
- package/src/components/Graphic.ts +6 -0
- package/src/components/Mesh.ts +38 -21
- package/src/components/Viewport.ts +4 -4
- package/src/components/index.ts +1 -1
- package/src/engine/reactive.ts +273 -53
- package/src/engine/signal.ts +74 -4
- package/src/hooks/useProps.ts +18 -6
- package/tsconfig.json +5 -1
package/src/engine/reactive.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1161
|
+
const index = change.index! + i;
|
|
1162
|
+
const element = ensureElement(createElementFn(item as T, index));
|
|
950
1163
|
if (element) {
|
|
951
|
-
elementMap.set(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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);
|
package/src/engine/signal.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
243
|
-
|
|
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
|
-
|
|
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
|
|
package/src/hooks/useProps.ts
CHANGED
|
@@ -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] =
|
|
113
|
-
? validatedValue
|
|
114
|
-
: signal(validatedValue)
|
|
123
|
+
validatedProps[key] = toPropSignal(validatedValue)
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
...
|
|
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
|
+
}
|