Система сущностей
Карта в JS API может быть представлена как совокупность различных объектов, которые объединены в общую древовидную структуру. Различные типы этих объектов взаимодействуют друг с другом, формируя систему сущностей.
В данном разделе будет рассмотрена эта система, её типы сущностей, их особенности и назначение, также научимся создавать собственные пользовательские объекты на основе этой системы.
Система сущностей является императивной частью JS API карт.
Базовые сущности
Базовые сущности представляют из себя абстрактные классы, компоненты дерева создаются благодаря наследованию от них.
Существуют следующие виды сущностей:
- Entity – базовая сущность;
- Complex Entity – сущность, которая может иметь собственное поддерево сущностей, но не имеет публичного интерфейса взаимодействия с ним;
- Group Entity – аналогична Complex Entity, но имеет публичный интерфейс для взаимодействия с поддеревом сущностей;
- Root Entity – корневая сущность, которая не может быть частью поддерева другой сущности. Аналогично Group Entity имеет публичный интерфейс управления собственным поддеревом.
Если рассматривать компоненты из модуля ymaps3, то вот список с соответствием к каждому типу сущности:
- Root Entity – YMap;
- Group Entity – YMapControls, YMapControl, YMapMarker;
- Complex Entity – YMapDefaultSchemeLayer, YMapDefaultFeaturesLayer;
- Entity – YMapTileDataSource, YMapFeatureDataSource, YMapLayer, YMapListener, YMapFeature.
Подробнее про каждый тип сущностей будет написано в соответствующих разделах.
Entity
Базовая сущность. Не может содержать собственное поддерево, но может быть добавлено к другой сущности.
В модуле ymaps3 существует абстрактный класс YMapEntity
для того чтобы создавать компонент сущности типа Entity с помощью наследования от данного класса:
type YMapSomeEntityProps = {
name: string;
};
class YMapSomeEntity extends ymaps3.YMapEntity<YMapSomeEntityProps> {
public isAttached: boolean;
constructor(props: YMapSomeEntityProps) {
super(props);
this.isAttached = false;
// Дополнительные действия, которые могут быть выполнены в конструкторе класса.
}
protected _onAttach(): void {
this.isAttached = true;
// Дополнительные действия, которые могут быть выполнены при присоединении объекта.
}
// ...
}
Класс YMapEntity
содержит различные protected методы для переопределения чтобы создавать обработчики события на различные события жизненного цикла сущности.
Методы _onAttach
и _onDetach
– обработчики вставки и удаления сущности из родительского поддерева соответственно:
class YMapSomeEntity extends ymaps3.YMapEntity<YMapSomeEntityProps> {
// При добавление сущности в родительское поддерево.
protected _onAttach(): void {
console.log('attach entity');
}
// При удалении сущности из родительского поддерева.
protected _onDetach(): void {
console.log('detach entity');
}
}
Каждая сущность может содержать собственные параметры в виде объекта со значениями. Конструктор класса принимает объект параметров первым аргументом. После создания компонента, свойства можно обновить через метод update
, указав только обновленные значения (другие значения не изменятся). Чтобы получить актуальные значения параметров сущности есть свойство _props
.
Метод _onUpdate
– обработчик обновления параметров сущности через метод update
. В качестве аргументов приходят значения параметров, которые обновили и старые значения параметров до обновления:
type YMapSomeEntityProps = {
visible: boolean;
};
class YMapSomeEntity extends ymaps3.YMapEntity<YMapSomeEntityProps> {
private _visible = false;
// Срабатывает при обновлении параметров сущности.
protected _onUpdate(propsDiff: Partial<YMapSomeEntityProps>, oldProps: YMapSomeEntityProps): void {
// Поскольку приходит разница параметров, то перед сохранением нового значения стоит проверить на undefined.
if (propsDiff.visible !== undefined) {
this._visible = propsDiff.visible;
}
// this._props будет содержать уже обновленные значения.
}
}
Экземпляр сущности YMapEntity
также имеет ссылки на другие связанные сущности дерева. Чтобы получить экземпляр родительской сущности существует readonly свойство parent
, которое возвращает сущность типа Complex Entity. Аналогичное свойство root
возвращает корневую сущность дерева Root Entity.
Complex Entity
Сущность которая может содержать в себе дочерние компоненты, но методы для добавления и удаления в поддерево являются внутренними.
В модуле ymaps3 существует абстрактный класс YMapComplexEntity
, который наследуется от YMapEntity
, поэтому имеет все описанные выше свойства и методы. Также имеет дополнительные методы для работы с собственным поддеревом. Создать компонент сущности типа Complex Entity можно с помощью наследования от класса YMapComplexEntity
:
type YMapSomeLayerProps = {
visible: boolean;
source: string;
};
class YMapSomeLayer extends ymaps3.YMapComplexEntity<YMapSomeLayerProps> {
private _dataSource?: YMapTileDataSource;
private _layer?: YMapLayer;
constructor(props: YMapSomeLayerProps) {
super(props);
const {source, visible} = this._props;
// Создаем экземпляры дочерних сущностей.
this._dataSource = new ymaps3.YMapTileDataSource({id: source});
this._layer = new ymaps3.YMapLayer({source, type: 'ground'});
// Добавляем дочерние сущности в собственное поддерево компонента.
this.addChild(this._dataSource);
if (visible) {
this.addChild(this._layer);
}
}
protected _onUpdate(propsDiff: Partial<YMapSomeLayerProps>): void {
if (propsDiff.visible !== undefined) {
// Удаляем или добавляем сущность в зависимости от значения visible.
propsDiff.visible ? this.addChild(this._layer) : this.removeChild(this._layer);
}
}
}
В примере выше использовались методы для взаимодействия с собственным поддеревом. Метод addChild
– добавляет стороннюю сущность в поддерево. Все дочерние сущности хранятся в некотором массиве, поэтому в качестве второго необязательного аргумента метод принимает порядковый номер на место которого добавится сущность в этот массив. Метод removeChild
– удаляет дочернюю сущность из поддерева.
Для получения списка дочерних элементов существует readonly массив children
.
Также конструктор класса YMapComplexEntity
имеет второй необязательный аргумент options
— объект со следующими свойствами:
children
– массив дочерних сущностей, которые будут добавлены в поддерево сразу после инициализации экземпляра класса.container
– еслиtrue
, то создает дочерний proxy-контейнер который будет содержать поддерево дочерних компонентов. По умолчанию proxy-контейнер не создается.
Подробнее про proxy-контейнеры рассказано в соответствующем разделе.
Group Entity
Сущность аналогична Complex Entity, но методы addChild
, removeChild
и свойство children
публичны.
В модуле ymaps3 существует абстрактный класс YMapGroupEntity
чтобы создавать собственные сущности типа Group Entity с помощью наследования:
type YMapSomeGroupEntityProps = {
name?: string;
};
class YMapSomeGroupEntity extends ymaps3.YMapGroupEntity<YMapSomeGroupEntityProps> {
// ...
}
const groupEntity = new YMapSomeGroupEntity();
const someEntity = new YMapSomeEntity(); // YMapSomeEntity наследуется от YMapEntity.
// Добавление внешней сущности из поддерева через публичный метод.
groupEntity.addChild(someEntity);
// Удаление внешней сущности из поддерева через публичный метод.
groupEntity.removeChild(someEntity);
Root Entity
Является корневой сущностью в дереве, поэтому не может быть добавлена в стороннее поддерево в качестве дочернего элемента.
Помимо остальных типов сущностей не нужно писать собственную реализацию корневой сущности. Для этого в модуле ymaps3 существует класс YMap
, который должен использоваться для создания дерева и является корневым для YMapEntity
, YMapComplexEntity
, YMapGroupEntity
.
Примеры использования YMap
можно найти на странице примеров.
Важно
Для определения собственных сущностей рекомендуется наследоваться только от 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 аргумента:
entity
— ссылка на сущность к которой создается привязка DOM-элемента.element
— DOM-элемент, к которой привязывается сущность.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 |
Удаляет дочерние элементы из внутреннее поддерева сущности | Удаляет дочерние элементы из внутреннее поддерева сущности |