Skip to content

Commit

Permalink
refactor(google-maps): reduce breaking changes from importLibrary int…
Browse files Browse the repository at this point in the history
…roduction

Reworks the Google Maps components to reduce the amount of breakages due to the initialization now being asynchronous.
  • Loading branch information
crisbeto committed Dec 19, 2023
1 parent add7799 commit 8ad07ca
Show file tree
Hide file tree
Showing 16 changed files with 409 additions and 212 deletions.
22 changes: 15 additions & 7 deletions src/google-maps/google-map/google-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,16 +309,24 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy {
// Create the object outside the zone so its events don't trigger change detection.
// We'll bring it back in inside the `MapEventManager` only for the events that the
// user has subscribed to.
this._ngZone.runOutsideAngular(async () => {
const mapConstructor =
google.maps.Map || (await importLibrary<google.maps.Map>('maps', 'Map'));
this.googleMap = new mapConstructor(this._mapEl, this._combineOptions());
this._eventManager.setTarget(this.googleMap);
this.mapInitialized.emit(this.googleMap);
});
if (google.maps.Map) {
this._initialize(google.maps.Map);
} else {
this._ngZone.runOutsideAngular(async () => {
this._initialize(await importLibrary<typeof google.maps.Map>('maps', 'Map'));
});
}
}
}

private _initialize(mapConstructor: typeof google.maps.Map) {
this._ngZone.runOutsideAngular(() => {
this.googleMap = new mapConstructor(this._mapEl, this._combineOptions());
this._eventManager.setTarget(this.googleMap);
this.mapInitialized.emit(this.googleMap);
});
}

ngOnDestroy() {
this.mapInitialized.complete();
this._eventManager.destroy();
Expand Down
21 changes: 15 additions & 6 deletions src/google-maps/map-base-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,28 @@ export class MapBaseLayer implements OnInit, OnDestroy {

ngOnInit() {
if (this._map._isBrowser) {
this._ngZone.runOutsideAngular(async () => {
const map = await this._map._resolveMap();
await this._initializeObject();
this._setMap(map);
this._ngZone.runOutsideAngular(() => {
this._initializeObject();
});
this._assertInitialized();
this._setMap();
}
}

ngOnDestroy() {
this._unsetMap();
}

protected async _initializeObject() {}
protected _setMap(_map: google.maps.Map) {}
private _assertInitialized() {
if (!this._map.googleMap) {
throw Error(
'Cannot access Google Map information before the API has been initialized. ' +
'Please wait for the API to load before trying to interact with it.',
);
}
}

protected _initializeObject() {}
protected _setMap() {}
protected _unsetMap() {}
}
42 changes: 29 additions & 13 deletions src/google-maps/map-bicycling-layer/map-bicycling-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
// Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265
/// <reference types="google.maps" />

import {Directive, EventEmitter, Output} from '@angular/core';
import {Directive, EventEmitter, NgZone, OnDestroy, OnInit, Output, inject} from '@angular/core';

import {MapBaseLayer} from '../map-base-layer';
import {importLibrary} from '../import-library';
import {GoogleMap} from '../google-map/google-map';

/**
* Angular component that renders a Google Maps Bicycling Layer via the Google Maps JavaScript API.
Expand All @@ -24,7 +24,10 @@ import {importLibrary} from '../import-library';
exportAs: 'mapBicyclingLayer',
standalone: true,
})
export class MapBicyclingLayer extends MapBaseLayer {
export class MapBicyclingLayer implements OnInit, OnDestroy {
private _map = inject(GoogleMap);
private _zone = inject(NgZone);

/**
* The underlying google.maps.BicyclingLayer object.
*
Expand All @@ -36,20 +39,33 @@ export class MapBicyclingLayer extends MapBaseLayer {
@Output() readonly bicyclingLayerInitialized: EventEmitter<google.maps.BicyclingLayer> =
new EventEmitter<google.maps.BicyclingLayer>();

protected override async _initializeObject() {
const layerConstructor =
google.maps.BicyclingLayer ||
(await importLibrary<google.maps.BicyclingLayer>('maps', 'BicyclingLayer'));
this.bicyclingLayer = new layerConstructor();
this.bicyclingLayerInitialized.emit(this.bicyclingLayer);
ngOnInit(): void {
if (this._map._isBrowser) {
if (google.maps.BicyclingLayer && this._map.googleMap) {
this._initialize(this._map.googleMap, google.maps.BicyclingLayer);
} else {
this._zone.runOutsideAngular(async () => {
this._initialize(
...(await Promise.all([
this._map._resolveMap(),
importLibrary<typeof google.maps.BicyclingLayer>('maps', 'BicyclingLayer'),
])),
);
});
}
}
}

protected override _setMap(map: google.maps.Map) {
this._assertLayerInitialized();
this.bicyclingLayer.setMap(map);
private _initialize(map: google.maps.Map, layerConstructor: typeof google.maps.BicyclingLayer) {
this._zone.runOutsideAngular(() => {
this.bicyclingLayer = new layerConstructor();
this.bicyclingLayerInitialized.emit(this.bicyclingLayer);
this._assertLayerInitialized();
this.bicyclingLayer.setMap(map);
});
}

protected override _unsetMap() {
ngOnDestroy() {
this.bicyclingLayer?.setMap(null);
}

Expand Down
47 changes: 31 additions & 16 deletions src/google-maps/map-circle/map-circle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,25 +175,40 @@ export class MapCircle implements OnInit, OnDestroy {
this._combineOptions()
.pipe(take(1))
.subscribe(options => {
// Create the object outside the zone so its events don't trigger change detection.
// We'll bring it back in inside the `MapEventManager` only for the events that the
// user has subscribed to.
this._ngZone.runOutsideAngular(async () => {
const map = await this._map._resolveMap();
const circleConstructor =
google.maps.Circle || (await importLibrary<google.maps.Circle>('maps', 'Circle'));
this.circle = new circleConstructor(options);
this._assertInitialized();
this.circle.setMap(map);
this._eventManager.setTarget(this.circle);
this.circleInitialized.emit(this.circle);
this._watchForOptionsChanges();
this._watchForCenterChanges();
this._watchForRadiusChanges();
});
if (google.maps.Circle && this._map.googleMap) {
this._initialize(this._map.googleMap, google.maps.Circle, options);
} else {
this._ngZone.runOutsideAngular(async () => {
const [map, circleConstructor] = await Promise.all([
this._map._resolveMap(),
importLibrary<typeof google.maps.Circle>('maps', 'Circle'),
]);
this._initialize(map, circleConstructor, options);
});
}
});
}

private _initialize(
map: google.maps.Map,
circleConstructor: typeof google.maps.Circle,
options: google.maps.CircleOptions,
) {
// Create the object outside the zone so its events don't trigger change detection.
// We'll bring it back in inside the `MapEventManager` only for the events that the
// user has subscribed to.
this._ngZone.runOutsideAngular(() => {
this.circle = new circleConstructor(options);
this._assertInitialized();
this.circle.setMap(map);
this._eventManager.setTarget(this.circle);
this.circleInitialized.emit(this.circle);
this._watchForOptionsChanges();
this._watchForCenterChanges();
this._watchForRadiusChanges();
});
}

ngOnDestroy() {
this._eventManager.destroy();
this._destroyed.next();
Expand Down
42 changes: 28 additions & 14 deletions src/google-maps/map-directions-renderer/map-directions-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,37 @@ export class MapDirectionsRenderer implements OnInit, OnChanges, OnDestroy {

ngOnInit() {
if (this._googleMap._isBrowser) {
// Create the object outside the zone so its events don't trigger change detection.
// We'll bring it back in inside the `MapEventManager` only for the events that the
// user has subscribed to.
this._ngZone.runOutsideAngular(async () => {
const map = await this._googleMap._resolveMap();
const rendererConstructor =
google.maps.DirectionsRenderer ||
(await importLibrary<google.maps.DirectionsRenderer>('routes', 'DirectionsRenderer'));
this.directionsRenderer = new rendererConstructor(this._combineOptions());
this._assertInitialized();
this.directionsRenderer.setMap(map);
this._eventManager.setTarget(this.directionsRenderer);
this.directionsRendererInitialized.emit(this.directionsRenderer);
});
if (google.maps.DirectionsRenderer && this._googleMap.googleMap) {
this._initialize(this._googleMap.googleMap, google.maps.DirectionsRenderer);
} else {
this._ngZone.runOutsideAngular(async () => {
this._initialize(
...(await Promise.all([
this._googleMap._resolveMap(),
importLibrary<typeof google.maps.DirectionsRenderer>('routes', 'DirectionsRenderer'),
])),
);
});
}
}
}

private _initialize(
map: google.maps.Map,
rendererConstructor: typeof google.maps.DirectionsRenderer,
) {
// Create the object outside the zone so its events don't trigger change detection.
// We'll bring it back in inside the `MapEventManager` only for the events that the
// user has subscribed to.
this._ngZone.runOutsideAngular(() => {
this.directionsRenderer = new rendererConstructor(this._combineOptions());
this._assertInitialized();
this.directionsRenderer.setMap(map);
this._eventManager.setTarget(this.directionsRenderer);
this.directionsRendererInitialized.emit(this.directionsRenderer);
});
}

ngOnChanges(changes: SimpleChanges) {
if (this.directionsRenderer) {
if (changes['options']) {
Expand Down
60 changes: 37 additions & 23 deletions src/google-maps/map-ground-overlay/map-ground-overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,34 +117,48 @@ export class MapGroundOverlay implements OnInit, OnDestroy {
return;
}

// Create the object outside the zone so its events don't trigger change detection.
// We'll bring it back in inside the `MapEventManager` only for the events that the
// user has subscribed to.
this._ngZone.runOutsideAngular(async () => {
const map = await this._map._resolveMap();
const overlayConstructor =
google.maps.GroundOverlay ||
(await importLibrary<google.maps.GroundOverlay>('maps', 'GroundOverlay'));
this.groundOverlay = new overlayConstructor(this._url.getValue(), bounds, {
clickable: this.clickable,
opacity: this._opacity.value,
if (google.maps.GroundOverlay && this._map.googleMap) {
this._initialize(this._map.googleMap, google.maps.GroundOverlay, bounds);
} else {
this._ngZone.runOutsideAngular(async () => {
const [map, overlayConstructor] = await Promise.all([
this._map._resolveMap(),
importLibrary<typeof google.maps.GroundOverlay>('maps', 'GroundOverlay'),
]);
this._initialize(map, overlayConstructor, bounds);
});
this._assertInitialized();
this.groundOverlay.setMap(map);
this._eventManager.setTarget(this.groundOverlay);
this.groundOverlayInitialized.emit(this.groundOverlay);

// We only need to set up the watchers once.
if (!this._hasWatchers) {
this._hasWatchers = true;
this._watchForOpacityChanges();
this._watchForUrlChanges();
}
});
}
});
}
}

private _initialize(
map: google.maps.Map,
overlayConstructor: typeof google.maps.GroundOverlay,
bounds: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral,
) {
// Create the object outside the zone so its events don't trigger change detection.
// We'll bring it back in inside the `MapEventManager` only for the events that the
// user has subscribed to.
this._ngZone.runOutsideAngular(() => {
this.groundOverlay = new overlayConstructor(this._url.getValue(), bounds, {
clickable: this.clickable,
opacity: this._opacity.value,
});
this._assertInitialized();
this.groundOverlay.setMap(map);
this._eventManager.setTarget(this.groundOverlay);
this.groundOverlayInitialized.emit(this.groundOverlay);

// We only need to set up the watchers once.
if (!this._hasWatchers) {
this._hasWatchers = true;
this._watchForOpacityChanges();
this._watchForUrlChanges();
}
});
}

ngOnDestroy() {
this._eventManager.destroy();
this._destroyed.next();
Expand Down
46 changes: 30 additions & 16 deletions src/google-maps/map-heatmap-layer/map-heatmap-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,25 +92,39 @@ export class MapHeatmapLayer implements OnInit, OnChanges, OnDestroy {
);
}

// Create the object outside the zone so its events don't trigger change detection.
// We'll bring it back in inside the `MapEventManager` only for the events that the
// user has subscribed to.
this._ngZone.runOutsideAngular(async () => {
const map = await this._googleMap._resolveMap();
const heatmapConstructor =
google.maps.visualization?.HeatmapLayer ||
(await importLibrary<google.maps.visualization.HeatmapLayer>(
'visualization',
'HeatmapLayer',
));
this.heatmap = new heatmapConstructor(this._combineOptions());
this._assertInitialized();
this.heatmap.setMap(map);
this.heatmapInitialized.emit(this.heatmap);
});
if (google.maps.visualization?.HeatmapLayer && this._googleMap.googleMap) {
this._initialize(this._googleMap.googleMap, google.maps.visualization.HeatmapLayer);
} else {
this._ngZone.runOutsideAngular(async () => {
this._initialize(
...(await Promise.all([
this._googleMap._resolveMap(),
importLibrary<typeof google.maps.visualization.HeatmapLayer>(
'visualization',
'HeatmapLayer',
),
])),
);
});
}
}
}

private _initialize(
map: google.maps.Map,
heatmapConstructor: typeof google.maps.visualization.HeatmapLayer,
) {
// Create the object outside the zone so its events don't trigger change detection.
// We'll bring it back in inside the `MapEventManager` only for the events that the
// user has subscribed to.
this._ngZone.runOutsideAngular(() => {
this.heatmap = new heatmapConstructor(this._combineOptions());
this._assertInitialized();
this.heatmap.setMap(map);
this.heatmapInitialized.emit(this.heatmap);
});
}

ngOnChanges(changes: SimpleChanges) {
const {_data, heatmap} = this;

Expand Down
Loading

0 comments on commit 8ad07ca

Please sign in to comment.