Skip to content

Commit ff9985f

Browse files
committed
feat: add beforeMount & beforeUnmount lifecycle and Transition
1 parent 4a6c5a0 commit ff9985f

File tree

10 files changed

+399
-27
lines changed

10 files changed

+399
-27
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as Transition } from "./transition";
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { KebabToCamel } from "../../../utils/dom";
2+
import { ref, computed, handleWithFunType } from "../../reactive";
3+
import { type IDomilyRenderOptions } from "../../render";
4+
5+
const classNames = (value: (string | undefined)[]) => {
6+
return value.filter(Boolean).join(" ");
7+
};
8+
9+
const nextFrame = (callback: FrameRequestCallback) => {
10+
requestAnimationFrame(() => {
11+
requestAnimationFrame(callback);
12+
});
13+
};
14+
15+
function getTotalTransitionTime(element: HTMLElement) {
16+
if (!(element instanceof HTMLElement)) {
17+
return 0;
18+
}
19+
const style = window.getComputedStyle(element);
20+
const durations = style.transitionDuration.split(",").map((d) => d.trim());
21+
const delays = style.transitionDelay.split(",").map((d) => d.trim());
22+
23+
function toMilliseconds(time: string) {
24+
if (time.endsWith("ms")) {
25+
return parseFloat(time);
26+
} else if (time.endsWith("s")) {
27+
return parseFloat(time) * 1000;
28+
}
29+
return 0;
30+
}
31+
32+
const durationArray = durations.map(toMilliseconds);
33+
let delayArray = delays.map(toMilliseconds);
34+
35+
// 如果两个数组长度不同,则重复较短的数组直到匹配较长的数组
36+
const maxLength = Math.max(durationArray.length, delayArray.length);
37+
if (durationArray.length < maxLength) {
38+
// 重复durationArray直到长度等于maxLength
39+
durationArray.length = maxLength;
40+
for (let i = durationArray.length; i < maxLength; i++) {
41+
durationArray[i] = durationArray[i % durationArray.length];
42+
}
43+
}
44+
if (delayArray.length < maxLength) {
45+
// 重复delayArray直到长度等于maxLength
46+
delayArray.length = maxLength;
47+
for (let i = delayArray.length; i < maxLength; i++) {
48+
delayArray[i] = delayArray[i % delayArray.length];
49+
}
50+
}
51+
52+
// 计算每个属性的总时间(持续时间+延迟时间)
53+
const totalTimes = durationArray.map((dur, index) => dur + delayArray[index]);
54+
// 取最大值
55+
const maxTotalTime = Math.max(...totalTimes);
56+
return maxTotalTime;
57+
}
58+
59+
interface IAnimationClassNames {
60+
enterFrom: string;
61+
enterActive: string;
62+
enterTo: string;
63+
leaveFrom: string;
64+
leaveActive: string;
65+
leaveTo: string;
66+
}
67+
68+
export default function Transition(props?: {
69+
name?: string;
70+
duration?: number | { enter?: number; leave?: number };
71+
type?: "transition" | "animation";
72+
slot?: IDomilyRenderOptions;
73+
}) {
74+
const { name, slot, type = "transition", duration } = props || {};
75+
76+
if (!name || !slot) {
77+
return slot;
78+
}
79+
80+
const animationClassNames = Object.fromEntries(
81+
[
82+
"enter-from",
83+
"enter-active",
84+
"enter-to",
85+
"leave-from",
86+
"leave-active",
87+
"leave-to",
88+
].map((e) => [KebabToCamel(e), `${name}-${e}`])
89+
) as unknown as IAnimationClassNames;
90+
91+
const cls = ref<string>("");
92+
93+
const enter = (dom: HTMLElement | Node | null) => {
94+
let timer: number;
95+
nextFrame(() => {
96+
if (!dom) {
97+
return;
98+
}
99+
const end = () => {
100+
clearTimeout(timer);
101+
if (cls.value === "") {
102+
return;
103+
}
104+
cls.value = "";
105+
};
106+
107+
dom?.addEventListener(type + "end", end, { once: true });
108+
109+
const totalTransitionTime = getTotalTransitionTime(dom as HTMLElement);
110+
111+
const timeout =
112+
typeof duration === "number"
113+
? duration
114+
: typeof duration === "object" && typeof duration?.enter === "number"
115+
? duration.enter
116+
: totalTransitionTime + 1;
117+
118+
timer = window.setTimeout(end, timeout);
119+
120+
cls.value = classNames([
121+
animationClassNames.enterActive,
122+
animationClassNames.enterTo,
123+
]);
124+
});
125+
};
126+
const leave = (dom: HTMLElement | Node | null) => {
127+
const { promise, resolve } = Promise.withResolvers<void>();
128+
cls.value = classNames([
129+
animationClassNames.leaveFrom,
130+
animationClassNames.leaveActive,
131+
]);
132+
133+
let timer: number;
134+
135+
nextFrame(() => {
136+
if (!dom) {
137+
return;
138+
}
139+
const end = () => {
140+
clearTimeout(timer);
141+
if (cls.value === "") {
142+
resolve();
143+
return;
144+
}
145+
cls.value = "";
146+
resolve();
147+
};
148+
149+
dom?.addEventListener(type + "end", end, { once: true });
150+
151+
const totalTransitionTime = getTotalTransitionTime(dom as HTMLElement);
152+
153+
const timeout =
154+
typeof duration === "number"
155+
? duration
156+
: typeof duration === "object" && typeof duration?.leave === "number"
157+
? duration.leave
158+
: totalTransitionTime + 1;
159+
160+
timer = window.setTimeout(end, timeout);
161+
162+
cls.value = classNames([
163+
animationClassNames.leaveActive,
164+
animationClassNames.leaveTo,
165+
]);
166+
});
167+
return promise;
168+
};
169+
170+
const originalBeforeMount = slot.beforeMount;
171+
const originalMounted = slot.mounted;
172+
const originalBeforeUnmount = slot.beforeUnmount;
173+
const originalUnmounted = slot.unmounted;
174+
const originalClassName = slot.className;
175+
176+
slot.beforeMount = (dom: HTMLElement | Node | null) => {
177+
typeof originalBeforeMount === "function" && originalBeforeMount(dom);
178+
cls.value = classNames([
179+
animationClassNames.enterFrom,
180+
animationClassNames.enterActive,
181+
]);
182+
};
183+
184+
slot.mounted = (dom: HTMLElement | Node | null) => {
185+
typeof originalMounted === "function" && originalMounted(dom);
186+
enter(dom);
187+
};
188+
189+
slot.beforeUnmount = (dom: HTMLElement | Node | null) => {
190+
typeof originalBeforeUnmount === "function" && originalBeforeUnmount(dom);
191+
return leave(dom);
192+
};
193+
194+
slot.unmounted = () => {
195+
typeof originalUnmounted === "function" && originalUnmounted();
196+
};
197+
198+
slot.className = computed(() =>
199+
classNames([handleWithFunType(originalClassName), cls.value])
200+
);
201+
202+
return slot;
203+
}

packages/runtime-core/src/core/component/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type {
77
import DomilyRenderSchema from "../render/schema";
88
import { domilyChildToDOMilyMountableRender } from "../render/shared/parse";
99

10+
export * from "./builtin";
11+
1012
export interface DOMilyComponent {
1113
(props?: any):
1214
| WithFuncType<DomilyRenderSchema>
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export * from "alien-signals";
2-
export { default as ref } from "./ref";
3-
export { default as reactive } from "./reactive";
2+
export { default as ref, shallowRef } from "./ref";
3+
export { default as reactive, shallowReactive } from "./reactive";
44
export { computed, signalComputed } from "./computed";
55
export * from "./handle-effect";
66
export * from "./type";

packages/runtime-core/src/core/reactive/reactive.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,51 @@ export default function reactive<T extends object>(initialValue: T) {
5252
},
5353
});
5454
}
55+
56+
export function shallowReactive<T extends object>(initialValue: T) {
57+
const value = signal<T>(initialValue);
58+
const setter = (newValue: Partial<T>) => {
59+
/**
60+
* if newValue is an array, we assume it is a new value for the whole object
61+
* otherwise, we merge the newValue with the existing value
62+
* this allows us to update a single property or the whole object
63+
*/
64+
const nextValue = (
65+
Array.isArray(newValue)
66+
? newValue
67+
: {
68+
...value(),
69+
...newValue,
70+
}
71+
) as T;
72+
value(nextValue);
73+
};
74+
return new Proxy(value as Reactive<T>, {
75+
get(target, p, receiver) {
76+
return Reflect.get(target(), p, receiver);
77+
},
78+
set(_target, p, newValue) {
79+
setter({ [p]: newValue } as Partial<T>);
80+
return true;
81+
},
82+
deleteProperty(target, p) {
83+
const nextValue = { ...target() };
84+
Reflect.deleteProperty(nextValue, p);
85+
value(nextValue);
86+
return true;
87+
},
88+
defineProperty(target, p, attributes) {
89+
const nextValue = { ...target() };
90+
Reflect.defineProperty(nextValue, p, attributes);
91+
value(nextValue);
92+
return true;
93+
},
94+
apply(target, thisArg, argArray) {
95+
if (argArray.length === 0) {
96+
return Reflect.apply(target, thisArg, argArray);
97+
}
98+
setter(argArray[0]);
99+
return;
100+
},
101+
});
102+
}

packages/runtime-core/src/core/reactive/ref.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,17 @@ export default function ref<T>(value: T): Ref<T> {
1717
});
1818
return signalValue as Ref<T>;
1919
}
20+
21+
export function shallowRef<T>(value: T): Ref<T> {
22+
const signalValue = signal(value);
23+
Reflect.defineProperty(signalValue, "value", {
24+
get() {
25+
const rs = signalValue();
26+
return rs;
27+
},
28+
set(newValue: T) {
29+
signalValue(newValue);
30+
},
31+
});
32+
return signalValue as Ref<T>;
33+
}

0 commit comments

Comments
 (0)