Способ разместить Яндекс.Карту в ShadowDOM

Valentin Fedyakov
3 min readMay 27, 2019

--

Photo by Jaromír Kavan on Unsplash

При разработке компонента, который предоставлял бы пользователю интерактивную Яндекс.Карту (далее — карта), я столкнулся с багом. И не я один (раз, два). Приведу пример.

Создадим веб-компонент, в котором попытаемся инкапсулировать карту. Возьмем из примеров яндекса самый простой код, например, такой.

Определим пользовательский элемент, в котором будет работать карта

class UCYandexMap extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.render();
this.initMap();
}
initMap() {
ymaps.ready(() => {
this.myMap = new ymaps.Map(this.querySelector("#map"), {
center: [55.76, 37.64],
zoom: 10
});
});
}
render() {
this.innerHTML = `
<style>
#map {
height: 100%;
width: 100%;
}
</style>
<div id="map"></div>
`;
}
}
window.customElements.define("uc-yandex-map", UCYandexMap);

и небольшой html

<!DOCTYPE html><html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Яндекс карта</title>
<script
src="https://api-maps.yandex.ru/2.1/?lang=ru_RU&apikey=<ваш ключ>"
type="text/javascript"
></script>
<script src="index.js" type="text/javascript"></script>
<style>
body,
html {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<uc-yandex-map></uc-yandex-map>
</body>
</html>

В результате получим следующее

Яндекс карта не в ShadowDOM

Карта отображается и занимает определенное ей место.

Теперь инкапсулируем стилизацию и структуру карты в ShadowDOM. Внесем небольшое изменение в компонент.

class UCYandexMap extends HTMLElement {
constructor() {
...
this.attachShadow({mode: "open"});
}
initMap() {
...
this.myMap = new ymaps.Map(
this.shadowRoot.querySelector("#map"), {
...
}
render() {
this.shadowRoot.innerHTML = ...
}
}

В результате карта теряет размер — на контейнер в 400px создается карта размером 656px.

Яндекс карта в ShadowDOM

При этом пропадают все элементы карты (копирайт, зум и т.д.), а в левом верхнем углу появляется лейбл “Слои”. Нет возможности добавить метки или балуны, они просто не появляются на карте.

Такое происходит при любом попадании карты в ShadowDOM.

Могу предположить, что это происходит из-за того, что карты используют структуру документа, а с shadowDOM она получается изолированна и не доступна из вне просто так.

Чтобы обойти данный баг, я бы предложил использовать iframe как открытую структуру, с которой сможет работать карта.

class UCYandexMap extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.IFrame = null;
this.IFrameDocument = null;
this.IFrameWindow = null;
}
connectedCallback() {
this.render();
this.initIframe();
this.initMap();
}
initIframe() {
// получим элемент iframe и две его глобальные переменные
// document и window
this.IFrame = this.shadowRoot.querySelector("iframe");
this.IFrameDocument = this.IFrame.contentDocument;
this.IFrameWindow = this.IFrame.contentWindow;
// создадим скрипт который подтянет в iframe карту
const script = document.createElement("script");
script.setAttribute(
"src",
"https://api-maps.yandex.ru/2.1/?lang=ru_RU&apikey=<ваш ключ>"
);
script.onload = this.initMap.bind(this);
// создадим контейнер для карты
this.mapElement = document.createElement("div");
// зададим размеры, что бы карта занимала весь iframe
this.mapElement.style.height = "100vh";
this.mapElement.style.width = "100vw";
// добавим элементы в iframe
this.IFrameDocument.body.appendChild(script);
this.IFrameDocument.body.appendChild(this.mapElement);
this.IFrameDocument.body.style.margin = "0";
}
initMap() {
// инициализируем карту используя window из iframe
const { ymaps } = this.IFrameWindow;
ymaps.ready(() => {
this.myMap = new ymaps.Map(this.mapElement, {
center: [55.76, 37.64],
zoom: 10
});
});
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
height: 400px;
width: 400px;
display: block;
}
iframe {
height: 100%;
width: 100%;
}
</style>
<iframe scrolling="no" frameborder="0"></iframe>
`;
}
}
window.customElements.define("uc-yandex-map", UCYandexMap);

и уберем из html лишний скрипт

<script
src="https://api-maps.yandex.ru/2.1/?lang=ru_RU&apikey=<ваш ключ>"
type="text/javascript"
></script>

И получаем следующий результат

Яндекс карта в ShadowDOM используя iframe

Карта находится в веб-компоненте. Изолированна с помощью ShadowDOM, управляема и, при этом, без бага. Все, что для этого нужно — небольшая функция initIframe(). Я понимаю, что использовать iframe в ShadowDOM — это “масло масляное”. Но, на мой взгляд, при отстствии вариантов решения этой проблемы (google maps не предлагать) — вполне допустимо. Я не отрицаю, что возможно существует какое-то другое решение, но я его не нашел.

--

--