Entity system
A map in the JS API can be represented as a collection of different objects combined into a common tree-like structure. Different types of these objects interact with each other to form an entity system.
This section describes this system, its entity types, their features and purpose.
The entity system is an imperative part of the Yandex Maps JS API.
Base entities
Base entities are abstract classes, and tree components are created by inheriting from them.
There are the following entity types:
- YMapEntity: Base entity.
- YMapComplexEntity: Entity that can have its own subtree, but does not have a public interface to interact with it.
- YMapGroupEntity: Similar to YMapComplex Entity, but has a public interface to interact with the entity subtree.
- RootEntity : Root entity that cannot be a part of the subtree of another entity. Similarly to Group Entity, it has a public interface to manage its own subtree.
- YMapCollection: Collection of map entity objects.
- YMapContext: Creates a context that components can provide or read.
- YMapContainer: Passes the necessary parameters to
ReactandVuelibrary components.
Examples of correspondences to the ymaps3 module entity types:
- RootEntity – YMap;
- YMapEntity – YMapTileDataSource, YMapFeatureDataSource, YMapLayer, YMapListener, YMapFeature;
- YMapComplexEntity – YMapDefaultSchemeLayer, YMapDefaultFeaturesLayer;
- YMapGroupEntity – YMapControls, YMapControl, YMapMarker;
- YMapCollection – YMapControls, YMapControl, YMapMarker.
For more information about each entity class, see the sections:
Root Entity
This is the root entity of the tree, meaning it cannot be added to an external subtree as a child element.
Apart from other entity types, you do not need to write a custom implementation of the root entity. For that, the ymaps3 module has the YMap class that must be used to create a tree and is the root element for YMapEntity, YMapComplexEntity, and YMapGroupEntity.
Warning
To determine custom entities, we recommend that you only use Inheritance from YMapEntity, YMapComplexEntity, and YMapGroupEntity. Backward compatibility is not guaranteed when inheriting from other classes.
Default entity parameters
An entity can have optional parameters, but you can set default values for them. There are two ways to do this:
- If the default value is static, use the
staticfield of thedefaultPropsclass. - If the default value is calculated dynamically, use the
protected_getDefaultPropsmethod.
For example, an entity has an optional name parameter, but we want to set the default value to 'some-entity' if it is not explicitly specified.
Example for a static default value of the name parameter:
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;
}
A similar example, but for a dynamically calculated default value:
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};
}
}
Note
To make optional parameters with a default value not optional in TypeScript, you must specify the type of the default value in the generic class.
For that, we created the DefaultProps type and passed it to the second generic.
Linking an entity to the DOM
Entities are not linked to the DOM tree, because they are all located in a separate virtual tree. To link an entity to a DOM element while preserving the order and nesting of entities, use the useDomContext hook:
class YMapDOMEntity extends ymaps3.YMapGroupEntity<{text: string}> {
private _element?: HTMLHeadingElement;
private _container?: HTMLDivElement;
private _detachDom?: () => void;
protected _onAttach(): void {
// Create a DOM element that will be linked to the entity.
this._element = document.createElement('div');
this._element.textContent = this._props.text;
this._element.classList.add('element');
// Create a container for the DOM elements of child entities.
this._container = document.createElement('div');
this._container.classList.add('container');
// Insert the container inside the element.
this._element.appendChild(this._container);
// Create entity linking to the DOM.
this._detachDom = ymaps3.useDomContext(this, this._element, this._container);
}
protected _onDetach(): void {
// Detach the DOM from the entity and remove references to the elements.
this._detachDom?.();
this._detachDom = undefined;
this._element = undefined;
this._container = undefined;
}
}
// Initializing the root element.
map = new ymaps3.YMap(document.getElementById('app'), {location: LOCATION});
// Initialize the YMapDOMEntity class entities.
const DOMEntity = new YMapDOMEntity({text: 'DOM Entity'});
const childDOMEntity = new YMapDOMEntity({text: 'Child DOM Entity'});
// DOMEntity._element will be inserted into the map's DOM element.
map.addChild(DOMEntity);
// childDOMEntity._element will be added to DOMEntity._container.
DOMEntity.addChild(childDOMEntity);
.element {
position: absolute;
top: 0;
width: 100%;
}
.container {
position: absolute;
top: 24px;
width: 100%;
}
This hook is available only when inheriting from the YMapComplexEntity and YMapGroupEntity classes, since it is implied that an entity can have child elements. The hook takes three arguments:
entity: Reference to the entity to which the DOM element is being linked to.element: DOM element the entity is being linked to.container: DOM element into which the DOM elements of child entities will be inserted (if an entity does not imply the addition of child elements,nullis passed).
The useDomContext hook returns a function that removes the link to the DOM element.
Proxy container
Proxy containers can be determined in the classes derived from YMapComplexEntity and YMapGroupEntity via a constructor by passing options.container as a second argument.
You should first study some of the internal properties and mechanisms of these classes without using a proxy container.
The class includes a _childContainer property. By default, this is a reference to the current instance (this). It was mentioned earlier that the children property returns an array of child elements. But In fact, child elements of an entity are stored in a closed class property, and children is just an accessor property to it.
In addition to the already known addChild method, there is a protected _addDirectChild method (similar to removeChild and _removeDirectChild), which adds a child entity directly to the internal subtree. This is how it differs from addChild, which adds an entity inside _childContainer.
So all child entities are available via the children property, and you can remove them from the subtree. This can cause problems if there is a child element that an external user shouldn't be able to access:
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 does not have a proxy container.
this._feature = new ymaps3.YMapFeature({
geometry: {
type: 'Point',
coordinates: this._props.coordinates
}
});
this.addChild(this._feature); // Add _feature to the internal subtree.
// this._addDirectChild(this._feature); — similar action.
}
private _removeFeature() {
this.removeChild(this._feature); // Remove _feature from the internal subtree.
// this._removeDirectChild(this._feature); — similar action.
}
}
const withoutContainerEntity = new YMapWithoutContainerEntity({coordinates: [0, 0]});
const [feature] = withoutContainerEntity.children; // Get access to withoutContainerEntity._feature.
withoutContainerEntity.removeChild(feature); // You can remove the closed entity from the subtree.
Now let's see how the mechanism for adding child entities changes when we use a proxy container.
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 has a proxy container.
this._feature = new ymaps3.YMapFeature({
geometry: {
type: 'Point',
coordinates: this._props.coordinates
}
});
// this.addChild(this._feature); — adds _feature to the open subtree inside this._childContainer.
this._addDirectChild(this._feature); // Add _feature to the internal closed subtree.
}
private _removeFeature() {
// this.removeChild(this._feature); — removes _feature from the open subtree inside this._childContainer.
this._removeDirectChild(this._feature); // Remove _feature from the internal closed subtree.
}
}
const withContainerEntity = new YMapWithContainerEntity({coordinates: [0, 0]});
const publicFeature = ymaps3.YMapFeature({
geometry: {type: 'Point', coordinates: [1, 1]}
});
withContainerEntity.addChild(publicFeature);
// You can get only publicFeature, because withContainerEntity._feature is in the closed subtree.
const [feature] = withContainerEntity.children;
withoutContainerEntity.removeChild(feature); // You can remove publicFeature from the subtree.
First, _childContainer now contains a proxy container — a child entity similar to a class instance. Second, children now returns an internal subtree inside _childContainer, not inside a class instance itself. This enables the entity to have two subtrees: open and closed.
An internal subtree of the class instance is closed, because it is not accessible and stores _childContainer and entities added via _addDirectChild (which you can remove only with _removeDirectChild).
The internal subtree of _childContainer is open, because it can be accessed via the public children property and stores entities added via addChild (which you can remove only with removeChild).
Let's compare the differences between entities with and without a proxy container:
| Without proxy container | With proxy container | |
|---|---|---|
_childContainer |
Refers to the class instance (this) |
Refers to the proxy container |
children |
Returns the internal subtree of the entity | Returns the internal subtree of the proxy container |
addChild |
Adds child elements to the internal subtree of the entity | Adds child elements to the internal subtree of the proxy container |
_addDirectChild |
Adds child elements to the internal subtree of the entity | Adds child elements to the internal subtree of the entity |
removeChild |
Removes child elements from the internal subtree of the entity | Removes child elements from the internal subtree of the proxy container |
_removeDirectChild |
Removes child elements from the internal subtree of the entity | Removes child elements from the internal subtree of the entity |