Skip to content

Commit 22fecb3

Browse files
crisbetojelbourn
authored andcommitted
fix(google-maps): unable to subscribe to events after initialization (#17845)
As things are set up right now in the `google-map`, `map-marker` and `map-info-window` components we choose whether to bind an event based on whether there are subscribers to its observable. The problem is that if the user decides to subscribe after init (e.g. by getting a hold of object with `ViewChild` and subscribing) it'll never emit because the native event wasn't added. These changes fix the issue by switching the event to be observables and binding the event when something has subscribed. I've also introduced a `MapEventManager` class to make it easier to keep track of the events and to avoid duplication between the three components.
1 parent af6c13f commit 22fecb3

File tree

10 files changed

+451
-428
lines changed

10 files changed

+451
-428
lines changed

src/google-maps/google-map/google-map.spec.ts

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Component} from '@angular/core';
1+
import {Component, ViewChild} from '@angular/core';
22
import {async, TestBed} from '@angular/core/testing';
33
import {By} from '@angular/platform-browser';
44

@@ -217,28 +217,46 @@ describe('GoogleMap', () => {
217217
mapSpy = createMapSpy(DEFAULT_OPTIONS);
218218
createMapConstructorSpy(mapSpy).and.callThrough();
219219

220+
const addSpy = mapSpy.addListener;
220221
const fixture = TestBed.createComponent(TestApp);
221222
fixture.detectChanges();
222223

223-
expect(mapSpy.addListener).toHaveBeenCalledWith('click', jasmine.any(Function));
224-
expect(mapSpy.addListener).toHaveBeenCalledWith('center_changed', jasmine.any(Function));
225-
expect(mapSpy.addListener).toHaveBeenCalledWith('rightclick', jasmine.any(Function));
226-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('bounds_changed', jasmine.any(Function));
227-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('dblclick', jasmine.any(Function));
228-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('drag', jasmine.any(Function));
229-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('dragend', jasmine.any(Function));
230-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('dragstart', jasmine.any(Function));
231-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('heading_changed', jasmine.any(Function));
232-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('idle', jasmine.any(Function));
233-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('maptypeid_changed', jasmine.any(Function));
234-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('mousemove', jasmine.any(Function));
235-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('mouseout', jasmine.any(Function));
236-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('mouseover', jasmine.any(Function));
237-
expect(mapSpy.addListener)
238-
.not.toHaveBeenCalledWith('projection_changed', jasmine.any(Function));
239-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('tilesloaded', jasmine.any(Function));
240-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('tilt_changed', jasmine.any(Function));
241-
expect(mapSpy.addListener).not.toHaveBeenCalledWith('zoom_changed', jasmine.any(Function));
224+
expect(addSpy).toHaveBeenCalledWith('click', jasmine.any(Function));
225+
expect(addSpy).toHaveBeenCalledWith('center_changed', jasmine.any(Function));
226+
expect(addSpy).toHaveBeenCalledWith('rightclick', jasmine.any(Function));
227+
expect(addSpy).not.toHaveBeenCalledWith('bounds_changed', jasmine.any(Function));
228+
expect(addSpy).not.toHaveBeenCalledWith('dblclick', jasmine.any(Function));
229+
expect(addSpy).not.toHaveBeenCalledWith('drag', jasmine.any(Function));
230+
expect(addSpy).not.toHaveBeenCalledWith('dragend', jasmine.any(Function));
231+
expect(addSpy).not.toHaveBeenCalledWith('dragstart', jasmine.any(Function));
232+
expect(addSpy).not.toHaveBeenCalledWith('heading_changed', jasmine.any(Function));
233+
expect(addSpy).not.toHaveBeenCalledWith('idle', jasmine.any(Function));
234+
expect(addSpy).not.toHaveBeenCalledWith('maptypeid_changed', jasmine.any(Function));
235+
expect(addSpy).not.toHaveBeenCalledWith('mousemove', jasmine.any(Function));
236+
expect(addSpy).not.toHaveBeenCalledWith('mouseout', jasmine.any(Function));
237+
expect(addSpy).not.toHaveBeenCalledWith('mouseover', jasmine.any(Function));
238+
expect(addSpy).not.toHaveBeenCalledWith('projection_changed', jasmine.any(Function));
239+
expect(addSpy).not.toHaveBeenCalledWith('tilesloaded', jasmine.any(Function));
240+
expect(addSpy).not.toHaveBeenCalledWith('tilt_changed', jasmine.any(Function));
241+
expect(addSpy).not.toHaveBeenCalledWith('zoom_changed', jasmine.any(Function));
242+
});
243+
244+
it('should be able to add an event listener after init', () => {
245+
mapSpy = createMapSpy(DEFAULT_OPTIONS);
246+
createMapConstructorSpy(mapSpy).and.callThrough();
247+
248+
const addSpy = mapSpy.addListener;
249+
const fixture = TestBed.createComponent(TestApp);
250+
fixture.detectChanges();
251+
252+
expect(addSpy).not.toHaveBeenCalledWith('projection_changed', jasmine.any(Function));
253+
254+
// Pick an event that isn't bound in the template.
255+
const subscription = fixture.componentInstance.map.projectionChanged.subscribe();
256+
fixture.detectChanges();
257+
258+
expect(addSpy).toHaveBeenCalledWith('projection_changed', jasmine.any(Function));
259+
subscription.unsubscribe();
242260
});
243261
});
244262

@@ -255,15 +273,14 @@ describe('GoogleMap', () => {
255273
</google-map>`,
256274
})
257275
class TestApp {
276+
@ViewChild(GoogleMap) map: GoogleMap;
258277
height?: string;
259278
width?: string;
260279
center?: google.maps.LatLngLiteral;
261280
zoom?: number;
262281
options?: google.maps.MapOptions;
263282

264283
handleClick(event: google.maps.MouseEvent) {}
265-
266284
handleCenterChanged() {}
267-
268285
handleRightclick(event: google.maps.MouseEvent) {}
269286
}

src/google-maps/google-map/google-map.ts

Lines changed: 55 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
ChangeDetectionStrategy,
1414
Component,
1515
ElementRef,
16-
EventEmitter,
1716
Input,
1817
OnChanges,
1918
OnDestroy,
@@ -27,6 +26,7 @@ import {
2726
import {isPlatformBrowser} from '@angular/common';
2827
import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs';
2928
import {map, shareReplay, take, takeUntil} from 'rxjs/operators';
29+
import {MapEventManager} from '../map-event-manager';
3030

3131
interface GoogleMapsWindow extends Window {
3232
google?: typeof google;
@@ -64,6 +64,20 @@ export const DEFAULT_WIDTH = '500px';
6464
encapsulation: ViewEncapsulation.None,
6565
})
6666
export class GoogleMap implements OnChanges, OnInit, OnDestroy {
67+
private _eventManager = new MapEventManager();
68+
69+
/** Whether we're currently rendering inside a browser. */
70+
private _isBrowser: boolean;
71+
private _googleMapChanges: Observable<google.maps.Map>;
72+
73+
private readonly _options = new BehaviorSubject<google.maps.MapOptions>(DEFAULT_OPTIONS);
74+
private readonly _center =
75+
new BehaviorSubject<google.maps.LatLngLiteral|google.maps.LatLng|undefined>(undefined);
76+
private readonly _zoom = new BehaviorSubject<number|undefined>(undefined);
77+
private readonly _destroy = new Subject<void>();
78+
private _mapEl: HTMLElement;
79+
_googleMap: UpdatedGoogleMap;
80+
6781
@Input() height = DEFAULT_HEIGHT;
6882

6983
@Input() width = DEFAULT_WIDTH;
@@ -85,125 +99,127 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy {
8599
* See
86100
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.bounds_changed
87101
*/
88-
@Output() boundsChanged = new EventEmitter<void>();
102+
@Output()
103+
boundsChanged: Observable<void> = this._eventManager.getLazyEmitter<void>('bounds_changed');
89104

90105
/**
91106
* See
92107
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.center_changed
93108
*/
94-
@Output() centerChanged = new EventEmitter<void>();
109+
@Output()
110+
centerChanged: Observable<void> = this._eventManager.getLazyEmitter<void>('center_changed');
95111

96112
/**
97113
* See
98114
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.click
99115
*/
100-
@Output() mapClick = new EventEmitter<google.maps.MouseEvent|google.maps.IconMouseEvent>();
116+
@Output()
117+
mapClick: Observable<google.maps.MouseEvent|google.maps.IconMouseEvent> =
118+
this._eventManager.getLazyEmitter<google.maps.MouseEvent|google.maps.IconMouseEvent>('click');
101119

102120
/**
103121
* See
104122
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.dblclick
105123
*/
106-
@Output() mapDblclick = new EventEmitter<google.maps.MouseEvent>();
124+
@Output()
125+
mapDblclick: Observable<google.maps.MouseEvent> =
126+
this._eventManager.getLazyEmitter<google.maps.MouseEvent>('dblclick');
107127

108128
/**
109129
* See
110130
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.drag
111131
*/
112-
@Output() mapDrag = new EventEmitter<void>();
132+
@Output() mapDrag: Observable<void> = this._eventManager.getLazyEmitter<void>('drag');
113133

114134
/**
115135
* See
116136
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.dragend
117137
*/
118-
@Output() mapDragend = new EventEmitter<void>();
138+
@Output() mapDragend: Observable<void> = this._eventManager.getLazyEmitter<void>('dragend');
119139

120140
/**
121141
* See
122142
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.dragstart
123143
*/
124-
@Output() mapDragstart = new EventEmitter<void>();
144+
@Output() mapDragstart: Observable<void> = this._eventManager.getLazyEmitter<void>('dragstart');
125145

126146
/**
127147
* See
128148
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.heading_changed
129149
*/
130-
@Output() headingChanged = new EventEmitter<void>();
150+
@Output()
151+
headingChanged: Observable<void> = this._eventManager.getLazyEmitter<void>('heading_changed');
131152

132153
/**
133154
* See
134155
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.idle
135156
*/
136-
@Output() idle = new EventEmitter<void>();
157+
@Output() idle: Observable<void> = this._eventManager.getLazyEmitter<void>('idle');
137158

138159
/**
139160
* See
140161
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.maptypeid_changed
141162
*/
142-
@Output() maptypeidChanged = new EventEmitter<void>();
163+
@Output()
164+
maptypeidChanged: Observable<void> = this._eventManager.getLazyEmitter<void>('maptypeid_changed');
143165

144166
/**
145167
* See
146168
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.mousemove
147169
*/
148-
@Output() mapMousemove = new EventEmitter<google.maps.MouseEvent>();
170+
@Output()
171+
mapMousemove: Observable<google.maps.MouseEvent> =
172+
this._eventManager.getLazyEmitter<google.maps.MouseEvent>('mousemove');
149173

150174
/**
151175
* See
152176
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.mouseout
153177
*/
154-
@Output() mapMouseout = new EventEmitter<google.maps.MouseEvent>();
178+
@Output()
179+
mapMouseout: Observable<google.maps.MouseEvent> =
180+
this._eventManager.getLazyEmitter<google.maps.MouseEvent>('mouseout');
155181

156182
/**
157183
* See
158184
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.mouseover
159185
*/
160-
@Output() mapMouseover = new EventEmitter<google.maps.MouseEvent>();
186+
@Output()
187+
mapMouseover: Observable<google.maps.MouseEvent> =
188+
this._eventManager.getLazyEmitter<google.maps.MouseEvent>('mouseover');
161189

162190
/**
163191
* See
164192
* developers.google.com/maps/documentation/javascript/reference/map#Map.projection_changed
165193
*/
166-
@Output() projectionChanged = new EventEmitter<void>();
194+
@Output()
195+
projectionChanged: Observable<void> =
196+
this._eventManager.getLazyEmitter<void>('projection_changed');
167197

168198
/**
169199
* See
170200
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.rightclick
171201
*/
172-
@Output() mapRightclick = new EventEmitter<google.maps.MouseEvent>();
202+
@Output()
203+
mapRightclick: Observable<google.maps.MouseEvent> =
204+
this._eventManager.getLazyEmitter<google.maps.MouseEvent>('rightclick');
173205

174206
/**
175207
* See
176208
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.tilesloaded
177209
*/
178-
@Output() tilesloaded = new EventEmitter<void>();
210+
@Output() tilesloaded: Observable<void> = this._eventManager.getLazyEmitter<void>('tilesloaded');
179211

180212
/**
181213
* See
182214
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.tilt_changed
183215
*/
184-
@Output() tiltChanged = new EventEmitter<void>();
216+
@Output() tiltChanged: Observable<void> = this._eventManager.getLazyEmitter<void>('tilt_changed');
185217

186218
/**
187219
* See
188220
* https://developers.google.com/maps/documentation/javascript/reference/map#Map.zoom_changed
189221
*/
190-
@Output() zoomChanged = new EventEmitter<void>();
191-
192-
private _mapEl: HTMLElement;
193-
_googleMap: UpdatedGoogleMap;
194-
195-
/** Whether we're currently rendering inside a browser. */
196-
private _isBrowser: boolean;
197-
private _googleMapChanges!: Observable<google.maps.Map>;
198-
199-
private _listeners: google.maps.MapsEventListener[] = [];
200-
201-
private readonly _options = new BehaviorSubject<google.maps.MapOptions>(DEFAULT_OPTIONS);
202-
private readonly _center =
203-
new BehaviorSubject<google.maps.LatLngLiteral|google.maps.LatLng|undefined>(undefined);
204-
private readonly _zoom = new BehaviorSubject<number|undefined>(undefined);
205-
206-
private readonly _destroy = new Subject<void>();
222+
@Output() zoomChanged: Observable<void> = this._eventManager.getLazyEmitter<void>('zoomChanged');
207223

208224
constructor(
209225
private readonly _elementRef: ElementRef,
@@ -238,13 +254,10 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy {
238254
if (this._isBrowser) {
239255
this._mapEl = this._elementRef.nativeElement.querySelector('.map-container')!;
240256
this._setSize();
241-
242-
const combinedOptionsChanges = this._combineOptions();
243-
244-
this._googleMapChanges = this._initializeMap(combinedOptionsChanges);
257+
this._googleMapChanges = this._initializeMap(this._combineOptions());
245258
this._googleMapChanges.subscribe((googleMap: google.maps.Map) => {
246259
this._googleMap = googleMap as UpdatedGoogleMap;
247-
this._initializeEventHandlers(this._googleMap);
260+
this._eventManager.setTarget(this._googleMap);
248261
});
249262

250263
this._watchForOptionsChanges();
@@ -254,9 +267,9 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy {
254267
}
255268

256269
ngOnDestroy() {
270+
this._eventManager.destroy();
257271
this._destroy.next();
258272
this._destroy.complete();
259-
this._clearListeners();
260273
}
261274

262275
/**
@@ -472,64 +485,7 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy {
472485
});
473486
}
474487

475-
private _initializeEventHandlers(googleMap: UpdatedGoogleMap) {
476-
// Ensure that we don't leak if called multiple times.
477-
this._clearListeners();
478-
479-
const eventHandlers = new Map<string, EventEmitter<void>>([
480-
['bounds_changed', this.boundsChanged],
481-
['center_changed', this.centerChanged],
482-
['drag', this.mapDrag],
483-
['dragend', this.mapDragend],
484-
['dragstart', this.mapDragstart],
485-
['heading_changed', this.headingChanged],
486-
['idle', this.idle],
487-
['maptypeid_changed', this.maptypeidChanged],
488-
['projection_changed', this.projectionChanged],
489-
['tilesloaded', this.tilesloaded],
490-
['tilt_changed', this.tiltChanged],
491-
['zoomChanged', this.zoomChanged],
492-
]);
493-
const mouseEventHandlers = new Map<string, EventEmitter<google.maps.MouseEvent>>([
494-
['dblclick', this.mapDblclick],
495-
['mousemove', this.mapMousemove],
496-
['mouseout', this.mapMouseout],
497-
['mouseover', this.mapMouseover],
498-
['rightclick', this.mapRightclick],
499-
]);
500-
eventHandlers.forEach((eventHandler: EventEmitter<void>, name: string) => {
501-
if (eventHandler.observers.length > 0) {
502-
this._listeners.push(googleMap.addListener(name, () => {
503-
eventHandler.emit();
504-
}));
505-
}
506-
});
507-
mouseEventHandlers.forEach(
508-
(eventHandler: EventEmitter<google.maps.MouseEvent>, name: string) => {
509-
if (eventHandler.observers.length > 0) {
510-
this._listeners.push(
511-
googleMap.addListener(name, (event: google.maps.MouseEvent) => {
512-
eventHandler.emit(event);
513-
}));
514-
}
515-
});
516-
if (this.mapClick.observers.length > 0) {
517-
this._listeners.push(googleMap.addListener(
518-
'click', (event: google.maps.MouseEvent|google.maps.IconMouseEvent) => {
519-
this.mapClick.emit(event);
520-
}));
521-
}
522-
}
523-
524-
/** Clears all currently-registered event listeners. */
525-
private _clearListeners() {
526-
for (let listener of this._listeners) {
527-
listener.remove();
528-
}
529-
530-
this._listeners = [];
531-
}
532-
488+
/** Asserts that the map has been initialized. */
533489
private _assertInitialized() {
534490
if (!this._googleMap) {
535491
throw Error('Cannot access Google Map information before the API has been initialized. ' +

0 commit comments

Comments
 (0)