Создаём кастомный маркер в react-yandex-maps через iconLayout: template

от автора

Всем привет, не так давно ко мне в команду в ПРОФИ пришла задача реализации довольно комплексной (в плане верстки и интерактивности) карты, на которой бы отображались заказы, оставленные нашими клиентами. Мы решили использовать фреймворк, адаптирующий яндексовый SDK под реакт.

react-yandex-maps + доки к нему

UI маркеров почти полностью приходится настраивать по докам уже Яндекса, тк react-yandex-maps предоставляет нам только внешний интерфейс, позволяющий удобно прокинуть параметры в объект маркера как пропсы.

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

Что нам нужно было реализовать

Нужен был пин, который состоит из чёрной точки и описания-балуна сверху. При том некоторые пины в дефолтном состоянии должны рисоваться без описания, которое будет появляться только по ховеру или при переходе пина в активное состояние. Более того, у пинов есть несколько особенностей:

Анимированная реакция на ховеры

Анимированный переход в active-state

Анимированное изменение вёрстки по ховеру на другой элемент (по изменению пропсов маркера)

Способ реализации

Для реализации кастомного пина будем использовать поле iconLayout пропа options компонента Placemark

<Placemark   geometry={[pin.coordinates.lat, pin.coordinates.lon]}   options={{    iconLayout: template,       }} />

В это поле нам надо будет пробросить template нашего кастомного компонента. О способах создания темплейтов можно почитать в доках Яндекса

Пример, как будет выглядеть наш компонент

type Props = {     id: string;     onClick: (orderId: string) => void;   mapInstanceRef: YMapsApi | null; };  const OrdersPin: React.FC<Props> = React.memo(     ({id, mapInstanceRef, onClick}) => {         // Тут я достаю все данные для моего пина из редакса     const pin = useSelector(createMapPlacemarkByIdSelector(id)) as PinData;    if (!mapInstanceRef) return null;              // Тут создаю template для проброса в iconLayout, о createPinTemplateFactory дальше     const template = createPinTemplateFactory(mapInstanceRef)({             onPinClick: onClick,             description: pin.description,             isActive: pin.isActive,             isViewed: pin.isViewed,         });             return (             <Placemark               // Проброс позиции пина на карте       geometry={[pin.coordinates.lat, pin.coordinates.lon]}           options={{        // Проброс темплейта   iconLayout: template,             }}       />     );   }, );

О темплейтах

Если кратко — схема работы с темплейтами выглядит так:

 const layout = ymaps.templateLayoutFactory.createClass(   '<Тут вёрстка>',    {     build:      function() {      layout.superclass.build.call(this);       <Тут JS>     }   } );

Этот layout и пробрасываем в iconLayout

Если вы обратили внимание на пример компонента, который я привёл выше, можете заметить, что я в своём коде вызываю createPinTemplateFactory сначала с mapInstanceRef, а потом результат с параметрами пина (onClick, isActive и так далее).

В целом можно сделать без фабрики, сразу получать layout через ymaps.templateLayoutFactory.createClass и пробрасывать в iconLayout

Про параметр createClass

Первый параметр тут — строка с вёрсткой. Да, вы не ослышались, кидаем сюда строку с HTML. Тут накидываем общий вид компонента и закидываем дефолтные стили через className

Благо, что мы можем удобно пробрасывать данные в строки, используя Интерполяцию выражений (структуру вида `Привет, я ${name}`)

Пример первого параметра, как он выглядит у меня:

`<div class="pin-container">       <div class="placemark-description">             <p class="placemark-description__title">     ${description.title}</p>              <p class="placemark-description__subtitle">                   ${description.subtitle.prefix}                     <span class="placemark-description__price">       ${description.subtitle.body}       </span>                     ${description.subtitle.postfix}               </p>         </div>         <div class="pin-container__pin">               <div class="placemark__background"></div>         </div> </div>`

Тут описываю контейнер, в нём компонент «описания» и компонент самого «пина» (для фона я использую отдельный див, это нужно, чтобы, при увеличении пина по ховеру, его центр не сдвигался)

Второй параметр — JS, который вызовется при создании компонента. Тут описываем состояния, используя пропсы и навешивая listener’ы эвентов geoObject

У себя я поделил код тут на несколько частей:

Получение элементов для дальнейшего взаимодействия с ними

// GET ELEMENTS const pinContainer = this.getParentElement().getElementsByClassName( 'pin-container', )[0];  const backgroundElement = this.getParentElement().getElementsByClassName( 'placemark__background', )[0];  ...

Обработка ховера через mouseenter и mouseleave

Тут по ховеру увеличивается размер пина через модификацию стилей backgroundElement и показывается описание пина. На mouseleave выполняется сброс состояния

(можно написать и красивее, тут так написано для максимальной читаемости)

// HOVER LAYOUT           this.getData().geoObject.events.add(             'mouseenter',             () => {               backgroundElement.style.top = `-${PIN_EXPANDED_INSET}px`;               backgroundElement.style.bottom = `-${PIN_EXPANDED_INSET}px`;               backgroundElement.style.left = `-${PIN_EXPANDED_INSET}px`;               backgroundElement.style.right = `-${PIN_EXPANDED_INSET}px`;                descriptionElement.style.transform = `translateY(-${PIN_EXPANDED_INSET}px)`;               if (isDescriptionHidden) {                 descriptionElement.style.opacity = 1;               }             },             this,           );            this.getData().geoObject.events.add(             'mouseleave',             () => {               // if placemark is active leave hover layout unaffected               if (!isActive) {                 backgroundElement.style.top = '0px';                 backgroundElement.style.bottom = '0px';                 backgroundElement.style.left = '0px';                 backgroundElement.style.right = '0px';                  descriptionElement.style.transform = 'translateY(0px)';                 if (isDescriptionHidden) {                   descriptionElement.style.opacity = 0;                 }               }             },             this,           );

Выставление кликабельной зоны маркера через shape

Тут я управляю кликабельной зоной маркера.

// TOUCHABLE ZONE SHAPE if (isDescriptionHidden) {   // При отсутствии описания у пина он представляет из себя круг и я    // использую для кликабельной зоны форму круга.             this.getData().options.set(               'shape',               getMapMarkerShape(pinSize, pinSize, 'Circle'),             );           } else {             // В случае, если описание отображено, используется форма прямоугольника             this.getData().options.set('shape', {               type: MarkerShape.RECTANGLE,               coordinates: [                 [-translateLeft, -translateTop],                 [                   descriptionElement.offsetWidth - translateLeft,                   elementHeight - translateTop,                 ],               ],             });           }

Привязка onClick

this.getData().geoObject.events.add('click', onPinClick, this);

Не буду указывать тут весь остальной код, тк его довольно много, и он довольно однообразен.

Общая суть:

  • Мы имеем доступ к пропсам нашего маркера и можем использовать их в JS части createClass

  • Меняем стили элементов, полученных через this.getParentElement().getElementsByClassName(...)[0]; в зависимости от пропсов

  • Подвязываемся на ивенты geoObject для обработки ховеров и кликов

  • Не забываем, что стилизация работает и через className и css. Вероятно, грубая стилизация через JS и не понадобится. Анимации можем довольно просто реализовывать через css transition

  • Не забываем указывать shape, чтобы пользователям было удобно взаимодействовать с получившимся маркером

Итоги

Мы можем делать красивые маркеры для react-yandex-maps, не ограничиваясь статичными картинками. Конечно, изменение стилей через JS выглядит не очень красиво, но по итогу работает довольно плавно и стабильно, если писать аккуратно 🙂


ссылка на оригинал статьи https://habr.com/ru/post/677874/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *