Skip to content

Commit cd7251b

Browse files
committed
fix(google-maps): internal events run inside NgZone
The custom Google Maps event work by binding everything ahead of time and then dispatching the results to the various listeners, however the result of this is that they'll trigger change detection for events that the consumer hasn't subscribed to. These changes switch to initializing the Google Maps objects outside the `NgZone` and then only bringing the ones that the consumer has subscribed to back inside.
1 parent a30094b commit cd7251b

File tree

6 files changed

+107
-19
lines changed

6 files changed

+107
-19
lines changed

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
Optional,
2323
Inject,
2424
PLATFORM_ID,
25+
NgZone,
2526
} from '@angular/core';
2627
import {isPlatformBrowser} from '@angular/common';
2728
import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs';
@@ -64,7 +65,7 @@ export const DEFAULT_WIDTH = '500px';
6465
encapsulation: ViewEncapsulation.None,
6566
})
6667
export class GoogleMap implements OnChanges, OnInit, OnDestroy {
67-
private _eventManager = new MapEventManager();
68+
private _eventManager = new MapEventManager(this._ngZone);
6869

6970
/** Whether we're currently rendering inside a browser. */
7071
private _isBrowser: boolean;
@@ -227,7 +228,12 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy {
227228
* @deprecated `platformId` parameter to become required.
228229
* @breaking-change 10.0.0
229230
*/
230-
@Optional() @Inject(PLATFORM_ID) platformId?: Object) {
231+
@Optional() @Inject(PLATFORM_ID) platformId?: Object,
232+
/**
233+
* @deprecated `_ngZone` parameter to become required.
234+
* @breaking-change 11.0.0
235+
*/
236+
private _ngZone?: NgZone) {
231237

232238
// @breaking-change 10.0.0 Remove null check for `platformId`.
233239
this._isBrowser =
@@ -453,7 +459,19 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy {
453459
Observable<google.maps.Map> {
454460
return optionsChanges.pipe(
455461
take(1),
456-
map(options => new google.maps.Map(this._mapEl, options)),
462+
map(options => {
463+
const initialize = () => new google.maps.Map(this._mapEl, options);
464+
465+
// @breaking-change 11.0.0 Remove null check for `ngZone`.
466+
if (this._ngZone) {
467+
// Create the object outside the zone so its events don't trigger change detection.
468+
// We'll bring it back in inside the `MapEventManager` only for the events that the
469+
// user has subscribed to.
470+
return this._ngZone.runOutsideAngular(initialize);
471+
} else {
472+
return initialize();
473+
}
474+
}),
457475
shareReplay(1));
458476
}
459477

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {NgZone} from '@angular/core';
910
import {Observable, Subscriber} from 'rxjs';
1011

1112
type MapEventManagerTarget = {
@@ -28,6 +29,11 @@ export class MapEventManager {
2829
this._listeners = [];
2930
}
3031

32+
// @breaking-change 11.0.0 `_ngZone` parameter to become required.
33+
constructor(private _ngZone?: NgZone) {
34+
console.log(!!_ngZone);
35+
}
36+
3137
/** Gets an observable that adds an event listener to the map when a consumer subscribes to it. */
3238
getLazyEmitter<T>(name: string): Observable<T> {
3339
const observable = new Observable<T>(observer => {
@@ -37,7 +43,14 @@ export class MapEventManager {
3743
return undefined;
3844
}
3945

40-
const listener = this._target.addListener(name, (event: T) => observer.next(event));
46+
const listener = this._target.addListener(name, (event: T) => {
47+
// @breaking-change 11.0.0 Remove null check for `_ngZone`.
48+
if (this._ngZone) {
49+
this._ngZone.run(() => observer.next(event));
50+
} else {
51+
observer.next(event);
52+
}
53+
});
4154
this._listeners.push(listener);
4255
return () => listener.remove();
4356
});

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
OnDestroy,
1717
OnInit,
1818
Output,
19+
NgZone,
1920
} from '@angular/core';
2021
import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs';
2122
import {map, takeUntil} from 'rxjs/operators';
@@ -33,7 +34,7 @@ import {MapEventManager} from '../map-event-manager';
3334
host: {'style': 'display: none'},
3435
})
3536
export class MapInfoWindow implements OnInit, OnDestroy {
36-
private _eventManager = new MapEventManager();
37+
private _eventManager = new MapEventManager(this._ngZone);
3738
private readonly _options = new BehaviorSubject<google.maps.InfoWindowOptions>({});
3839
private readonly _position =
3940
new BehaviorSubject<google.maps.LatLngLiteral|google.maps.LatLng|undefined>(undefined);
@@ -87,14 +88,30 @@ export class MapInfoWindow implements OnInit, OnDestroy {
8788
zindexChanged: Observable<void> = this._eventManager.getLazyEmitter<void>('zindex_changed');
8889

8990
constructor(private readonly _googleMap: GoogleMap,
90-
private _elementRef: ElementRef<HTMLElement>) {}
91+
private _elementRef: ElementRef<HTMLElement>,
92+
/**
93+
* @deprecated `_ngZone` parameter to become required.
94+
* @breaking-change 11.0.0
95+
*/
96+
private _ngZone?: NgZone) {}
9197

9298
ngOnInit() {
9399
this._combineOptions().pipe(takeUntil(this._destroy)).subscribe(options => {
94100
if (this._infoWindow) {
95101
this._infoWindow.setOptions(options);
96102
} else {
97-
this._infoWindow = new google.maps.InfoWindow(options);
103+
const initialize = () => this._infoWindow = new google.maps.InfoWindow(options);
104+
105+
// @breaking-change 11.0.0 Remove null check for `ngZone`.
106+
if (this._ngZone) {
107+
// Create the object outside the zone so its events don't trigger change detection.
108+
// We'll bring it back in inside the `MapEventManager` only for the events that the
109+
// user has subscribed to.
110+
this._ngZone.runOutsideAngular(initialize);
111+
} else {
112+
initialize();
113+
}
114+
98115
this._eventManager.setTarget(this._infoWindow);
99116
}
100117
});

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
OnDestroy,
1717
OnInit,
1818
Output,
19-
ViewEncapsulation
19+
ViewEncapsulation,
20+
NgZone
2021
} from '@angular/core';
2122
import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs';
2223
import {map, take, takeUntil} from 'rxjs/operators';
@@ -43,7 +44,7 @@ export const DEFAULT_MARKER_OPTIONS = {
4344
encapsulation: ViewEncapsulation.None,
4445
})
4546
export class MapMarker implements OnInit, OnDestroy {
46-
private _eventManager = new MapEventManager();
47+
private _eventManager = new MapEventManager(this._ngZone);
4748
private readonly _options =
4849
new BehaviorSubject<google.maps.MarkerOptions>(DEFAULT_MARKER_OPTIONS);
4950
private readonly _title = new BehaviorSubject<string|undefined>(undefined);
@@ -236,14 +237,31 @@ export class MapMarker implements OnInit, OnDestroy {
236237

237238
_marker?: google.maps.Marker;
238239

239-
constructor(private readonly _googleMap: GoogleMap) {}
240+
constructor(
241+
private readonly _googleMap: GoogleMap,
242+
/**
243+
* @deprecated `_ngZone` parameter to become required.
244+
* @breaking-change 11.0.0
245+
*/
246+
private _ngZone?: NgZone) {}
240247

241248
ngOnInit() {
242249
const combinedOptionsChanges = this._combineOptions();
243250

244251
combinedOptionsChanges.pipe(take(1)).subscribe(options => {
245-
this._marker = new google.maps.Marker(options);
246-
this._marker.setMap(this._googleMap._googleMap);
252+
const initialize = () => this._marker = new google.maps.Marker(options);
253+
254+
// @breaking-change 11.0.0 Remove null check for `ngZone`.
255+
if (this._ngZone) {
256+
// Create the object outside the zone so its events don't trigger change detection.
257+
// We'll bring it back in inside the `MapEventManager` only for the events that the
258+
// user has subscribed to.
259+
this._ngZone.runOutsideAngular(initialize);
260+
} else {
261+
initialize();
262+
}
263+
264+
this._marker!.setMap(this._googleMap._googleMap);
247265
this._eventManager.setTarget(this._marker);
248266
});
249267

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
OnDestroy,
1616
OnInit,
1717
Output,
18+
NgZone,
1819
} from '@angular/core';
1920
import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs';
2021
import {map, take, takeUntil} from 'rxjs/operators';
@@ -30,7 +31,7 @@ import {MapEventManager} from '../map-event-manager';
3031
selector: 'map-polyline',
3132
})
3233
export class MapPolyline implements OnInit, OnDestroy {
33-
private _eventManager = new MapEventManager();
34+
private _eventManager = new MapEventManager(this._ngZone);
3435
private readonly _options = new BehaviorSubject<google.maps.PolylineOptions>({});
3536
private readonly _path =
3637
new BehaviorSubject<google.maps.MVCArray<google.maps.LatLng>|google.maps.LatLng[]|
@@ -129,13 +130,30 @@ export class MapPolyline implements OnInit, OnDestroy {
129130
polylineRightclick: Observable<google.maps.PolyMouseEvent> =
130131
this._eventManager.getLazyEmitter<google.maps.PolyMouseEvent>('rightclick');
131132

132-
constructor(private readonly _map: GoogleMap) {}
133+
constructor(
134+
private readonly _map: GoogleMap,
135+
/**
136+
* @deprecated `_ngZone` parameter to become required.
137+
* @breaking-change 11.0.0
138+
*/
139+
private _ngZone?: NgZone) {}
133140

134141
ngOnInit() {
135142
const combinedOptionsChanges = this._combineOptions();
136143

137144
combinedOptionsChanges.pipe(take(1)).subscribe(options => {
138-
this._polyline = new google.maps.Polyline(options);
145+
const initialize = () => this._polyline = new google.maps.Polyline(options);
146+
147+
// @breaking-change 11.0.0 Remove null check for `ngZone`.
148+
if (this._ngZone) {
149+
// Create the object outside the zone so its events don't trigger change detection.
150+
// We'll bring it back in inside the `MapEventManager` only for the events that the
151+
// user has subscribed to.
152+
this._ngZone.runOutsideAngular(initialize);
153+
} else {
154+
initialize();
155+
}
156+
139157
this._polyline.setMap(this._map._googleMap);
140158
this._eventManager.setTarget(this._polyline);
141159
});

tools/public_api_guard/google-maps/google-maps.d.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export declare class GoogleMap implements OnChanges, OnInit, OnDestroy {
2828
zoom: number;
2929
zoomChanged: Observable<void>;
3030
constructor(_elementRef: ElementRef,
31-
platformId?: Object);
31+
platformId?: Object,
32+
_ngZone?: NgZone | undefined);
3233
fitBounds(bounds: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral, padding?: number | google.maps.Padding): void;
3334
getBounds(): google.maps.LatLngBounds | null;
3435
getCenter(): google.maps.LatLng;
@@ -62,7 +63,8 @@ export declare class MapInfoWindow implements OnInit, OnDestroy {
6263
position: google.maps.LatLngLiteral | google.maps.LatLng;
6364
positionChanged: Observable<void>;
6465
zindexChanged: Observable<void>;
65-
constructor(_googleMap: GoogleMap, _elementRef: ElementRef<HTMLElement>);
66+
constructor(_googleMap: GoogleMap, _elementRef: ElementRef<HTMLElement>,
67+
_ngZone?: NgZone | undefined);
6668
close(): void;
6769
getContent(): string | Node;
6870
getPosition(): google.maps.LatLng | null;
@@ -102,7 +104,8 @@ export declare class MapMarker implements OnInit, OnDestroy {
102104
titleChanged: Observable<void>;
103105
visibleChanged: Observable<void>;
104106
zindexChanged: Observable<void>;
105-
constructor(_googleMap: GoogleMap);
107+
constructor(_googleMap: GoogleMap,
108+
_ngZone?: NgZone | undefined);
106109
getAnimation(): google.maps.Animation | null;
107110
getClickable(): boolean;
108111
getCursor(): string | null;
@@ -136,7 +139,8 @@ export declare class MapPolyline implements OnInit, OnDestroy {
136139
polylineMouseover: Observable<google.maps.PolyMouseEvent>;
137140
polylineMouseup: Observable<google.maps.PolyMouseEvent>;
138141
polylineRightclick: Observable<google.maps.PolyMouseEvent>;
139-
constructor(_map: GoogleMap);
142+
constructor(_map: GoogleMap,
143+
_ngZone?: NgZone | undefined);
140144
getDraggable(): boolean;
141145
getEditable(): boolean;
142146
getPath(): google.maps.MVCArray<google.maps.LatLng>;

0 commit comments

Comments
 (0)