[Vue.js] Implementing Vue Kakao Maps 03 - Implementing Overlay Popup

Implementing an Overlay popup for Kakao Maps in Vue.js.

[Vue.js] Vue Kakao Map Implementation 03 - Implementing the Overlay popup

project Structure

Project Structure
[ROOT] +- public | +- index.html +- src +- components/map | +- overlay | +- index.js (+) |Β | +- KakaoMap.vue (E) | +- marker-handler.js |Β +- service | +- api.js | +- app.vue (E) +- App.vue (E) +- main.js
  • overlay/index.js - Class responsible for the overlay
  • App.vue - Inserts the element to be used for overaly into KakaoMap
  • KakaoMap.vue - defines the element for overlay as slot

In Section1, I attach the complete source code.

for a detailed description, go to Section 2.

1. Source Code

src/components/map/overlay/index.js (+)

Class responsible for Overlay popup (new)

.../map/overlay/index.js
const kakao = window.kakao; class KakaoOverlay { constructor(vueMap, content) { this.vueMap = vueMap; this.content = content; // init instance this.instance = new kakao.maps.CustomOverlay({ map: null, clickable: false, content: content, position: null, xAnchor: 0.5, yAnchor: 1, zIndex: 3, }); } showAt(lat, lng) { // show at (lat, lng ) console.log("overlay show!!", lat, lng); this.instance.setMap(this.vueMap.mapInstance); const pos = new kakao.maps.LatLng(lat, lng); this.instance.setPosition(pos); } } hide() { this.instance.setMap(null); } } export default KakaoOverlay;

src/App.vue

define the element to be used for the overlay

src/App.vue
<template> <div> <div> <h3>kakao map demo(center, level)</h3> <div class="controll"> <button @click="zoom(-1)"> <span class="material-icons"> zoom_in </span> </button> <button @click="zoom(1)"> <span class="material-icons"> zoom_in</span> </button> <span class="material-icons"> zoom_out </span> </button> </div> <div class="map-area"> <div class="harbors"> <div class="harbors"> <div class="harbor" v-for="hbr in harbors" :key="hbr.seq" @click="showOnMap(hbr)" :class="{ active: hbr === activeHarbor }" > <h4>{{ hbr.place }}</h4> </div> </div> <KakaoMap ref="kmap" class="kmap" :options="mapOption"> <div class="overlay-popup" ref="harborOverlay" slot="overlay"> <div v-if="overlayHarber"> <h3>{{ overlayHarber.place }}</h3> <div class="addr">{{ overlayHarber.addr }}</div> <a class="close" href="#" @click.prevent="closeOverlay()" ><span class="material-icons"> close </span></a > </div> </div> </KakaoMap> </div> </div> </template> <script> <script> import KakaoMap from "./components/map/KakaoMap.vue"; import api from "./service/api"; import MarkerHandler from "./components/map/marker-handler"; import KakaoOverlay from "./components/map/overlay"; export default { components: { KakaoMap, }, data() { return { mapOption: { center: { lat: 33.450701, lng: 126.570667, }, level: 8, }, harbors: [], // empty markers: null, // marker handler! activeHarbor: null, // selected harbor! overlay: null, // overlay instance overlayHarber: null, // harbor to show in overlay }; }, mounted() { const vueKakaoMap = this.$refs.kmap; this.markers = new MarkerHandler(vueKakaoMap, { markerClicked: (harbor) => { console.log("[clicked ]", harbor); // this.activeHarbor = harbor; this.showOnMap(harbor); // on marker click this.overlayHarber = harbor; this.overlay.showAt(harbor.lat, harbor.lng); }, }); this.overlay = new KakaoOverlay(vueKakaoMap, this.$refs.harborOverlay); api.harbor.all((res) => { console.log("[List of harbors]", res.harbors); this.harbors = res.harbors; // create markers this.markers.add(this.harbors, (harbor) => { return { lat: harbor.lat, lng: harbor.lng }; }); }); }, methods: { zoom(delta) { // delta : 1 or -1 // console.log("[delta]", delta); const level = Math.max(3, this.mapOption.level + delta); // min level 3 this.mapOption.level = level; // console.log(this.mapOption.level); }, showOnMap(harbor) { this.activeHarbor = harbor; // console.log("[center]", harbor); this.mapOption.center = { lat: harbor.lat, lng: harbor.lng, }; }, closeOverlay() { this.overlay.hide(); }, }, }; </script> </script <style lang="scss"> button { border: 1px solid transparent; padding: 6px; background-color: #efefefdd; border-radius: 6px; &:hover { background-color: #ddd; border-color: #ddd; cursor: pointer; } &:active { background-color: #aaa; border-color: #aaa; } } .map-area { display: flex; .harbors { .harbor { padding: 10px; border: 1px solid transparent; &:hover { background-color: aliceblue; border-color: #6a9dff; cursor: pointer; } &:active { background-color: rgb(166, 197, 224); border-color: #4471c5; } &.active { background-color: rgb(253, 229, 150); border-color: rgb(211, 173, 3); } h4 { margin: 0; } } } .kmap { flex: 1 1 auto; .overlay-popup { background-color: #ffffffcc; box-shadow: 0 0 8px #0000004d, 0 0 1px 2px #00000022; max-width: 200px; min-width: 160px; position: absolute; bottom: 44px; left: 50%; transform: translateX(-50%); h3 { margin: 0; padding: 8px; padding-right: 24px; background-color: #ed4215; color: white; font-weight: 400; font-size: 16px; } .addr { padding: 8px; white-space: break-spaces; } .close { color: black; position: absolute; top: 0; left: 100%; transform: translate(-50%, -50%); background-color: white; border-radius: 100%; line-height: 0; padding: 6px; box-shadow: 0 0 6px #0000004d; } } } } </style>

src/components/map/KakaoMap.vue

  • define named slot(<slot name="overlay"/>)
  • use panTo method instead of setCenter to move the center coordinate
KakaoMap.vue
<template> <div ref="map"> <slot name="overlay"></slot> </div> </template> <script> </script let kakao = window.kakao; export default { props: ["options"], data() { return { mapInstance: null, }; }, mounted() { kakao = kakao || window.kakao; console.log(this.$refs.map); // should be not null var container = this.$refs.map; const { center, level } = this.options; this.mapInstance = new kakao.maps.Map(container, { center: new kakao.maps.LatLng(center.lat, center.lng), level, }); }, watch: { "options.level"(cur, prev) { console.log(`[LEVEL CHANGED] ${prev} => ${cur}`); // for testing this.mapInstance.setLevel(cur); }, "options.center"(cur) { this.mapInstance.panTo(new kakao.maps.LatLng(cur.lat, cur.lng)); // pan smoothly }, }, }; </script> <style> .kmap { height: 600px; } </style>

2. Description

When implementing the CustomOverlay provided by the Kakao Maps API in a vue.js-based project, there are some issues that need to be solved.

the image below shows an example page of overlay usage

the corresponding code is shown below.

JAVASCRIPT
// The content to be displayed in the custom overlay can be either an HTML string or a document element var content = '<div class="customoverlay">' + ' <a href="https://map.kakao.com/link/map/11394059" target="_blank">' +' ' <span class="title">구구의야구공원</span>' + ' </a>' + '</div>';

assembling overlay HTML from strings like the above is something you should avoid in SPA projects like vue.js.

here, we're going to use the <slot/> provided by vue.js to render the overlay screen.

<slot /> Using slot /&gt

inside the KakaoMap component, paste the html we want to use in the overlay like below.

App.vue
<template> <div <div> <div> .... <div class="map-area"> .... <KakaoMap ref="kmap" class="kmap" :options="mapOption"> <div class="overlay-popup" ref="harborOverlay" slot="overlay"> <div> <h3>Harbor name location <h3>Harbor name location</h3> <div class="addr">Harbor Address Location</div> <a href="#">Close</a> </div> </div> </KakaoMap> </div> </div> </template>
  • prepared the title (h3) and address (.addr) to show the port information.
  • we also added a function to close the overlay by pressing the close button.
  • ref - refer to the element with ref to pass it when creating the overlay
  • slot="overlay" - Insert the slot named "overlay" inside the KakaoMap component.

now define the corresponding slot inside KakaoMap.vue.

KakaoMap.vue
<template> <div <div ref="map"></slot <slot name="overlay"></slot> </div> </template>

when the application starts correctly, the slot position will be replaced with the element of the overlay that you inserted in App.vue, as shown below.

KakaoMap.vue
<template> <div ref="map <div ref="map"> <div class="overlay-popup"> <div class="overlay-popup"> <div> <div> <h3>Port name location</h3> <div class="addr">Port address location</div> <a href="#">Close</a> </div> </div> </div> </template>

<div ref="map"> is a placeholder where the Kakao Maps library inserts map-related images. The elements of the overlay exist as children of the placehoder, but they are moved to a different place when the overlay window is opened.

The click event that you want to assign to the close button in App.vue still works even though the element in the overlay is moved elsewhere.

Create the KakaoOverlay class

in the official documentation, the code to bring up the overlay is given below.

Using javascript

the class KakaoOverlay is where we hide the specific overlay creation process and add additional functionality.

.../map/overlay/index.js
const kakao = window.kakao; class KakaoOverlay { constructor(vueMap, content) { this.vueMap = vueMap; this.content = content; // init instance this.instance = new kakao.maps.CustomOverlay({ ... }); } showAt(lat, lng) { // renders the overlay at the given location } hide() { // closes the overlay if null is specified, as shown below this.instance.setMap(null); } } export default KakaoOverlay;
  • Regular js files, not Vue files
  • vueMap - an instance of the file KakaoMap.vue (VueComponent)
  • content - the on-screen content of the overlay. the official documentation mostly shows examples of assembling HTML directly, but you can also pass a DOM element to this location. in this case, we pass the overlay element that we injected from App.vue.

The KakaoOverlay class is used like below in App.vue.

App.vue
<template> </template> <script> ... // (1) Import import KakaoOverlay from "./components/map/overlay"; export default { data() { return { ... overlay: null, // (2) for reference to overlay instances } }, mounted() { const vueKakaoMap = this.$refs.kmap; this.markers = .... // (3) Pass in a map component and an overlay element this.overlay = new KakaoOverlay(vueKakaoMap, this.$refs.harborOverlay); .... }, } </script>
  • (1) Import the KakaoOverlay class
  • (2) Variable to reference the overlay instance
  • (3) Call the constructor passing in the map component (an instance of KakaoMap.vue) and the element reference to use as the content of the overlay

now, in the KakaoOverlay class, we prepare the actual overlay as shown below.

.../map/overlay/index.js
const kakao = window.kakao; class KakaoOverlay { constructor(vueMap, content) { this.vueMap = vueMap; this.content = content; // init instance this.instance = new kakao.maps.CustomOverlay({ map: null, clickable: false, content: content, xAnchor: 0.5, yAnchor: 1, zIndex: 3, }); } .... } export default KakaoOverlay;
  • When using the CustomOverlay class provided by the Kakao Maps library, we specify null for the map property, so we don't display it on the screen yet.
  • xAnchor: 0.5 - coordinates to be centered on the left and right width of the overlay screen
  • yAnchor: 1 - position the coordinates at the y-axis of the overlay screen's height
  • zIndex: You can also adjust this value to make certain overlays appear on top of other overlays.

note that the coordinates don't actually move (it's the overlay that does)

TXT
(xAhcnor: 0.5, yAnchor: 1) BBBBB BBBBB BBBBB + (xAhcnor: 0.5, yAnchor: 0) + (xAhcnor: 0.5) BBBBB BBBBB BBBBB (xAhcnor: 0, yAnchor: 0) +0 BBBBB BBBBB BBBBB (xAhcnor: 0.5, yAnchor: 0.5) BBBBB BB+BB BBBBB

click the ### marker

now in App.vue, when you click on the marker, we'll pass in the location of the harbor to bring up the overlay.

App.vue
export default { mounted() { .... this.markers = new MarkerHandler(vueKakaoMap, { markerClicked: (harbor) => { .... this.showOnMap(harbor); // show overlay on marker click this.overlay.showAt(harbor.lat, harbor.lng); }, }); this.overlay = ... .... },
  • call showAt(lat, lng) to show the overlay

Implement the showAt method in the KakaoOverlay class.

.../map/overlay/index.js
const kakao = window.kakao; class KakaoOverlay { constructor(vueMap, content) { this.vueMap = vueMap; this.content = content; this.instance = .... .... } showAt(lat, lng) { this.instance.setMap(this.vueMap.mapInstance); const pos = new kakao.maps.LatLng(lat, lng); this.instance.setPosition(pos); } }

Overly Design

In App.vue, design the overlay like below.

App.vue
<style lang="scss"><style lang="scss"></style .... .map-area { .kmap { .... .overlay-popup { background-color: #ffffffcc; box-shadow: 0 0 8px #0000004d, 0 0 1px 2px #00000022; max-width: 200px; min-width: 160px; h3 { margin: 0; padding: 8px; padding-right: 24px; background-color: #ed4215; color: white; font-weight: 400; font-size: 16px; } .addr { padding: 8px; white-space: break-spaces; } } } } </style>

when you click the marker, an overlay appears on top of the coordinates, as shown below.

the overlay is rendered in the desired position, but it overlaps the marker and doesn't look good. add a style to the .overlay-popup that modifies the position.

App.vue
<style lang="scss"> .... .map-area { .kmap { .... .overlay-popup { .... position: absolute; bottom: 44px; left: 50%; transform: translateX(-50%); h3 { ... } .addr { ... } } } } </style>
  • bottom: 44px - spacing by the height of the base marker so they don't overlap
  • left, transform - centered alignment

See the video for the close button design https://youtu.be/2wEKmcdVWmA

Fill the Overlay Content

we want to show the port information in the overlay when the marker is clicked.

define a variable to represent the port to show in the overlay and bind the port name and address to the overay element.

App.vue
<template .... <KakaoMap ...> <div class="overlay-popup" ...> <div v-if="overlayHarber"><!-- (3) Rendering --> <!! <div v-if="overlayHarber"><!-- (3) Rendering --> <h3>{{ overlayHarber.place }}</h3> <div class="addr">{{ overlayHarber.addr }}</div> <a class="close" href="#" @click.prevent="closeOverlay()" ><span class="material-icons"> close </span></a > </div> </div> </KakaoMap> </template> <script> <script export default { data() { return { ... overlay: null, overlayHarber: null, // (1) Ports to show in the overlay } }, mounted() { .... this.markers = new MarkerHandler(vueKakaoMap, { markerClicked: (harbor) => { ... // on marker click this.overlayHarber = harbor; // (2) bind the clicked harbor this.overlay.showAt(harbor.lat, harbor.lng); }, }); .... }, methods: { ... closeOverlay() { this.overlay.hide(); // (4) Close the overlay } } } </script>
  • (1, 2) Bind the corresponding harbor on marker click to a variable (this.overlayHarber = harbor)
  • (3) Render overlayHarbor to the overlay element in the template
  • (4) Close the overlay when the close button is clicked

finally, implement the method hide() in the KakaoOverlay class.

.../map/overlay/index.js
const kakao = window.kakao; class KakaoOverlay { ... showAt(lat, lng) { ... } hide() { this.instance.setMap(null); } }