Система сущностей

Карта в JS API может быть представлена как совокупность различных объектов, которые объединены в общую древовидную структуру. Различные типы этих объектов взаимодействуют друг с другом, формируя систему сущностей.

В данном разделе будет рассмотрена эта система, её типы сущностей, их особенности и назначение.

Система сущностей является императивной частью JS API карт.

Базовые сущности

Базовые сущности представляют из себя абстрактные классы, компоненты дерева создаются благодаря наследованию от них.

Существуют следующие виды сущностей:

  • YMapEntity – базовая сущность.
  • YMapComplexEntity – сущность, которая может иметь собственное поддерево сущностей, но не имеет публичного интерфейса взаимодействия с ним.
  • YMapGroupEntity – аналогична YMapComplex Entity, но имеет публичный интерфейс для взаимодействия с поддеревом сущностей.
  • YMapCollection – коллекция объектов-сущностей карты.
  • YMapContext – позволяет создать контекст, который компоненты могут предоставлять или считывать.
  • YMapContainer – позволяет передавать необходимые параметры компонентам библиотек react и vue.

Пример соответствий к типам сущностей модуля ymaps3:

  • YMapEntity – YMapTileDataSource, YMapFeatureDataSource, YMapLayer, YMapListener, YMapFeature;
  • YMapComplexEntity – YMapDefaultSchemeLayer, YMapDefaultFeaturesLayer;
  • YMapGroupEntity – YMapControls, YMapControl, YMapMarker;
  • YMapCollection – YMapControls, YMapControl, YMapMarker.

Root Entity

RootEntity – корневая сущность, которая не может быть частью поддерева другой сущности. Аналогично YMapGroupEntity имеет публичный интерфейс управления собственным поддеревом.

Помимо остальных типов сущностей не нужно писать собственную реализацию корневой сущности. Для этого в модуле ymaps3 существует класс YMap, который должен использоваться для создания дерева и является корневым для YMapEntity, YMapComplexEntity, YMapGroupEntity.

Важно

Для определения собственных сущностей рекомендуется наследоваться только от YMapEntity, YMapComplexEntity, YMapGroupEntity. При наследовании от других классов не гарантируется обратная совместимость.

Параметры сущности по умолчанию

У сущности могут быть необязательные параметры, но для них можно задать значения по умолчанию. Это можно сделать двумя способами:

  • Если значение по умолчанию статично, используйте static поле класса defaultProps.
  • Если значение по умолчанию вычисляется динамически, примените protected метод _getDefaultProps.

Например, у сущности есть необязательный параметр name, но мы хотим установить значение по умолчанию 'some-entity' в случае, если оно не указано явно.

Пример для статичного значения параметра name по умолчанию:

type YMapSomeEntityProps = {
  name?: string;
};

const defaultProps = Object.freeze({name: 'some-entity'});
type DefaultProps = typeof defaultProps;

class YMapSomeEntity extends ymaps3.YMapEntity<YMapSomeEntityProps, DefaultProps> {
  static defaultProps = defaultProps;
}

Аналогичный пример, но для динамически рассчитываемого значения по умолчанию:

type YMapSomeEntityProps = {
  id?: string;
  name?: string;
};

type DefaultProps = {name: string};

class YMapSomeEntity extends ymaps3.YMapGroupEntity<YMapSomeEntityProps, DefaultProps> {
  private static _uid = 0; // entity counter

  protected _getDefaultProps(props: YMapSomeEntityProps): DefaultProps {
    const id = props.name !== undefined ? `id-${props.name}` : `id-${YMapSomeEntity._uid++}_auto`;
    return {id};
  }
}

Примечание

Чтобы опциональные параметры со значением по умолчанию не были опциональными в TypeScript, необходимо указать тип значения по умолчанию в generic классе.

Для этого мы создали тип DefaultProps и передали его во второй generic.

Привязка сущности к DOM

Сущности никак не связаны с DOM деревом, поскольку все они располагаются в отдельном виртуальном дереве. Чтобы привязать сущность к DOM-элементу, при этом сохранить порядок и вложенность сущностей, необходимо использовать хук useDomContext:

class YMapDOMEntity extends ymaps3.YMapGroupEntity<{text: string}> {
  private _element?: HTMLHeadingElement;
  private _container?: HTMLDivElement;
  private _detachDom?: () => void;

  protected _onAttach(): void {
    // Создаем DOM-элемент, который будет привязан к сущности.
    this._element = document.createElement('div');
    this._element.textContent = this._props.text;
    this._element.classList.add('element');

    // Создаем контейнер, в который будут добавляться DOM-элементы дочерних сущностей.
    this._container = document.createElement('div');
    this._container.classList.add('container');

    // Вставляем контейнер внутрь элемента.
    this._element.appendChild(this._container);

    // Создаем привязку сущности к DOM.
    this._detachDom = ymaps3.useDomContext(this, this._element, this._container);
  }
  protected _onDetach(): void {
    // Открепляем DOM от сущности и удаляем ссылки на элементы.
    this._detachDom?.();
    this._detachDom = undefined;
    this._element = undefined;
    this._container = undefined;
  }
}
// Инициализация корневого элемента.
map = new ymaps3.YMap(document.getElementById('app'), {location: LOCATION});
// Инициализируем сущности класса YMapDOMEntity.
const DOMEntity = new YMapDOMEntity({text: 'DOM Entity'});
const childDOMEntity = new YMapDOMEntity({text: 'Child DOM Entity'});

// DOMEntity._element будет вставлен в DOM-элемент карты.
map.addChild(DOMEntity);
// childDOMEntity._element будет добавлен в DOMEntity._container.
DOMEntity.addChild(childDOMEntity);
.element {
  position: absolute;
  top: 0;
  width: 100%;
}
.container {
  position: absolute;
  top: 24px;
  width: 100%;
}

Данный хук доступен только при наследовании от классов YMapComplexEntity и YMapGroupEntity, поскольку подразумевается что сущность может иметь дочерние элементы. Хук принимает 3 аргумента:

  1. entity — ссылка на сущность к которой создается привязка DOM-элемента.
  2. element — DOM-элемент, к которой привязывается сущность.
  3. container — DOM-элемент, в который будут вставляться DOM-элементы дочерних сущностей (если сущность не подразумевает добавление дочерних элементов, то передается null).

Хук useDomContext возвращает функцию, которая удаляет привязку к DOM-элементу.

Proxy-контейнер

Proxy-контейнеры можно определить в производных классах от YMapComplexEntity и YMapGroupEntity через конструктор, передав вторым аргументом options.container.

Для начала стоит разобраться с некоторыми внутренними свойствами и механизмами этих классов без использования proxy-контейнера.

Внутри класса есть защищенное (protected) свойство _childContainer. По умолчанию это ссылка на текущий экземпляр (this). Также выше было сказано, что свойство children возвращает массив дочерних элементов. Но фактически дочерние элементы сущности хранятся в закрытом свойстве класса, а children всего лишь свойство-аксессор (accessor property) к нему.

Помимо уже известного addChild есть защищенный (protected) метод _addDirectChild (аналогично removeChild и _removeDirectChild), который добавляет дочернюю сущность напрямую во внутреннее поддерево. В этом его отличие от addChild, который добавляет сущность внутрь _childContainer.

Таким образом все дочерние сущности доступны через свойство children и их можно удалить из поддерева. Это может вызвать проблемы, когда у нас есть дочерний элемент, но мы не хотим давать к нему доступ внешнему пользователю:

import type {LngLat, YMapFeature} from '@yandex/ymaps3-types';

type YMapSomeProps = {
  coordinates: LngLat;
};

class YMapWithoutContainerEntity extends ymaps3.YMapGroupEntity<YMapSomeProps> {
  private _feature: YMapFeature;

  constructor(props: YMapSomeProps) {
    super(props); // YMapWithoutContainerEntity не имеет proxy-контейнера.

    this._feature = new ymaps3.YMapFeature({
      geometry: {
        type: 'Point',
        coordinates: this._props.coordinates
      }
    });
    this.addChild(this._feature); // Добавляем _feature во внутреннее поддерево.
    // this._addDirectChild(this._feature); — аналогичное действие.
  }

  private _removeFeature() {
    this.removeChild(this._feature); // Удаляем _feature из внутреннего поддерева.
    // this._removeDirectChild(this._feature); — аналогичное действие.
  }
}

const withoutContainerEntity = new YMapWithoutContainerEntity({coordinates: [0, 0]});
const [feature] = withoutContainerEntity.children; // Получаем доступ к withoutContainerEntity._feature.
withoutContainerEntity.removeChild(feature); // Можем удалить закрытую сущность из поддерева.

Теперь рассмотрим как изменится механизм добавления дочерних сущностей при использовании proxy-контейнера.

import type {LngLat, YMapFeature} from '@yandex/ymaps3-types';

type YMapSomeProps = {
  coordinates: LngLat;
};

class YMapWithContainerEntity extends ymaps3.YMapGroupEntity<YMapSomeProps> {
  private _feature: YMapFeature;

  constructor(props: YMapSomeProps) {
    super(props, {container: true}); // YMapWithContainerEntity имеет proxy-контейнер.

    this._feature = new ymaps3.YMapFeature({
      geometry: {
        type: 'Point',
        coordinates: this._props.coordinates
      }
    });
    // this.addChild(this._feature); — добавляет _feature в открытое поддерево внутри this._childContainer.
    this._addDirectChild(this._feature); // Добавляем _feature во внутреннее закрытое поддерево.
  }

  private _removeFeature() {
    // this.removeChild(this._feature); — удаляет _feature из открытого поддерева внутри this._childContainer.
    this._removeDirectChild(this._feature); // Удаляем _feature из внутреннего закрытого поддерева.
  }
}

const withContainerEntity = new YMapWithContainerEntity({coordinates: [0, 0]});

const publicFeature = ymaps3.YMapFeature({
  geometry: {type: 'Point', coordinates: [1, 1]}
});

withContainerEntity.addChild(publicFeature);

// Можем получить только publicFeature, поскольку withContainerEntity._feature находится в закрытом поддереве.
const [feature] = withContainerEntity.children;
withoutContainerEntity.removeChild(feature); // Можем удалить publicFeature из поддерева.

Во-первых, _childContainer теперь содержит proxy-контейнер — аналогичную экземпляру класса дочернюю сущность. Во-вторых, children теперь возвращает внутреннее поддерево внутри _childContainer, а не самого экземпляра класса. Такая замена позволяет сущности иметь два поддерева: открытое и закрытое.

Внутренне поддерево экземпляра класса является закрытым, потому что теперь к нему нет доступа и в нем хранятся _childContainer и сущности добавленные через _addDirectChild (которые можно удалить только через _removeDirectChild).

Внутреннее дерево _childContainer является открытым, поскольку его можно получить через публичное свойство children и в нем хранятся сущности добавленные через addChild (которые можно удалить только через removeChild).

Итого представим отличия сущностей с proxy-контейнером и без него в виде таблицы:

Без proxy-контейнера С использованием proxy-контейнера
_childContainer Ссылается на экземпляр класса (this) Ссылается на proxy-контейнер
children Возвращает внутреннее поддерево сущности Возвращает внутреннее поддерево proxy-контейнера
addChild Добавляет дочерние элементы во внутреннее поддерево сущности Добавляет дочерние элементы во внутреннее поддерево proxy-контейнера
_addDirectChild Добавляет дочерние элементы во внутреннее поддерево сущности Добавляет дочерние элементы во внутреннее поддерево сущности
removeChild Удаляет дочерние элементы из внутреннего поддерева сущности Удаляет дочерние элементы из внутреннего поддерева proxy-контейнера
_removeDirectChild Удаляет дочерние элементы из внутреннего поддерева сущности Удаляет дочерние элементы из внутреннее поддерева сущности
Предыдущая
Следующая