mirror of
https://github.com/ecomfe/vue-echarts.git
synced 2025-12-17 16:31:17 +08:00
fix: refactor slot implementation and improve types
- Improved types. - Refactored slot implementation to make it more robust. - Modified the `setOption` behavior to ensure it respects the `manual-update` flag. - Renamed the `smart-update` files to `update`. - Improved warnings. - Added more tests.
This commit is contained in:
@@ -11,7 +11,6 @@ import {
|
||||
nextTick,
|
||||
watchEffect,
|
||||
toValue,
|
||||
warn,
|
||||
} from "vue";
|
||||
import { init as initChart } from "echarts/core";
|
||||
import type { EChartsOption } from "echarts";
|
||||
@@ -25,10 +24,10 @@ import {
|
||||
useSlotOption,
|
||||
} from "./composables";
|
||||
import type { PublicMethods, SlotsTypes } from "./composables";
|
||||
import { isOn, omitOn } from "./utils";
|
||||
import { isOn, omitOn, warn } from "./utils";
|
||||
import { register, TAG_NAME } from "./wc";
|
||||
import { planUpdate } from "./smart-update";
|
||||
import type { Signature, UpdatePlan } from "./smart-update";
|
||||
import { planUpdate } from "./update";
|
||||
import type { Signature, UpdatePlan } from "./update";
|
||||
|
||||
import type { PropType, InjectionKey } from "vue";
|
||||
import type {
|
||||
@@ -81,7 +80,6 @@ export default defineComponent({
|
||||
|
||||
const { autoresize, manualUpdate, loading, loadingOptions } = toRefs(props);
|
||||
|
||||
const realOption = computed(() => props.option || {});
|
||||
const realTheme = computed(() => props.theme || toValue(defaultTheme));
|
||||
const realInitOptions = computed(
|
||||
() => props.initOptions || toValue(defaultInitOptions) || undefined,
|
||||
@@ -186,7 +184,7 @@ export default defineComponent({
|
||||
listeners.set({ event, zr, once }, attrs[key]);
|
||||
});
|
||||
|
||||
function init(option?: Option, manual = false, override?: UpdateOptions) {
|
||||
function init() {
|
||||
if (!root.value) {
|
||||
return;
|
||||
}
|
||||
@@ -235,10 +233,17 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function commit() {
|
||||
const opt = option || realOption.value;
|
||||
if (opt) {
|
||||
applyOption(instance, opt, override, manual);
|
||||
override = undefined;
|
||||
const { option } = props;
|
||||
|
||||
if (manualUpdate.value) {
|
||||
if (option) {
|
||||
applyOption(instance, option, undefined, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (option) {
|
||||
applyOption(instance, option);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,9 +264,7 @@ export default defineComponent({
|
||||
lazyUpdate?: boolean,
|
||||
) => {
|
||||
if (!props.manualUpdate) {
|
||||
warn(
|
||||
"[vue-echarts] setOption is only available when manual-update is true.",
|
||||
);
|
||||
warn("`setOption` is only available when `manual-update` is `true`.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -283,40 +286,32 @@ export default defineComponent({
|
||||
lastSignature = undefined;
|
||||
}
|
||||
|
||||
let unwatchOption: (() => void) | null = null;
|
||||
watch(
|
||||
manualUpdate,
|
||||
(manualUpdate) => {
|
||||
if (typeof unwatchOption === "function") {
|
||||
unwatchOption();
|
||||
unwatchOption = null;
|
||||
() => props.option,
|
||||
(option) => {
|
||||
if (!option) {
|
||||
lastSignature = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!manualUpdate) {
|
||||
unwatchOption = watch(
|
||||
() => props.option,
|
||||
(option) => {
|
||||
if (!option) {
|
||||
lastSignature = undefined;
|
||||
return;
|
||||
}
|
||||
if (!chart.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyOption(chart.value, option);
|
||||
},
|
||||
{ deep: true },
|
||||
if (manualUpdate.value) {
|
||||
warn(
|
||||
"`option` prop changes are ignored when `manual-update` is `true`.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chart.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyOption(chart.value, option);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
realInitOptions,
|
||||
[manualUpdate, realInitOptions],
|
||||
() => {
|
||||
cleanup();
|
||||
init();
|
||||
|
||||
@@ -26,25 +26,26 @@ export type PublicMethods = Pick<EChartsType, MethodName>;
|
||||
export function usePublicAPI(
|
||||
chart: Ref<EChartsType | undefined>,
|
||||
): PublicMethods {
|
||||
function makePublicMethod<T extends MethodName>(
|
||||
name: T,
|
||||
): (...args: Parameters<EChartsType[T]>) => ReturnType<EChartsType[T]> {
|
||||
return (...args) => {
|
||||
function makePublicMethod<T extends MethodName>(name: T): EChartsType[T] {
|
||||
// Return a function that matches the signature of EChartsType[T]
|
||||
const fn = function (this: unknown, ...args: unknown[]): unknown {
|
||||
if (!chart.value) {
|
||||
throw new Error("ECharts is not initialized yet.");
|
||||
}
|
||||
return (chart.value[name] as any).apply(chart.value, args);
|
||||
// Use Reflect.apply to call the method with proper context
|
||||
return Reflect.apply(chart.value[name], chart.value, args);
|
||||
};
|
||||
return fn as EChartsType[T];
|
||||
}
|
||||
|
||||
function makePublicMethods(): PublicMethods {
|
||||
const methods = Object.create(null);
|
||||
METHOD_NAMES.forEach((name) => {
|
||||
methods[name] = makePublicMethod(name);
|
||||
});
|
||||
// Build the methods object with proper typing
|
||||
const methods = METHOD_NAMES.reduce(
|
||||
(acc, name) => {
|
||||
acc[name] = makePublicMethod(name);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<MethodName, unknown>,
|
||||
) as PublicMethods;
|
||||
|
||||
return methods as PublicMethods;
|
||||
}
|
||||
|
||||
return makePublicMethods();
|
||||
return methods;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@ import {
|
||||
onMounted,
|
||||
shallowRef,
|
||||
shallowReactive,
|
||||
warn,
|
||||
} from "vue";
|
||||
import type { Slots, SlotsType } from "vue";
|
||||
import type { Option } from "../types";
|
||||
import { isBrowser, isValidArrayIndex, isSameSet } from "../utils";
|
||||
import { isBrowser, isValidArrayIndex, isSameSet, warn } from "../utils";
|
||||
import type { TooltipComponentFormatterCallbackParams } from "echarts";
|
||||
|
||||
const SLOT_OPTION_PATHS = {
|
||||
@@ -52,7 +51,11 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
ref: (el) => (containers[slotName] = el as HTMLElement),
|
||||
ref: (el) => {
|
||||
if (el instanceof HTMLElement) {
|
||||
containers[slotName] = el;
|
||||
}
|
||||
},
|
||||
style: { display: "contents" },
|
||||
},
|
||||
slotContent,
|
||||
@@ -62,38 +65,68 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) {
|
||||
: undefined;
|
||||
};
|
||||
|
||||
// Shallow-clone the option along the path and override the target callback
|
||||
// Helper to check if a value is a plain object
|
||||
function isObject(val: unknown): val is Record<string, unknown> {
|
||||
return val !== null && typeof val === "object" && !Array.isArray(val);
|
||||
}
|
||||
|
||||
// Shallow-clone the option along each path and override the target callback
|
||||
function patchOption(src: Option): Option {
|
||||
const root = { ...src };
|
||||
const root = { ...src } as Record<string, unknown>;
|
||||
|
||||
// Ensure the child at `seg` is a writable container (cloned or newly created).
|
||||
// Returns the child container, or undefined if traversal is blocked by a primitive.
|
||||
const ensureChild = (
|
||||
parent: Record<string, unknown>,
|
||||
seg: string,
|
||||
): Record<string, unknown> | undefined => {
|
||||
const next = parent[seg];
|
||||
|
||||
if (Array.isArray(next)) {
|
||||
parent[seg] = [...next];
|
||||
return parent[seg] as Record<string, unknown>;
|
||||
}
|
||||
if (isObject(next)) {
|
||||
parent[seg] = { ...next };
|
||||
return parent[seg] as Record<string, unknown>;
|
||||
}
|
||||
if (next === undefined) {
|
||||
parent[seg] = isValidArrayIndex(seg) ? [] : {};
|
||||
return parent[seg] as Record<string, unknown>;
|
||||
}
|
||||
// Blocked by a non-container value
|
||||
return undefined;
|
||||
};
|
||||
|
||||
Object.keys(slots)
|
||||
.filter((key) => {
|
||||
const isValidSlot = isValidSlotName(key);
|
||||
if (!isValidSlot) {
|
||||
warn(`Invalid vue-echarts slot name: ${key}`);
|
||||
const valid = isValidSlotName(key);
|
||||
if (!valid) {
|
||||
warn(`Invalid slot name: ${key}`);
|
||||
}
|
||||
return isValidSlot;
|
||||
return valid;
|
||||
})
|
||||
.forEach((key) => {
|
||||
const path = key.split("-");
|
||||
const prefix = path.shift() as SlotPrefix;
|
||||
path.push(...SLOT_OPTION_PATHS[prefix]);
|
||||
|
||||
let cur: any = root;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const seg = path[i];
|
||||
const next = cur[seg];
|
||||
|
||||
// Shallow-clone the link; create empty shell if missing
|
||||
cur[seg] = next
|
||||
? Array.isArray(next)
|
||||
? [...next]
|
||||
: { ...next }
|
||||
: isValidArrayIndex(seg)
|
||||
? []
|
||||
: {};
|
||||
cur = cur[seg];
|
||||
const [prefix, ...rest] = key.split("-") as [SlotPrefix, ...string[]];
|
||||
const tail = SLOT_OPTION_PATHS[prefix];
|
||||
if (!tail) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = [...rest, ...tail];
|
||||
if (path.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse to the parent of the leaf, cloning or creating along the way
|
||||
let cur: Record<string, unknown> | undefined = root;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
cur = cur && ensureChild(cur, path[i]);
|
||||
if (!cur) {
|
||||
return; // Blocked by a primitive — skip this key
|
||||
}
|
||||
}
|
||||
|
||||
cur[path[path.length - 1]] = (p: unknown) => {
|
||||
initialized[key] = true;
|
||||
params[key] = p;
|
||||
@@ -101,7 +134,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) {
|
||||
};
|
||||
});
|
||||
|
||||
return root;
|
||||
return root as Option;
|
||||
}
|
||||
|
||||
// `slots` is not reactive, so we need to watch it manually
|
||||
|
||||
16
src/utils.ts
16
src/utils.ts
@@ -1,3 +1,5 @@
|
||||
import { warn as vueWarn } from "vue";
|
||||
|
||||
type Attrs = Record<string, any>;
|
||||
|
||||
export function isBrowser(): boolean {
|
||||
@@ -34,10 +36,14 @@ export function isSameSet<T>(a: T[], b: T[]): boolean {
|
||||
const setA = new Set(a);
|
||||
const setB = new Set(b);
|
||||
|
||||
if (setA.size !== setB.size) return false;
|
||||
if (setA.size !== setB.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const val of setA) {
|
||||
if (!setB.has(val)) return false;
|
||||
if (!setB.has(val)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -46,3 +52,9 @@ export function isSameSet<T>(a: T[], b: T[]): boolean {
|
||||
export function isPlainObject(v: unknown): v is Record<string, unknown> {
|
||||
return v != null && typeof v === "object" && !Array.isArray(v);
|
||||
}
|
||||
|
||||
const LOG_PREFIX = "[vue-echarts]";
|
||||
|
||||
export function warn(message: string): void {
|
||||
vueWarn(`${LOG_PREFIX} ${message}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user