Khi làm việc với AMap (Gaode Maps) trong môi trường Vue 3, thách thức phổ biến nhất là làm thế nào để hiển thị một Vue Component bên trong cửa sổ thông tin (InfoWindow). Thông thường, InfoWindow của AMap chỉ nhận chuỗi HTML hoặc các phần tử DOM thuần túy. Để tận dụng sức mạnh của Vue như reactivity và quản lý trạng thái, chúng ta cần một cơ chế render động.
Cách tiếp cận cơ bản với HTML thuần
Thông thường, AMap cho phép tạo InfoWindow bằng cách truyền một chuỗi HTML trực tiếp vào thuộc tính content:
const infoWindowContent = `
<div class="custom-card">
<h4>Thông tin địa điểm</h4>
<div class="content-body">
<p>Tọa độ hiện tại:</p>
<input id="coord-input" type="text" />
</div>
<button onclick="fetchCoordinates()">Lấy tọa độ</button>
</div>
`;
const popup = new AMap.InfoWindow({
position: [116.39, 39.9],
offset: new AMap.Pixel(0, -25),
content: infoWindowContent
});
Tuy nhiên, cách này khiến chúng ta khó quản lý các logic phức tạp hoặc sử dụng lại các component có sẵn trong dự án Vue.
Xây dựng Composable để Render Component động
Để giải quyết vấn đề này, chúng ta có thể sử dụng hàm h (createVNode) và render của Vue 3 để gắn (mount) một component vào một phần tử DOM trước khi đưa nó vào InfoWindow.
import { h, render, getCurrentInstance, Component, AppContext } from "vue";
interface PopupOptions {
map: AMap.Map;
position: AMap.LngLat | [number, number];
data: any;
component: Component;
preventScroll?: boolean;
handlers?: Record<string, (...args: any[]) => void>;
}
export function useMapPopup() {
let windowInstance: AMap.InfoWindow | null = null;
let mountPoint: HTMLDivElement | null = null;
const { appContext } = getCurrentInstance() || {};
const initWindow = () => {
if (!windowInstance) {
windowInstance = new AMap.InfoWindow({
offset: new AMap.Pixel(0, -32),
isCustom: true, // Cho phép tùy chỉnh hoàn toàn UI
autoMove: true,
});
}
return windowInstance;
};
const showPopup = (options: PopupOptions) => {
const { map, position, data, component, preventScroll = true, handlers = {} } = options;
try {
const iw = initWindow();
hidePopup(); // Dọn dẹp cửa sổ cũ nếu có
mountPoint = document.createElement("div");
mountPoint.className = "amap-vue-popup-wrapper";
// Chuyển đổi event handlers từ 'click' thành 'onClick'
const componentProps = {
payload: data,
instance: iw,
...Object.keys(handlers).reduce((acc, key) => {
const eventName = `on${key.charAt(0).toUpperCase() + key.slice(1)}`;
acc[eventName] = handlers[key];
return acc;
}, {} as any)
};
const vnode = h(component, componentProps);
// Kế thừa context để sử dụng được Pinia, Router...
if (appContext) {
vnode.appContext = appContext;
}
// Xử lý ngăn chặn sự kiện cuộn chuột làm zoom bản đồ khi đang thao tác trên InfoWindow
if (preventScroll) {
mountPoint.addEventListener("wheel", (e) => e.stopPropagation());
}
render(vnode, mountPoint);
iw.setContent(mountPoint);
iw.open(map, position);
} catch (error) {
console.error("Failed to render InfoWindow component:", error);
}
};
const hidePopup = () => {
if (windowInstance) {
windowInstance.close();
if (mountPoint) {
render(null, mountPoint);
mountPoint = null;
}
}
};
return {
showPopup,
hidePopup,
instance: windowInstance
};
}
Định nghĩa Component cho InfoWindow
Component được sử dụng cần nhận payload (dữ liệu) và instance (đối tượng InfoWindow) thông qua props để có thể tương tác ngược lại.
// LocationDetail.vue
interface Props {
payload: {
id: string;
name: string;
};
instance: AMap.InfoWindow;
}
const props = defineProps<Props>();
const emit = defineEmits(['update', 'delete']);
const handleUpdate = () => {
emit('update', props.payload.id);
};
Sử dụng trong thực tế
Dưới đây là cách gọi Composable khi người dùng tương tác với các điểm đánh dấu trên bản đồ:
const { showPopup } = useMapPopup();
function onMarkerClick(event: any) {
const targetData = event.target.getExtData();
showPopup({
map: mapContext.getMap(),
position: event.lnglat,
data: targetData,
component: LocationDetailComponent,
handlers: {
update: (id) => {
console.log("Cập nhật địa điểm:", id);
},
delete: (id) => {
console.log("Xóa địa điểm:", id);
}
}
});
}
Bằng cách sử dụng appContext, các Component được render động này vẫn có thể truy cập đầy đủ vào các plugin của Vue dự án như Vuex, Pinia hoặc Vue Router, giúp việc phát triển các ứng dụng bản đồ phức tạp trở nên dễ dàng và đồng nhất hơn.