Skip to content

Commit

Permalink
feat(ObjectManager): Create ObjectManager directive (#162)
Browse files Browse the repository at this point in the history
* feat(typings): Add Prefix utility and improve some interfaces

Add Prefix utility and replace hardcoded interfaces with it. Improve IObjectManagerOptions, IGeoObjectOptions and IClusterPlacemarkOptions.

* feat(ObjectManager): Create ObjectManager directive

* chore(ObjectManager): Add usage of ObjectManager component in app.component
  • Loading branch information
ddubrava committed Feb 21, 2022
1 parent 2698aa7 commit 72d905a
Show file tree
Hide file tree
Showing 8 changed files with 603 additions and 177 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { YaControlDirective } from './components/ya-control/ya-control.directive
import { YaGeoObjectDirective } from './components/ya-geoobject/ya-geoobject.directive';
import { YaMapComponent } from './components/ya-map/ya-map.component';
import { YaMultirouteDirective } from './components/ya-multiroute/ya-multiroute.directive';
import { YaObjectManagerDirective } from './components/ya-object-manager/ya-object-manager.directive';
import { YaPanoramaDirective } from './components/ya-panorama/ya-panorama.directive';
import { YaPlacemarkDirective } from './components/ya-placemark/ya-placemark.directive';

Expand All @@ -17,6 +18,7 @@ import { YaPlacemarkDirective } from './components/ya-placemark/ya-placemark.dir
YaGeoObjectDirective,
YaMapComponent,
YaMultirouteDirective,
YaObjectManagerDirective,
YaPanoramaDirective,
YaPlacemarkDirective,
],
Expand All @@ -26,6 +28,7 @@ import { YaPlacemarkDirective } from './components/ya-placemark/ya-placemark.dir
YaGeoObjectDirective,
YaMapComponent,
YaMultirouteDirective,
YaObjectManagerDirective,
YaPanoramaDirective,
YaPlacemarkDirective,
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { Component, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BehaviorSubject } from 'rxjs';

import { YaMapComponent } from '../ya-map/ya-map.component';
import { YaObjectManagerDirective } from './ya-object-manager.directive';
import {
createMapSpy,
createObjectManagerConstructorSpy,
createObjectManagerSpy,
} from '../../testing/fake-ymaps-utils';
import { YaReadyEvent } from '../../typings/ya-ready-event';

@Component({
template: `
<ya-object-manager
[options]="options"
(yaclick)="handleClick()"
(geometrychange)="handleGeometryChange()"
(multitouchmove)="handleMultitouchMove()"
></ya-object-manager>
`,
})
class MockHostComponent {
@ViewChild(YaObjectManagerDirective, { static: true }) objectManager: YaObjectManagerDirective;

options: ymaps.IObjectManagerOptions;

handleClick(): void {}

handleGeometryChange(): void {}

handleMultitouchMove(): void {}
}

describe('YaObjectManagerDirective', () => {
let component: YaObjectManagerDirective;
let fixture: ComponentFixture<MockHostComponent>;

let mapSpy: jasmine.SpyObj<ymaps.Map>;
let objectManagerSpy: jasmine.SpyObj<ymaps.ObjectManager>;
let objectManagerConstructorSpy: jasmine.Spy;

beforeEach(async () => {
mapSpy = createMapSpy();

await TestBed.configureTestingModule({
declarations: [MockHostComponent, YaObjectManagerDirective],
providers: [
{
provide: YaMapComponent,
useValue: {
isBrowser: true,
map$: new BehaviorSubject(mapSpy),
},
},
],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(MockHostComponent);
component = fixture.componentInstance.objectManager;
objectManagerSpy = createObjectManagerSpy();
objectManagerConstructorSpy = createObjectManagerConstructorSpy(objectManagerSpy);
});

afterEach(() => {
(window.ymaps as any) = undefined;
});

it('should create objectManager', () => {
const options = {
clusterize: true,
clusterHasBalloon: false,
geoObjectOpenBalloonOnClick: false,
};

fixture.componentInstance.options = options;
fixture.detectChanges();

expect(objectManagerConstructorSpy).toHaveBeenCalledWith(options);
expect(mapSpy.geoObjects.add).toHaveBeenCalledWith(objectManagerSpy);
});

it('should emit ready on objectManager load', () => {
spyOn(component.ready, 'emit');
fixture.detectChanges();

const readyEvent: YaReadyEvent = {
ymaps: window.ymaps,
target: objectManagerSpy,
};

expect(component.ready.emit).toHaveBeenCalledWith(readyEvent);
});

it('should set options', () => {
const options = {
clusterize: true,
minClusterSize: 10,
clusterDisableClickZoom: true,
};

fixture.componentInstance.options = options;
fixture.detectChanges();

expect(objectManagerConstructorSpy.calls.mostRecent()?.args[0]).toEqual(options);
});

it('should set options after init', () => {
fixture.detectChanges();

const options = {
clusterize: true,
minClusterSize: 10,
clusterDisableClickZoom: true,
};

fixture.componentInstance.options = options;

fixture.detectChanges();

expect(objectManagerSpy.options.set).toHaveBeenCalledWith(options);
});

it('should remove objectManager from map.geoObjects on destroy', () => {
fixture.detectChanges();
fixture.destroy();

expect(mapSpy.geoObjects.remove).toHaveBeenCalledWith(objectManagerSpy);
});

it('should init event handlers that are set on the objectManager', () => {
const addSpy = objectManagerSpy.events.add;
fixture.detectChanges();

expect(addSpy).toHaveBeenCalledWith('click', jasmine.any(Function));
expect(addSpy).toHaveBeenCalledWith('geometrychange', jasmine.any(Function));
expect(addSpy).toHaveBeenCalledWith('multitouchmove', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('contextmenu', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('dblclick', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('mapchange', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('mousedown', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('mouseenter', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('mouseleave', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('mousemove', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('mouseup', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('multitouchend', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('multitouchstart', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('optionschange', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('overlaychange', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('parentchange', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('propertieschange', jasmine.any(Function));
expect(addSpy).not.toHaveBeenCalledWith('wheel', jasmine.any(Function));
});

it('should be able to add an event listener after init', () => {
const addSpy = objectManagerSpy.events.add;
fixture.detectChanges();

expect(addSpy).not.toHaveBeenCalledWith('overlaychange', jasmine.any(Function));

// Pick an event that isn't bound in the template.
const subscription = fixture.componentInstance.objectManager.overlaychange.subscribe();
fixture.detectChanges();

expect(addSpy).toHaveBeenCalledWith('overlaychange', jasmine.any(Function));
subscription.unsubscribe();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import {
Directive,
EventEmitter,
Input,
NgZone,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
} from '@angular/core';
import { Observable, Subscription } from 'rxjs';

import { EventManager } from '../../event-manager';
import { YaEvent } from '../../typings/ya-event';
import { YaMapComponent } from '../ya-map/ya-map.component';
import { YaReadyEvent } from '../../typings/ya-ready-event';

@Directive({
selector: 'ya-object-manager',
})
export class YaObjectManagerDirective implements OnInit, OnChanges, OnDestroy {
private readonly _sub = new Subscription();

private readonly _eventManager = new EventManager(this._ngZone);

private _objectManager?: ymaps.ObjectManager;

@Input() options: ymaps.IObjectManagerOptions;

/**
* ObjectManager instance is added in a Map.
*/
@Output() ready: EventEmitter<YaReadyEvent<ymaps.ObjectManager>> = new EventEmitter<
YaReadyEvent<ymaps.ObjectManager>
>();

/**
* Single left-click on the object.
*/
@Output() yaclick: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('click');

/**
* Calls the element's context menu.
*/
@Output() yacontextmenu: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('contextmenu');

/**
* Double left-click on the object.
*/
@Output() yadblclick: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('dblclick');

/**
* Change to the geo object geometry.
*/
@Output() geometrychange: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('geometrychange');

/**
* Map reference changed.
*/
@Output() mapchange: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('mapchange');

/**
* Pressing the mouse button over the object.
*/
@Output() yamousedown: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('mousedown');

/**
* Pointing the cursor at the object.
*/
@Output() yamouseenter: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('mouseenter');

/**
* Moving the cursor off of the object.
*/
@Output() yamouseleave: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('mouseleave');

/**
* Moving the cursor over the object.
*/
@Output() yamousemove: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('mousemove');

/**
* Letting go of the mouse button over an object.
*/
@Output() yamouseup: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('mouseup');

/**
* End of multitouch.
*/
@Output() multitouchend: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('multitouchend');

/**
* Repeating event during multitouch.
*/
@Output() multitouchmove: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('multitouchmove');

/**
* Start of multitouch.
*/
@Output() multitouchstart: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('multitouchstart');

/**
* Change to the object options.
*/
@Output() optionschange: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('optionschange');

/**
* Change to the geo object overlay.
*/
@Output() overlaychange: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('overlaychange');

/**
* The parent object reference changed.
*/
@Output() parentchange: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('parentchange');

/**
* Change to the geo object data.
*/
@Output() propertieschange: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('propertieschange');

/**
* Mouse wheel scrolling.
*/
@Output() yawheel: Observable<YaEvent<ymaps.ObjectManager>> =
this._eventManager.getLazyEmitter('wheel');

constructor(private readonly _ngZone: NgZone, private readonly _yaMapComponent: YaMapComponent) {}

/**
* Handles input changes and passes them in API.
* @param changes
*/
ngOnChanges(changes: SimpleChanges): void {
const objectManager = this._objectManager;

if (objectManager) {
const { options } = changes;

if (options) {
objectManager.options.set(options.currentValue);
}
}
}

ngOnInit(): void {
// It should be a noop during server-side rendering.
if (this._yaMapComponent.isBrowser) {
const sub = this._yaMapComponent.map$.subscribe((map) => {
if (map) {
const objectManager = this._createObjectManager();
this._objectManager = objectManager;

map.geoObjects.add(objectManager);
this._eventManager.setTarget(objectManager);
this._ngZone.run(() => this.ready.emit({ ymaps, target: objectManager }));
}
});

this._sub.add(sub);
}
}

ngOnDestroy(): void {
if (this._objectManager) {
this._yaMapComponent?.map$.value?.geoObjects.remove(this._objectManager);
this._eventManager.destroy();
}

this._sub.unsubscribe();
}

/**
* Creates ObjectManager.
*/
private _createObjectManager(): ymaps.ObjectManager {
return new ymaps.ObjectManager(this.options);
}
}

0 comments on commit 72d905a

Please sign in to comment.