From da3bcc69dad6304674b366e24621bc020f43be0f Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 19 Dec 2023 10:16:49 +0200 Subject: [PATCH] feat(google-maps): add support for dynamic library loading API Currently the `@angular/google-maps` module is implemented on top of the legacy ` + diff --git a/src/google-maps/README.md b/src/google-maps/README.md index 82d5c257b82c..e43b0cdb4df3 100644 --- a/src/google-maps/README.md +++ b/src/google-maps/README.md @@ -14,63 +14,25 @@ Follow [these steps](https://developers.google.com/maps/gmp-get-started) to get ## Loading the API -The API can be loaded when the component is actually used by using the Angular HttpClient jsonp -method to make sure that the component doesn't load until after the API has loaded. - -```typescript -// google-maps-demo.module.ts - -import { NgModule } from '@angular/core'; -import { provideHttpClient, withJsonpSupport } from '@angular/common/http'; - -import { GoogleMapsDemoComponent } from './google-maps-demo.component'; - -@NgModule({ - imports: [GoogleMapsDemoComponent], - providers: [provideHttpClient(withJsonpSupport())] -}) -export class GoogleMapsDemoModule {} - - -// google-maps-demo.component.ts - -import { Component } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { GoogleMap } from '@angular/google-maps'; -import { Observable, of } from 'rxjs'; -import { catchError, map } from 'rxjs/operators'; - -@Component({ - selector: 'google-maps-demo', - templateUrl: './google-maps-demo.component.html', - standalone: true, - imports: [GoogleMap] -}) -export class GoogleMapsDemoComponent { - apiLoaded: Observable; - - constructor(httpClient: HttpClient) { - // If you're using the `` directive, you also have to include the `visualization` library - // when loading the Google Maps API. To do so, you can add `&libraries=visualization` to the script URL: - // https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=visualization - - this.apiLoaded = httpClient.jsonp('https://maps.googleapis.com/maps/api/js?key=YOUR_KEY_HERE', 'callback') - .pipe( - map(() => true), - catchError(() => of(false)), - ); - } -} -``` +Include the [Dynamic Library Import script](https://developers.google.com/maps/documentation/javascript/load-maps-js-api#dynamic-library-import) in the `index.html` of your app. When a Google Map is being rendered, it'll use the Dynamic Import API to load the necessary JavaScript automatically. ```html - - -@if (apiLoaded | async) { - -} + + + + ... + + + ``` +**Note:** the component also supports loading the API using the [legacy script tag](https://developers.google.com/maps/documentation/javascript/load-maps-js-api#use-legacy-tag), however it isn't recommended because it requires all of the Google Maps JavaScript to be loaded up-front, even if it isn't used. + ## Components - [`GoogleMap`](./google-map/README.md) diff --git a/src/google-maps/google-map/google-map.spec.ts b/src/google-maps/google-map/google-map.spec.ts index 7e6056e24c37..51203384f363 100644 --- a/src/google-maps/google-map/google-map.spec.ts +++ b/src/google-maps/google-map/google-map.spec.ts @@ -116,7 +116,7 @@ describe('GoogleMap', () => { it('sets center and zoom of the map', () => { const options = {center: {lat: 3, lng: 5}, zoom: 7, mapTypeId: DEFAULT_OPTIONS.mapTypeId}; mapSpy = createMapSpy(options); - mapConstructorSpy = createMapConstructorSpy(mapSpy).and.callThrough(); + mapConstructorSpy = createMapConstructorSpy(mapSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.center = options.center; @@ -142,7 +142,7 @@ describe('GoogleMap', () => { mapTypeId: DEFAULT_OPTIONS.mapTypeId, }; mapSpy = createMapSpy(options); - mapConstructorSpy = createMapConstructorSpy(mapSpy).and.callThrough(); + mapConstructorSpy = createMapConstructorSpy(mapSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = options; @@ -160,7 +160,7 @@ describe('GoogleMap', () => { it('should set a default center if the custom options do not provide one', () => { const options = {zoom: 7}; mapSpy = createMapSpy(options); - mapConstructorSpy = createMapConstructorSpy(mapSpy).and.callThrough(); + mapConstructorSpy = createMapConstructorSpy(mapSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = options; @@ -172,7 +172,7 @@ describe('GoogleMap', () => { it('should set a default zoom level if the custom options do not provide one', () => { const options = {}; mapSpy = createMapSpy(options); - mapConstructorSpy = createMapConstructorSpy(mapSpy).and.callThrough(); + mapConstructorSpy = createMapConstructorSpy(mapSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = options; @@ -184,7 +184,7 @@ describe('GoogleMap', () => { it('should not set a default zoom level if the custom options provide "zoom: 0"', () => { const options = {zoom: 0}; mapSpy = createMapSpy(options); - mapConstructorSpy = createMapConstructorSpy(mapSpy).and.callThrough(); + mapConstructorSpy = createMapConstructorSpy(mapSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = options; @@ -216,7 +216,7 @@ describe('GoogleMap', () => { it('exposes methods that change the configuration of the Google Map', () => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); @@ -238,7 +238,7 @@ describe('GoogleMap', () => { it('exposes methods that get information about the Google Map', () => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); @@ -275,7 +275,7 @@ describe('GoogleMap', () => { it('initializes event handlers that are set on the map', () => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); const addSpy = mapSpy.addListener; const fixture = TestBed.createComponent(TestApp); @@ -303,7 +303,7 @@ describe('GoogleMap', () => { it('should be able to add an event listener after init', () => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); const addSpy = mapSpy.addListener; const fixture = TestBed.createComponent(TestApp); @@ -321,7 +321,7 @@ describe('GoogleMap', () => { it('should set the map type', () => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - mapConstructorSpy = createMapConstructorSpy(mapSpy).and.callThrough(); + mapConstructorSpy = createMapConstructorSpy(mapSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.mapTypeId = 'terrain' as unknown as google.maps.MapTypeId; @@ -341,7 +341,7 @@ describe('GoogleMap', () => { it('sets mapTypeId through the options', () => { const options = {mapTypeId: 'satellite'}; mapSpy = createMapSpy(options); - mapConstructorSpy = createMapConstructorSpy(mapSpy).and.callThrough(); + mapConstructorSpy = createMapConstructorSpy(mapSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = options; fixture.detectChanges(); diff --git a/src/google-maps/google-map/google-map.ts b/src/google-maps/google-map/google-map.ts index fece3bc22657..d2cf0800a0a5 100644 --- a/src/google-maps/google-map/google-map.ts +++ b/src/google-maps/google-map/google-map.ts @@ -29,6 +29,7 @@ import { import {isPlatformBrowser} from '@angular/common'; import {Observable} from 'rxjs'; import {MapEventManager} from '../map-event-manager'; +import {take} from 'rxjs/operators'; interface GoogleMapsWindow extends Window { gm_authFailure?: () => void; @@ -307,15 +308,19 @@ 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(() => { - this.googleMap = new google.maps.Map(this._mapEl, this._combineOptions()); + this._ngZone.runOutsideAngular(async () => { + const mapConstructor = + google.maps.Map || + ((await google.maps.importLibrary('maps')) as google.maps.MapsLibrary).Map; + this.googleMap = new mapConstructor(this._mapEl, this._combineOptions()); + this._eventManager.setTarget(this.googleMap); + this.mapInitialized.emit(this.googleMap); }); - this._eventManager.setTarget(this.googleMap); - this.mapInitialized.emit(this.googleMap); } } ngOnDestroy() { + this.mapInitialized.complete(); this._eventManager.destroy(); if (this._isBrowser) { @@ -483,6 +488,11 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy { return this.googleMap.overlayMapTypes; } + /** Returns a promise that resolves when the map has been initialized. */ + async _resolveMap(): Promise { + return this.googleMap || this.mapInitialized.pipe(take(1)).toPromise(); + } + private _setSize() { if (this._mapEl) { const styles = this._mapEl.style; diff --git a/src/google-maps/map-base-layer.ts b/src/google-maps/map-base-layer.ts index 5448a9e5678b..1ea8f85429e1 100644 --- a/src/google-maps/map-base-layer.ts +++ b/src/google-maps/map-base-layer.ts @@ -24,13 +24,13 @@ export class MapBaseLayer implements OnInit, OnDestroy { protected readonly _ngZone: NgZone, ) {} - ngOnInit() { + async ngOnInit() { if (this._map._isBrowser) { - this._ngZone.runOutsideAngular(() => { - this._initializeObject(); + this._ngZone.runOutsideAngular(async () => { + const map = await this._map._resolveMap(); + await this._initializeObject(); + this._setMap(map); }); - this._assertInitialized(); - this._setMap(); } } @@ -38,16 +38,7 @@ export class MapBaseLayer implements OnInit, OnDestroy { this._unsetMap(); } - 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 async _initializeObject() {} + protected _setMap(_map: google.maps.Map) {} protected _unsetMap() {} } diff --git a/src/google-maps/map-bicycling-layer/map-bicycling-layer.spec.ts b/src/google-maps/map-bicycling-layer/map-bicycling-layer.spec.ts index d523854b06d4..b090236e7cce 100644 --- a/src/google-maps/map-bicycling-layer/map-bicycling-layer.spec.ts +++ b/src/google-maps/map-bicycling-layer/map-bicycling-layer.spec.ts @@ -1,5 +1,5 @@ import {Component} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; import { @@ -16,24 +16,23 @@ describe('MapBicyclingLayer', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Map Bicycling Layer', () => { + it('initializes a Google Map Bicycling Layer', fakeAsync(() => { const bicyclingLayerSpy = createBicyclingLayerSpy(); - const bicyclingLayerConstructorSpy = - createBicyclingLayerConstructorSpy(bicyclingLayerSpy).and.callThrough(); - + const bicyclingLayerConstructorSpy = createBicyclingLayerConstructorSpy(bicyclingLayerSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(bicyclingLayerConstructorSpy).toHaveBeenCalled(); expect(bicyclingLayerSpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); }); @Component({ diff --git a/src/google-maps/map-bicycling-layer/map-bicycling-layer.ts b/src/google-maps/map-bicycling-layer/map-bicycling-layer.ts index bf1536f21605..9935e2c94b4f 100644 --- a/src/google-maps/map-bicycling-layer/map-bicycling-layer.ts +++ b/src/google-maps/map-bicycling-layer/map-bicycling-layer.ts @@ -9,7 +9,7 @@ // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 /// -import {Directive} from '@angular/core'; +import {Directive, EventEmitter, Output} from '@angular/core'; import {MapBaseLayer} from '../map-base-layer'; @@ -31,19 +31,25 @@ export class MapBicyclingLayer extends MapBaseLayer { */ bicyclingLayer?: google.maps.BicyclingLayer; - protected override _initializeObject() { - this.bicyclingLayer = new google.maps.BicyclingLayer(); + /** Event emitted when the bicycling layer is initialized. */ + @Output() readonly bicyclingLayerInitialized: EventEmitter = + new EventEmitter(); + + protected override async _initializeObject() { + const layerConstructor = + google.maps.BicyclingLayer || + ((await google.maps.importLibrary('maps')) as google.maps.MapsLibrary).BicyclingLayer; + this.bicyclingLayer = new layerConstructor(); + this.bicyclingLayerInitialized.emit(this.bicyclingLayer); } - protected override _setMap() { + protected override _setMap(map: google.maps.Map) { this._assertLayerInitialized(); - this.bicyclingLayer.setMap(this._map.googleMap!); + this.bicyclingLayer.setMap(map); } protected override _unsetMap() { - if (this.bicyclingLayer) { - this.bicyclingLayer.setMap(null); - } + this.bicyclingLayer?.setMap(null); } private _assertLayerInitialized(): asserts this is {bicyclingLayer: google.maps.BicyclingLayer} { diff --git a/src/google-maps/map-circle/map-circle.spec.ts b/src/google-maps/map-circle/map-circle.spec.ts index e86b92eae4f6..d7cefaebf4c1 100644 --- a/src/google-maps/map-circle/map-circle.spec.ts +++ b/src/google-maps/map-circle/map-circle.spec.ts @@ -1,5 +1,5 @@ import {Component, ViewChild} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; @@ -31,64 +31,68 @@ describe('MapCircle', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Map Circle', () => { + it('initializes a Google Map Circle', fakeAsync(() => { const circleSpy = createCircleSpy({}); - const circleConstructorSpy = createCircleConstructorSpy(circleSpy).and.callThrough(); + const circleConstructorSpy = createCircleConstructorSpy(circleSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(circleConstructorSpy).toHaveBeenCalledWith({center: undefined, radius: undefined}); expect(circleSpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); - it('sets center and radius from input', () => { + it('sets center and radius from input', fakeAsync(() => { const center: google.maps.LatLngLiteral = {lat: 3, lng: 5}; const radius = 15; const options: google.maps.CircleOptions = {center, radius}; const circleSpy = createCircleSpy(options); - const circleConstructorSpy = createCircleConstructorSpy(circleSpy).and.callThrough(); + const circleConstructorSpy = createCircleConstructorSpy(circleSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.center = center; fixture.componentInstance.radius = radius; fixture.detectChanges(); + flush(); expect(circleConstructorSpy).toHaveBeenCalledWith(options); - }); + })); - it('gives precedence to other inputs over options', () => { + it('gives precedence to other inputs over options', fakeAsync(() => { const center: google.maps.LatLngLiteral = {lat: 3, lng: 5}; const radius = 15; const expectedOptions: google.maps.CircleOptions = {...circleOptions, center, radius}; const circleSpy = createCircleSpy(expectedOptions); - const circleConstructorSpy = createCircleConstructorSpy(circleSpy).and.callThrough(); + const circleConstructorSpy = createCircleConstructorSpy(circleSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = circleOptions; fixture.componentInstance.center = center; fixture.componentInstance.radius = radius; fixture.detectChanges(); + flush(); expect(circleConstructorSpy).toHaveBeenCalledWith(expectedOptions); - }); + })); - it('exposes methods that provide information about the Circle', () => { + it('exposes methods that provide information about the Circle', fakeAsync(() => { const circleSpy = createCircleSpy(circleOptions); - createCircleConstructorSpy(circleSpy).and.callThrough(); + createCircleConstructorSpy(circleSpy); const fixture = TestBed.createComponent(TestApp); const circleComponent = fixture.debugElement .query(By.directive(MapCircle))! .injector.get(MapCircle); fixture.detectChanges(); + flush(); circleComponent.getCenter(); expect(circleSpy.getCenter).toHaveBeenCalled(); @@ -104,15 +108,16 @@ describe('MapCircle', () => { circleSpy.getVisible.and.returnValue(true); expect(circleComponent.getVisible()).toBe(true); - }); + })); - it('initializes Circle event handlers', () => { + it('initializes Circle event handlers', fakeAsync(() => { const circleSpy = createCircleSpy(circleOptions); - createCircleConstructorSpy(circleSpy).and.callThrough(); + createCircleConstructorSpy(circleSpy); const addSpy = circleSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).toHaveBeenCalledWith('center_changed', jasmine.any(Function)); expect(addSpy).toHaveBeenCalledWith('click', jasmine.any(Function)); @@ -127,15 +132,16 @@ describe('MapCircle', () => { expect(addSpy).not.toHaveBeenCalledWith('mouseup', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('radius_changed', jasmine.any(Function)); expect(addSpy).toHaveBeenCalledWith('rightclick', jasmine.any(Function)); - }); + })); - it('should be able to add an event listener after init', () => { + it('should be able to add an event listener after init', fakeAsync(() => { const circleSpy = createCircleSpy(circleOptions); - createCircleConstructorSpy(circleSpy).and.callThrough(); + createCircleConstructorSpy(circleSpy); const addSpy = circleSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).not.toHaveBeenCalledWith('dragend', jasmine.any(Function)); @@ -145,7 +151,7 @@ describe('MapCircle', () => { expect(addSpy).toHaveBeenCalledWith('dragend', jasmine.any(Function)); subscription.unsubscribe(); - }); + })); }); @Component({ diff --git a/src/google-maps/map-circle/map-circle.ts b/src/google-maps/map-circle/map-circle.ts index a1ce7f6070ec..f42766af0d43 100644 --- a/src/google-maps/map-circle/map-circle.ts +++ b/src/google-maps/map-circle/map-circle.ts @@ -9,7 +9,16 @@ // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 /// -import {Directive, Input, NgZone, OnDestroy, OnInit, Output, inject} from '@angular/core'; +import { + Directive, + EventEmitter, + Input, + NgZone, + OnDestroy, + OnInit, + Output, + inject, +} from '@angular/core'; import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs'; import {map, take, takeUntil} from 'rxjs/operators'; @@ -148,40 +157,48 @@ export class MapCircle implements OnInit, OnDestroy { @Output() readonly circleRightclick: Observable = this._eventManager.getLazyEmitter('rightclick'); + /** Event emitted when the circle is initialized. */ + @Output() readonly circleInitialized: EventEmitter = + new EventEmitter(); + constructor( private readonly _map: GoogleMap, private readonly _ngZone: NgZone, ) {} ngOnInit() { - if (this._map._isBrowser) { - 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(() => { - this.circle = new google.maps.Circle(options); - }); + if (!this._map._isBrowser) { + return; + } + + 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 google.maps.importLibrary('maps')) as google.maps.MapsLibrary).Circle; + this.circle = new circleConstructor(options); this._assertInitialized(); - this.circle.setMap(this._map.googleMap!); + this.circle.setMap(map); this._eventManager.setTarget(this.circle); + this.circleInitialized.emit(this.circle); + this._watchForOptionsChanges(); + this._watchForCenterChanges(); + this._watchForRadiusChanges(); }); - - this._watchForOptionsChanges(); - this._watchForCenterChanges(); - this._watchForRadiusChanges(); - } + }); } ngOnDestroy() { this._eventManager.destroy(); this._destroyed.next(); this._destroyed.complete(); - if (this.circle) { - this.circle.setMap(null); - } + this.circle?.setMap(null); } /** @@ -278,12 +295,6 @@ export class MapCircle implements OnInit, OnDestroy { private _assertInitialized(): asserts this is {circle: google.maps.Circle} { if (typeof ngDevMode === 'undefined' || ngDevMode) { - 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.', - ); - } if (!this.circle) { throw Error( 'Cannot interact with a Google Map Circle before it has been ' + diff --git a/src/google-maps/map-directions-renderer/map-directions-renderer.spec.ts b/src/google-maps/map-directions-renderer/map-directions-renderer.spec.ts index 55421e79cd97..48ee83e8f238 100644 --- a/src/google-maps/map-directions-renderer/map-directions-renderer.spec.ts +++ b/src/google-maps/map-directions-renderer/map-directions-renderer.spec.ts @@ -1,5 +1,5 @@ import {Component, ViewChild} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {MapDirectionsRenderer} from './map-directions-renderer'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; @@ -20,69 +20,72 @@ describe('MapDirectionsRenderer', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Maps DirectionsRenderer', () => { + it('initializes a Google Maps DirectionsRenderer', fakeAsync(() => { const directionsRendererSpy = createDirectionsRendererSpy({directions: DEFAULT_DIRECTIONS}); const directionsRendererConstructorSpy = - createDirectionsRendererConstructorSpy(directionsRendererSpy).and.callThrough(); + createDirectionsRendererConstructorSpy(directionsRendererSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = {directions: DEFAULT_DIRECTIONS}; fixture.detectChanges(); + flush(); expect(directionsRendererConstructorSpy).toHaveBeenCalledWith({ directions: DEFAULT_DIRECTIONS, map: jasmine.any(Object), }); expect(directionsRendererSpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); - it('sets directions from directions input', () => { + it('sets directions from directions input', fakeAsync(() => { const directionsRendererSpy = createDirectionsRendererSpy({directions: DEFAULT_DIRECTIONS}); const directionsRendererConstructorSpy = - createDirectionsRendererConstructorSpy(directionsRendererSpy).and.callThrough(); + createDirectionsRendererConstructorSpy(directionsRendererSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.directions = DEFAULT_DIRECTIONS; fixture.detectChanges(); + flush(); expect(directionsRendererConstructorSpy).toHaveBeenCalledWith({ directions: DEFAULT_DIRECTIONS, map: jasmine.any(Object), }); expect(directionsRendererSpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); - it('gives precedence to directions over options', () => { + it('gives precedence to directions over options', fakeAsync(() => { const updatedDirections: google.maps.DirectionsResult = { geocoded_waypoints: [{partial_match: false, place_id: 'test', types: []}], routes: [], }; const directionsRendererSpy = createDirectionsRendererSpy({directions: updatedDirections}); const directionsRendererConstructorSpy = - createDirectionsRendererConstructorSpy(directionsRendererSpy).and.callThrough(); + createDirectionsRendererConstructorSpy(directionsRendererSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = {directions: DEFAULT_DIRECTIONS}; fixture.componentInstance.directions = updatedDirections; fixture.detectChanges(); + flush(); expect(directionsRendererConstructorSpy).toHaveBeenCalledWith({ directions: updatedDirections, map: jasmine.any(Object), }); expect(directionsRendererSpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); - it('exposes methods that provide information from the DirectionsRenderer', () => { + it('exposes methods that provide information from the DirectionsRenderer', fakeAsync(() => { const directionsRendererSpy = createDirectionsRendererSpy({}); - createDirectionsRendererConstructorSpy(directionsRendererSpy).and.callThrough(); + createDirectionsRendererConstructorSpy(directionsRendererSpy); const fixture = TestBed.createComponent(TestApp); @@ -90,6 +93,7 @@ describe('MapDirectionsRenderer', () => { .query(By.directive(MapDirectionsRenderer))! .injector.get(MapDirectionsRenderer); fixture.detectChanges(); + flush(); directionsRendererSpy.getDirections.and.returnValue(DEFAULT_DIRECTIONS); expect(directionsRendererComponent.getDirections()).toBe(DEFAULT_DIRECTIONS); @@ -99,20 +103,21 @@ describe('MapDirectionsRenderer', () => { directionsRendererSpy.getRouteIndex.and.returnValue(10); expect(directionsRendererComponent.getRouteIndex()).toBe(10); - }); + })); - it('initializes DirectionsRenderer event handlers', () => { + it('initializes DirectionsRenderer event handlers', fakeAsync(() => { const directionsRendererSpy = createDirectionsRendererSpy({}); - createDirectionsRendererConstructorSpy(directionsRendererSpy).and.callThrough(); + createDirectionsRendererConstructorSpy(directionsRendererSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(directionsRendererSpy.addListener).toHaveBeenCalledWith( 'directions_changed', jasmine.any(Function), ); - }); + })); }); @Component({ diff --git a/src/google-maps/map-directions-renderer/map-directions-renderer.ts b/src/google-maps/map-directions-renderer/map-directions-renderer.ts index c32f83f4f5db..8c28b42b4f32 100644 --- a/src/google-maps/map-directions-renderer/map-directions-renderer.ts +++ b/src/google-maps/map-directions-renderer/map-directions-renderer.ts @@ -11,6 +11,7 @@ import { Directive, + EventEmitter, Input, NgZone, OnChanges, @@ -66,6 +67,10 @@ export class MapDirectionsRenderer implements OnInit, OnChanges, OnDestroy { readonly directionsChanged: Observable = this._eventManager.getLazyEmitter('directions_changed'); + /** Event emitted when the directions renderer is initialized. */ + @Output() readonly directionsRendererInitialized: EventEmitter = + new EventEmitter(); + /** The underlying google.maps.DirectionsRenderer object. */ directionsRenderer?: google.maps.DirectionsRenderer; @@ -79,12 +84,18 @@ export class MapDirectionsRenderer 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(() => { - this.directionsRenderer = new google.maps.DirectionsRenderer(this._combineOptions()); + this._ngZone.runOutsideAngular(async () => { + const map = await this._googleMap._resolveMap(); + const rendererConstructor = + google.maps.DirectionsRenderer || + ((await google.maps.importLibrary('routes')) as google.maps.RoutesLibrary) + .DirectionsRenderer; + this.directionsRenderer = new rendererConstructor(this._combineOptions()); + this._assertInitialized(); + this.directionsRenderer.setMap(map); + this._eventManager.setTarget(this.directionsRenderer); + this.directionsRendererInitialized.emit(this.directionsRenderer); }); - this._assertInitialized(); - this.directionsRenderer.setMap(this._googleMap.googleMap!); - this._eventManager.setTarget(this.directionsRenderer); } } @@ -102,9 +113,7 @@ export class MapDirectionsRenderer implements OnInit, OnChanges, OnDestroy { ngOnDestroy() { this._eventManager.destroy(); - if (this.directionsRenderer) { - this.directionsRenderer.setMap(null); - } + this.directionsRenderer?.setMap(null); } /** @@ -147,12 +156,6 @@ export class MapDirectionsRenderer implements OnInit, OnChanges, OnDestroy { directionsRenderer: google.maps.DirectionsRenderer; } { if (typeof ngDevMode === 'undefined' || ngDevMode) { - if (!this._googleMap.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.', - ); - } if (!this.directionsRenderer) { throw Error( 'Cannot interact with a Google Map Directions Renderer before it has been ' + diff --git a/src/google-maps/map-directions-renderer/map-directions-service.spec.ts b/src/google-maps/map-directions-renderer/map-directions-service.spec.ts index b2fcfb9a7741..0c7fb8f522f9 100644 --- a/src/google-maps/map-directions-renderer/map-directions-service.spec.ts +++ b/src/google-maps/map-directions-renderer/map-directions-service.spec.ts @@ -12,8 +12,7 @@ describe('MapDirectionsService', () => { beforeEach(() => { directionsServiceSpy = createDirectionsServiceSpy(); - directionsServiceConstructorSpy = - createDirectionsServiceConstructorSpy(directionsServiceSpy).and.callThrough(); + directionsServiceConstructorSpy = createDirectionsServiceConstructorSpy(directionsServiceSpy); mapDirectionsService = TestBed.inject(MapDirectionsService); }); diff --git a/src/google-maps/map-directions-renderer/map-directions-service.ts b/src/google-maps/map-directions-renderer/map-directions-service.ts index e5aae996e3db..4bff705cf2c5 100644 --- a/src/google-maps/map-directions-renderer/map-directions-service.ts +++ b/src/google-maps/map-directions-renderer/map-directions-service.ts @@ -36,18 +36,26 @@ export class MapDirectionsService { */ route(request: google.maps.DirectionsRequest): Observable { return new Observable(observer => { - // Initialize the `DirectionsService` lazily since the Google Maps API may - // not have been loaded when the provider is instantiated. - if (!this._directionsService) { - this._directionsService = new google.maps.DirectionsService(); - } - - this._directionsService.route(request, (result, status) => { - this._ngZone.run(() => { - observer.next({result: result || undefined, status}); - observer.complete(); + this._getService().then(service => { + service.route(request, (result, status) => { + this._ngZone.run(() => { + observer.next({result: result || undefined, status}); + observer.complete(); + }); }); }); }); } + + private async _getService(): Promise { + if (!this._directionsService) { + const serviceConstructor = + google.maps.DirectionsService || + ((await google.maps.importLibrary('routes')) as google.maps.RoutesLibrary) + .DirectionsService; + this._directionsService = new serviceConstructor(); + } + + return this._directionsService; + } } diff --git a/src/google-maps/map-geocoder/map-geocoder.spec.ts b/src/google-maps/map-geocoder/map-geocoder.spec.ts index 57ec0ef3f807..3bc234d2dc09 100644 --- a/src/google-maps/map-geocoder/map-geocoder.spec.ts +++ b/src/google-maps/map-geocoder/map-geocoder.spec.ts @@ -9,7 +9,7 @@ describe('MapGeocoder', () => { beforeEach(() => { geocoderSpy = createGeocoderSpy(); - geocoderConstructorSpy = createGeocoderConstructorSpy(geocoderSpy).and.callThrough(); + geocoderConstructorSpy = createGeocoderConstructorSpy(geocoderSpy); geocoder = TestBed.inject(MapGeocoder); }); diff --git a/src/google-maps/map-geocoder/map-geocoder.ts b/src/google-maps/map-geocoder/map-geocoder.ts index cb18f3588c55..87a1fc94b4e5 100644 --- a/src/google-maps/map-geocoder/map-geocoder.ts +++ b/src/google-maps/map-geocoder/map-geocoder.ts @@ -32,18 +32,25 @@ export class MapGeocoder { */ geocode(request: google.maps.GeocoderRequest): Observable { return new Observable(observer => { - // Initialize the `Geocoder` lazily since the Google Maps API may - // not have been loaded when the provider is instantiated. - if (!this._geocoder) { - this._geocoder = new google.maps.Geocoder(); - } - - this._geocoder.geocode(request, (results, status) => { - this._ngZone.run(() => { - observer.next({results: results || [], status}); - observer.complete(); + this._getGeocoder().then(geocoder => { + geocoder.geocode(request, (results, status) => { + this._ngZone.run(() => { + observer.next({results: results || [], status}); + observer.complete(); + }); }); }); }); } + + private async _getGeocoder(): Promise { + if (!this._geocoder) { + const geocoderConstructor = + google.maps.Geocoder || + ((await google.maps.importLibrary('geocoding')) as google.maps.GeocodingLibrary).Geocoder; + this._geocoder = new geocoderConstructor(); + } + + return this._geocoder; + } } diff --git a/src/google-maps/map-ground-overlay/map-ground-overlay.spec.ts b/src/google-maps/map-ground-overlay/map-ground-overlay.spec.ts index 65e4846d11ba..60e5ab0b9901 100644 --- a/src/google-maps/map-ground-overlay/map-ground-overlay.spec.ts +++ b/src/google-maps/map-ground-overlay/map-ground-overlay.spec.ts @@ -1,5 +1,5 @@ import {Component, ViewChild} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; @@ -22,17 +22,16 @@ describe('MapGroundOverlay', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Map Ground Overlay', () => { + it('initializes a Google Map Ground Overlay', fakeAsync(() => { const groundOverlaySpy = createGroundOverlaySpy(url, bounds, groundOverlayOptions); - const groundOverlayConstructorSpy = - createGroundOverlayConstructorSpy(groundOverlaySpy).and.callThrough(); + const groundOverlayConstructorSpy = createGroundOverlayConstructorSpy(groundOverlaySpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.url = url; @@ -40,14 +39,15 @@ describe('MapGroundOverlay', () => { fixture.componentInstance.clickable = clickable; fixture.componentInstance.opacity = opacity; fixture.detectChanges(); + flush(); expect(groundOverlayConstructorSpy).toHaveBeenCalledWith(url, bounds, groundOverlayOptions); expect(groundOverlaySpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); - it('exposes methods that provide information about the Ground Overlay', () => { + it('exposes methods that provide information about the Ground Overlay', fakeAsync(() => { const groundOverlaySpy = createGroundOverlaySpy(url, bounds, groundOverlayOptions); - createGroundOverlayConstructorSpy(groundOverlaySpy).and.callThrough(); + createGroundOverlayConstructorSpy(groundOverlaySpy); const fixture = TestBed.createComponent(TestApp); const groundOverlayComponent = fixture.debugElement @@ -57,6 +57,7 @@ describe('MapGroundOverlay', () => { fixture.componentInstance.bounds = bounds; fixture.componentInstance.opacity = opacity; fixture.detectChanges(); + flush(); groundOverlayComponent.getBounds(); expect(groundOverlaySpy.getBounds).toHaveBeenCalled(); @@ -66,31 +67,33 @@ describe('MapGroundOverlay', () => { groundOverlaySpy.getUrl.and.returnValue(url); expect(groundOverlayComponent.getUrl()).toBe(url); - }); + })); - it('initializes Ground Overlay event handlers', () => { + it('initializes Ground Overlay event handlers', fakeAsync(() => { const groundOverlaySpy = createGroundOverlaySpy(url, bounds, groundOverlayOptions); - createGroundOverlayConstructorSpy(groundOverlaySpy).and.callThrough(); + createGroundOverlayConstructorSpy(groundOverlaySpy); const addSpy = groundOverlaySpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.url = url; fixture.componentInstance.bounds = bounds; fixture.detectChanges(); + flush(); expect(addSpy).toHaveBeenCalledWith('click', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('dblclick', jasmine.any(Function)); - }); + })); - it('should be able to add an event listener after init', () => { + it('should be able to add an event listener after init', fakeAsync(() => { const groundOverlaySpy = createGroundOverlaySpy(url, bounds, groundOverlayOptions); - createGroundOverlayConstructorSpy(groundOverlaySpy).and.callThrough(); + createGroundOverlayConstructorSpy(groundOverlaySpy); const addSpy = groundOverlaySpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.url = url; fixture.componentInstance.bounds = bounds; fixture.detectChanges(); + flush(); expect(addSpy).not.toHaveBeenCalledWith('dblclick', jasmine.any(Function)); @@ -100,12 +103,11 @@ describe('MapGroundOverlay', () => { expect(addSpy).toHaveBeenCalledWith('dblclick', jasmine.any(Function)); subscription.unsubscribe(); - }); + })); - it('should be able to change the image after init', () => { + it('should be able to change the image after init', fakeAsync(() => { const groundOverlaySpy = createGroundOverlaySpy(url, bounds, groundOverlayOptions); - const groundOverlayConstructorSpy = - createGroundOverlayConstructorSpy(groundOverlaySpy).and.callThrough(); + const groundOverlayConstructorSpy = createGroundOverlayConstructorSpy(groundOverlaySpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.url = url; @@ -113,6 +115,7 @@ describe('MapGroundOverlay', () => { fixture.componentInstance.clickable = clickable; fixture.componentInstance.opacity = opacity; fixture.detectChanges(); + flush(); expect(groundOverlayConstructorSpy).toHaveBeenCalledWith(url, bounds, groundOverlayOptions); expect(groundOverlaySpy.setMap).toHaveBeenCalledWith(mapSpy); @@ -125,23 +128,25 @@ describe('MapGroundOverlay', () => { expect(groundOverlaySpy.setMap).toHaveBeenCalledTimes(2); expect(groundOverlaySpy.setMap).toHaveBeenCalledWith(null); expect(groundOverlaySpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); - it('should recreate the ground overlay when the bounds change', () => { + it('should recreate the ground overlay when the bounds change', fakeAsync(() => { const groundOverlaySpy = createGroundOverlaySpy(url, bounds, groundOverlayOptions); - createGroundOverlayConstructorSpy(groundOverlaySpy).and.callThrough(); + createGroundOverlayConstructorSpy(groundOverlaySpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); const oldOverlay = fixture.componentInstance.groundOverlay.groundOverlay; fixture.componentInstance.bounds = {...bounds}; fixture.detectChanges(); + flush(); const newOverlay = fixture.componentInstance.groundOverlay.groundOverlay; expect(newOverlay).toBeTruthy(); expect(newOverlay).not.toBe(oldOverlay); - }); + })); }); @Component({ diff --git a/src/google-maps/map-ground-overlay/map-ground-overlay.ts b/src/google-maps/map-ground-overlay/map-ground-overlay.ts index c7adab03ea0f..5c5e2170b91e 100644 --- a/src/google-maps/map-ground-overlay/map-ground-overlay.ts +++ b/src/google-maps/map-ground-overlay/map-ground-overlay.ts @@ -9,7 +9,16 @@ // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 /// -import {Directive, Input, NgZone, OnDestroy, OnInit, Output, inject} from '@angular/core'; +import { + Directive, + EventEmitter, + Input, + NgZone, + OnDestroy, + OnInit, + Output, + inject, +} from '@angular/core'; import {BehaviorSubject, Observable, Subject} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; @@ -35,6 +44,7 @@ export class MapGroundOverlay implements OnInit, OnDestroy { google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral | undefined >(undefined); private readonly _destroyed = new Subject(); + private _hasWatchers: boolean; /** * The underlying google.maps.GroundOverlay object. @@ -82,6 +92,10 @@ export class MapGroundOverlay implements OnInit, OnDestroy { @Output() readonly mapDblclick: Observable = this._eventManager.getLazyEmitter('dblclick'); + /** Event emitted when the ground overlay is initialized. */ + @Output() readonly groundOverlayInitialized: EventEmitter = + new EventEmitter(); + constructor( private readonly _map: GoogleMap, private readonly _ngZone: NgZone, @@ -98,24 +112,35 @@ export class MapGroundOverlay implements OnInit, OnDestroy { this.groundOverlay = undefined; } + if (!bounds) { + 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. - if (bounds) { - this._ngZone.runOutsideAngular(() => { - this.groundOverlay = new google.maps.GroundOverlay(this._url.getValue(), bounds, { - clickable: this.clickable, - opacity: this._opacity.value, - }); + this._ngZone.runOutsideAngular(async () => { + const map = await this._map._resolveMap(); + const overlayConstructor = + google.maps.GroundOverlay || + ((await google.maps.importLibrary('maps')) as google.maps.MapsLibrary).GroundOverlay; + this.groundOverlay = new overlayConstructor(this._url.getValue(), bounds, { + clickable: this.clickable, + opacity: this._opacity.value, }); this._assertInitialized(); - this.groundOverlay.setMap(this._map.googleMap!); + 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(); + } + }); }); - - this._watchForOpacityChanges(); - this._watchForUrlChanges(); } } @@ -123,9 +148,7 @@ export class MapGroundOverlay implements OnInit, OnDestroy { this._eventManager.destroy(); this._destroyed.next(); this._destroyed.complete(); - if (this.groundOverlay) { - this.groundOverlay.setMap(null); - } + this.groundOverlay?.setMap(null); } /** @@ -161,32 +184,26 @@ export class MapGroundOverlay implements OnInit, OnDestroy { private _watchForOpacityChanges() { this._opacity.pipe(takeUntil(this._destroyed)).subscribe(opacity => { if (opacity != null) { - this._assertInitialized(); - this.groundOverlay.setOpacity(opacity); + this.groundOverlay?.setOpacity(opacity); } }); } private _watchForUrlChanges() { this._url.pipe(takeUntil(this._destroyed)).subscribe(url => { - this._assertInitialized(); const overlay = this.groundOverlay; - overlay.set('url', url); - // Google Maps only redraws the overlay if we re-set the map. - overlay.setMap(null); - overlay.setMap(this._map.googleMap!); + if (overlay) { + overlay.set('url', url); + // Google Maps only redraws the overlay if we re-set the map. + overlay.setMap(null); + overlay.setMap(this._map.googleMap!); + } }); } private _assertInitialized(): asserts this is {groundOverlay: google.maps.GroundOverlay} { if (typeof ngDevMode === 'undefined' || ngDevMode) { - 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.', - ); - } if (!this.groundOverlay) { throw Error( 'Cannot interact with a Google Map GroundOverlay before it has been initialized. ' + diff --git a/src/google-maps/map-heatmap-layer/README.md b/src/google-maps/map-heatmap-layer/README.md index 62d9867bf090..82d7200c969a 100644 --- a/src/google-maps/map-heatmap-layer/README.md +++ b/src/google-maps/map-heatmap-layer/README.md @@ -5,24 +5,6 @@ a heatmap layer on the map when it is a content child of a `GoogleMap` component this directive offers an `options` input as well as a convenience input for passing in the `data` that is shown on the heatmap. -## Requirements - -In order to render a heatmap, the Google Maps JavaScript API has to be loaded with the -`visualization` library. To load the library, you have to add `&libraries=visualization` to the -script that loads the Google Maps API. E.g. - -**Before:** -```html - -``` - -**After:** -```html - -``` - -More information: https://developers.google.com/maps/documentation/javascript/heatmaplayer - ## Example ```typescript diff --git a/src/google-maps/map-heatmap-layer/map-heatmap-layer.spec.ts b/src/google-maps/map-heatmap-layer/map-heatmap-layer.spec.ts index 1f81668cc6f2..d4ddabbb17fe 100644 --- a/src/google-maps/map-heatmap-layer/map-heatmap-layer.spec.ts +++ b/src/google-maps/map-heatmap-layer/map-heatmap-layer.spec.ts @@ -1,5 +1,5 @@ import {Component, ViewChild} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; @@ -20,38 +20,40 @@ describe('MapHeatmapLayer', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); latLngSpy = createLatLngSpy(); - createMapConstructorSpy(mapSpy).and.callThrough(); - createLatLngConstructorSpy(latLngSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); + createLatLngConstructorSpy(latLngSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Map heatmap layer', () => { + it('initializes a Google Map heatmap layer', fakeAsync(() => { const heatmapSpy = createHeatmapLayerSpy(); - const heatmapConstructorSpy = createHeatmapLayerConstructorSpy(heatmapSpy).and.callThrough(); + const heatmapConstructorSpy = createHeatmapLayerConstructorSpy(heatmapSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(heatmapConstructorSpy).toHaveBeenCalledWith({ data: [], map: mapSpy, }); - }); + })); - it('should throw if the `visualization` library has not been loaded', () => { + it('should throw if the `visualization` library has not been loaded', fakeAsync(() => { createHeatmapLayerConstructorSpy(createHeatmapLayerSpy()); delete (window.google.maps as any).visualization; expect(() => { const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); }).toThrowError(/Namespace `google.maps.visualization` not found, cannot construct heatmap/); - }); + })); - it('sets heatmap inputs', () => { + it('sets heatmap inputs', fakeAsync(() => { const options: google.maps.visualization.HeatmapLayerOptions = { map: mapSpy, data: [ @@ -61,16 +63,17 @@ describe('MapHeatmapLayer', () => { ], }; const heatmapSpy = createHeatmapLayerSpy(); - const heatmapConstructorSpy = createHeatmapLayerConstructorSpy(heatmapSpy).and.callThrough(); + const heatmapConstructorSpy = createHeatmapLayerConstructorSpy(heatmapSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.data = options.data; fixture.detectChanges(); + flush(); expect(heatmapConstructorSpy).toHaveBeenCalledWith(options); - }); + })); - it('sets heatmap options, ignoring map', () => { + it('sets heatmap options, ignoring map', fakeAsync(() => { const options: Partial = { radius: 5, dissipating: true, @@ -81,31 +84,33 @@ describe('MapHeatmapLayer', () => { new google.maps.LatLng(37.782, -122.443), ]; const heatmapSpy = createHeatmapLayerSpy(); - const heatmapConstructorSpy = createHeatmapLayerConstructorSpy(heatmapSpy).and.callThrough(); + const heatmapConstructorSpy = createHeatmapLayerConstructorSpy(heatmapSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.data = data; fixture.componentInstance.options = options; fixture.detectChanges(); + flush(); expect(heatmapConstructorSpy).toHaveBeenCalledWith({...options, map: mapSpy, data}); - }); + })); - it('exposes methods that provide information about the heatmap', () => { + it('exposes methods that provide information about the heatmap', fakeAsync(() => { const heatmapSpy = createHeatmapLayerSpy(); - createHeatmapLayerConstructorSpy(heatmapSpy).and.callThrough(); + createHeatmapLayerConstructorSpy(heatmapSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); const heatmap = fixture.componentInstance.heatmap; heatmapSpy.getData.and.returnValue([] as any); expect(heatmap.getData()).toEqual([]); - }); + })); - it('should update the heatmap data when the input changes', () => { + it('should update the heatmap data when the input changes', fakeAsync(() => { const heatmapSpy = createHeatmapLayerSpy(); - const heatmapConstructorSpy = createHeatmapLayerConstructorSpy(heatmapSpy).and.callThrough(); + const heatmapConstructorSpy = createHeatmapLayerConstructorSpy(heatmapSpy); let data = [ new google.maps.LatLng(1, 2), new google.maps.LatLng(3, 4), @@ -115,6 +120,7 @@ describe('MapHeatmapLayer', () => { const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.data = data; fixture.detectChanges(); + flush(); expect(heatmapConstructorSpy).toHaveBeenCalledWith(jasmine.objectContaining({data})); data = [ @@ -126,22 +132,23 @@ describe('MapHeatmapLayer', () => { fixture.detectChanges(); expect(heatmapSpy.setData).toHaveBeenCalledWith(data); - }); + })); - it('should create a LatLng object if a LatLngLiteral is passed in', () => { - const latLngConstructor = createLatLngConstructorSpy(latLngSpy).and.callThrough(); - createHeatmapLayerConstructorSpy(createHeatmapLayerSpy()).and.callThrough(); + it('should create a LatLng object if a LatLngLiteral is passed in', fakeAsync(() => { + const latLngConstructor = createLatLngConstructorSpy(latLngSpy); + createHeatmapLayerConstructorSpy(createHeatmapLayerSpy()); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.data = [ {lat: 1, lng: 2}, {lat: 3, lng: 4}, ]; fixture.detectChanges(); + flush(); expect(latLngConstructor).toHaveBeenCalledWith(1, 2); expect(latLngConstructor).toHaveBeenCalledWith(3, 4); expect(latLngConstructor).toHaveBeenCalledTimes(2); - }); + })); }); @Component({ diff --git a/src/google-maps/map-heatmap-layer/map-heatmap-layer.ts b/src/google-maps/map-heatmap-layer/map-heatmap-layer.ts index 45566136d32c..a2647399a0a7 100644 --- a/src/google-maps/map-heatmap-layer/map-heatmap-layer.ts +++ b/src/google-maps/map-heatmap-layer/map-heatmap-layer.ts @@ -9,7 +9,17 @@ // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 /// -import {Input, OnDestroy, OnInit, NgZone, Directive, OnChanges, SimpleChanges} from '@angular/core'; +import { + Input, + OnDestroy, + OnInit, + NgZone, + Directive, + OnChanges, + SimpleChanges, + Output, + EventEmitter, +} from '@angular/core'; import {GoogleMap} from '../google-map/google-map'; @@ -58,6 +68,10 @@ export class MapHeatmapLayer implements OnInit, OnChanges, OnDestroy { */ heatmap?: google.maps.visualization.HeatmapLayer; + /** Event emitted when the heatmap is initialized. */ + @Output() readonly heatmapInitialized: EventEmitter = + new EventEmitter(); + constructor( private readonly _googleMap: GoogleMap, private _ngZone: NgZone, @@ -65,7 +79,11 @@ export class MapHeatmapLayer implements OnInit, OnChanges, OnDestroy { ngOnInit() { if (this._googleMap._isBrowser) { - if (!window.google?.maps?.visualization && (typeof ngDevMode === 'undefined' || ngDevMode)) { + if ( + !window.google?.maps?.visualization && + !window.google?.maps.importLibrary && + (typeof ngDevMode === 'undefined' || ngDevMode) + ) { throw Error( 'Namespace `google.maps.visualization` not found, cannot construct heatmap. ' + 'Please install the Google Maps JavaScript API with the "visualization" library: ' + @@ -76,11 +94,17 @@ 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(() => { - this.heatmap = new google.maps.visualization.HeatmapLayer(this._combineOptions()); + this._ngZone.runOutsideAngular(async () => { + const map = await this._googleMap._resolveMap(); + const heatmapConstructor = + google.maps.visualization?.HeatmapLayer || + ((await google.maps.importLibrary('visualization')) as google.maps.VisualizationLibrary) + .HeatmapLayer; + this.heatmap = new heatmapConstructor(this._combineOptions()); + this._assertInitialized(); + this.heatmap.setMap(map); + this.heatmapInitialized.emit(this.heatmap); }); - this._assertInitialized(); - this.heatmap.setMap(this._googleMap.googleMap!); } } @@ -99,9 +123,7 @@ export class MapHeatmapLayer implements OnInit, OnChanges, OnDestroy { } ngOnDestroy() { - if (this.heatmap) { - this.heatmap.setMap(null); - } + this.heatmap?.setMap(null); } /** @@ -144,12 +166,6 @@ export class MapHeatmapLayer implements OnInit, OnChanges, OnDestroy { /** Asserts that the heatmap object has been initialized. */ private _assertInitialized(): asserts this is {heatmap: google.maps.visualization.HeatmapLayer} { if (typeof ngDevMode === 'undefined' || ngDevMode) { - if (!this._googleMap.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.', - ); - } if (!this.heatmap) { throw Error( 'Cannot interact with a Google Map HeatmapLayer before it has been ' + diff --git a/src/google-maps/map-info-window/map-info-window.spec.ts b/src/google-maps/map-info-window/map-info-window.spec.ts index e510214c74d8..82497407123c 100644 --- a/src/google-maps/map-info-window/map-info-window.spec.ts +++ b/src/google-maps/map-info-window/map-info-window.spec.ts @@ -1,5 +1,5 @@ import {Component, ViewChild} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; @@ -18,17 +18,16 @@ describe('MapInfoWindow', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Map Info Window', () => { + it('initializes a Google Map Info Window', fakeAsync(() => { const infoWindowSpy = createInfoWindowSpy({}); - const infoWindowConstructorSpy = - createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough(); + const infoWindowConstructorSpy = createInfoWindowConstructorSpy(infoWindowSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); @@ -37,45 +36,45 @@ describe('MapInfoWindow', () => { position: undefined, content: jasmine.any(Node), }); - }); + })); - it('sets position', () => { + it('sets position', fakeAsync(() => { const position: google.maps.LatLngLiteral = {lat: 5, lng: 7}; const infoWindowSpy = createInfoWindowSpy({position}); - const infoWindowConstructorSpy = - createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough(); + const infoWindowConstructorSpy = createInfoWindowConstructorSpy(infoWindowSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.position = position; fixture.detectChanges(); + flush(); expect(infoWindowConstructorSpy).toHaveBeenCalledWith({ position, content: jasmine.any(Node), }); - }); + })); - it('sets options', () => { + it('sets options', fakeAsync(() => { const options: google.maps.InfoWindowOptions = { position: {lat: 3, lng: 5}, maxWidth: 50, disableAutoPan: true, }; const infoWindowSpy = createInfoWindowSpy(options); - const infoWindowConstructorSpy = - createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough(); + const infoWindowConstructorSpy = createInfoWindowConstructorSpy(infoWindowSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = options; fixture.detectChanges(); + flush(); expect(infoWindowConstructorSpy).toHaveBeenCalledWith({ ...options, content: jasmine.any(Node), }); - }); + })); - it('gives preference to position over options', () => { + it('gives preference to position over options', fakeAsync(() => { const position: google.maps.LatLngLiteral = {lat: 5, lng: 7}; const options: google.maps.InfoWindowOptions = { position: {lat: 3, lng: 5}, @@ -83,35 +82,36 @@ describe('MapInfoWindow', () => { disableAutoPan: true, }; const infoWindowSpy = createInfoWindowSpy({...options, position}); - const infoWindowConstructorSpy = - createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough(); + const infoWindowConstructorSpy = createInfoWindowConstructorSpy(infoWindowSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = options; fixture.componentInstance.position = position; fixture.detectChanges(); + flush(); expect(infoWindowConstructorSpy).toHaveBeenCalledWith({ ...options, position, content: jasmine.any(Node), }); - }); + })); - it('exposes methods that change the configuration of the info window', () => { + it('exposes methods that change the configuration of the info window', fakeAsync(() => { const fakeMarker = {} as unknown as google.maps.Marker; const fakeMarkerComponent = { marker: fakeMarker, getAnchor: () => fakeMarker, } as unknown as MapMarker; const infoWindowSpy = createInfoWindowSpy({}); - createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough(); + createInfoWindowConstructorSpy(infoWindowSpy); const fixture = TestBed.createComponent(TestApp); const infoWindowComponent = fixture.debugElement .query(By.directive(MapInfoWindow))! .injector.get(MapInfoWindow); fixture.detectChanges(); + flush(); infoWindowComponent.close(); expect(infoWindowSpy.close).toHaveBeenCalled(); @@ -124,22 +124,23 @@ describe('MapInfoWindow', () => { shouldFocus: undefined, }), ); - }); + })); - it('should not try to reopen info window multiple times for the same marker', () => { + it('should not try to reopen info window multiple times for the same marker', fakeAsync(() => { const fakeMarker = {} as unknown as google.maps.Marker; const fakeMarkerComponent = { marker: fakeMarker, getAnchor: () => fakeMarker, } as unknown as MapMarker; const infoWindowSpy = createInfoWindowSpy({}); - createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough(); + createInfoWindowConstructorSpy(infoWindowSpy); const fixture = TestBed.createComponent(TestApp); const infoWindowComponent = fixture.debugElement .query(By.directive(MapInfoWindow))! .injector.get(MapInfoWindow); fixture.detectChanges(); + flush(); infoWindowComponent.open(fakeMarkerComponent); expect(infoWindowSpy.open).toHaveBeenCalledTimes(1); @@ -150,17 +151,18 @@ describe('MapInfoWindow', () => { infoWindowComponent.close(); infoWindowComponent.open(fakeMarkerComponent); expect(infoWindowSpy.open).toHaveBeenCalledTimes(2); - }); + })); - it('exposes methods that provide information about the info window', () => { + it('exposes methods that provide information about the info window', fakeAsync(() => { const infoWindowSpy = createInfoWindowSpy({}); - createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough(); + createInfoWindowConstructorSpy(infoWindowSpy); const fixture = TestBed.createComponent(TestApp); const infoWindowComponent = fixture.debugElement .query(By.directive(MapInfoWindow))! .injector.get(MapInfoWindow); fixture.detectChanges(); + flush(); infoWindowSpy.getContent.and.returnValue('test content'); expect(infoWindowComponent.getContent()).toBe('test content'); @@ -170,30 +172,32 @@ describe('MapInfoWindow', () => { infoWindowSpy.getZIndex.and.returnValue(5); expect(infoWindowComponent.getZIndex()).toBe(5); - }); + })); - it('initializes info window event handlers', () => { + it('initializes info window event handlers', fakeAsync(() => { const infoWindowSpy = createInfoWindowSpy({}); - createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough(); + createInfoWindowConstructorSpy(infoWindowSpy); const addSpy = infoWindowSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).toHaveBeenCalledWith('closeclick', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('content_changed', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('domready', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('position_changed', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('zindex_changed', jasmine.any(Function)); - }); + })); - it('should be able to add an event listener after init', () => { + it('should be able to add an event listener after init', fakeAsync(() => { const infoWindowSpy = createInfoWindowSpy({}); - createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough(); + createInfoWindowConstructorSpy(infoWindowSpy); const addSpy = infoWindowSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).not.toHaveBeenCalledWith('zindex_changed', jasmine.any(Function)); @@ -203,36 +207,38 @@ describe('MapInfoWindow', () => { expect(addSpy).toHaveBeenCalledWith('zindex_changed', jasmine.any(Function)); subscription.unsubscribe(); - }); + })); - it('should be able to open an info window without passing in an anchor', () => { + it('should be able to open an info window without passing in an anchor', fakeAsync(() => { const infoWindowSpy = createInfoWindowSpy({}); - createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough(); + createInfoWindowConstructorSpy(infoWindowSpy); const fixture = TestBed.createComponent(TestApp); const infoWindowComponent = fixture.debugElement .query(By.directive(MapInfoWindow))! .injector.get(MapInfoWindow); fixture.detectChanges(); + flush(); infoWindowComponent.open(); expect(infoWindowSpy.open).toHaveBeenCalledTimes(1); - }); + })); - it('should allow for the focus behavior to be changed when opening the info window', () => { + it('should allow for the focus behavior to be changed when opening the info window', fakeAsync(() => { const fakeMarker = {} as unknown as google.maps.Marker; const fakeMarkerComponent = { marker: fakeMarker, getAnchor: () => fakeMarker, } as unknown as MapMarker; const infoWindowSpy = createInfoWindowSpy({}); - createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough(); + createInfoWindowConstructorSpy(infoWindowSpy); const fixture = TestBed.createComponent(TestApp); const infoWindowComponent = fixture.debugElement .query(By.directive(MapInfoWindow))! .injector.get(MapInfoWindow); fixture.detectChanges(); + flush(); infoWindowComponent.open(fakeMarkerComponent, false); expect(infoWindowSpy.open).toHaveBeenCalledWith( @@ -240,7 +246,7 @@ describe('MapInfoWindow', () => { shouldFocus: false, }), ); - }); + })); }); @Component({ diff --git a/src/google-maps/map-info-window/map-info-window.ts b/src/google-maps/map-info-window/map-info-window.ts index bf82b0353de5..eb7c00019348 100644 --- a/src/google-maps/map-info-window/map-info-window.ts +++ b/src/google-maps/map-info-window/map-info-window.ts @@ -12,6 +12,7 @@ import { Directive, ElementRef, + EventEmitter, Input, NgZone, OnDestroy, @@ -100,6 +101,10 @@ export class MapInfoWindow implements OnInit, OnDestroy { @Output() readonly zindexChanged: Observable = this._eventManager.getLazyEmitter('zindex_changed'); + /** Event emitted when the info window is initialized. */ + @Output() readonly infoWindowInitialized: EventEmitter = + new EventEmitter(); + constructor( private readonly _googleMap: GoogleMap, private _elementRef: ElementRef, @@ -108,21 +113,23 @@ export class MapInfoWindow implements OnInit, OnDestroy { ngOnInit() { if (this._googleMap._isBrowser) { - const combinedOptionsChanges = this._combineOptions(); - - combinedOptionsChanges.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(() => { - this.infoWindow = new google.maps.InfoWindow(options); + 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 infoWindowConstructor = + google.maps.InfoWindow || + ((await google.maps.importLibrary('maps')) as google.maps.MapsLibrary).InfoWindow; + this.infoWindow = new infoWindowConstructor(options); + this._eventManager.setTarget(this.infoWindow); + this.infoWindowInitialized.emit(this.infoWindow); + this._watchForOptionsChanges(); + this._watchForPositionChanges(); + }); }); - - this._eventManager.setTarget(this.infoWindow); - }); - - this._watchForOptionsChanges(); - this._watchForPositionChanges(); } } @@ -229,12 +236,6 @@ export class MapInfoWindow implements OnInit, OnDestroy { private _assertInitialized(): asserts this is {infoWindow: google.maps.InfoWindow} { if (typeof ngDevMode === 'undefined' || ngDevMode) { - if (!this._googleMap.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.', - ); - } if (!this.infoWindow) { throw Error( 'Cannot interact with a Google Map Info Window before it has been ' + diff --git a/src/google-maps/map-kml-layer/map-kml-layer.spec.ts b/src/google-maps/map-kml-layer/map-kml-layer.spec.ts index 4ac4053ea770..35491897b0e5 100644 --- a/src/google-maps/map-kml-layer/map-kml-layer.spec.ts +++ b/src/google-maps/map-kml-layer/map-kml-layer.spec.ts @@ -1,5 +1,5 @@ import {Component, ViewChild} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; @@ -24,59 +24,63 @@ describe('MapKmlLayer', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Map Kml Layer', () => { + it('initializes a Google Map Kml Layer', fakeAsync(() => { const kmlLayerSpy = createKmlLayerSpy({}); - const kmlLayerConstructorSpy = createKmlLayerConstructorSpy(kmlLayerSpy).and.callThrough(); + const kmlLayerConstructorSpy = createKmlLayerConstructorSpy(kmlLayerSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(kmlLayerConstructorSpy).toHaveBeenCalledWith({url: undefined}); expect(kmlLayerSpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); - it('sets url from input', () => { + it('sets url from input', fakeAsync(() => { const options: google.maps.KmlLayerOptions = {url: DEMO_URL}; const kmlLayerSpy = createKmlLayerSpy(options); - const kmlLayerConstructorSpy = createKmlLayerConstructorSpy(kmlLayerSpy).and.callThrough(); + const kmlLayerConstructorSpy = createKmlLayerConstructorSpy(kmlLayerSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.url = DEMO_URL; fixture.detectChanges(); + flush(); expect(kmlLayerConstructorSpy).toHaveBeenCalledWith(options); - }); + })); - it('gives precedence to url input over options', () => { + it('gives precedence to url input over options', fakeAsync(() => { const expectedUrl = 'www.realurl.kml'; const expectedOptions: google.maps.KmlLayerOptions = {...DEFAULT_KML_OPTIONS, url: expectedUrl}; const kmlLayerSpy = createKmlLayerSpy(expectedOptions); - const kmlLayerConstructorSpy = createKmlLayerConstructorSpy(kmlLayerSpy).and.callThrough(); + const kmlLayerConstructorSpy = createKmlLayerConstructorSpy(kmlLayerSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = DEFAULT_KML_OPTIONS; fixture.componentInstance.url = expectedUrl; fixture.detectChanges(); + flush(); expect(kmlLayerConstructorSpy).toHaveBeenCalledWith(expectedOptions); - }); + })); - it('exposes methods that provide information about the KmlLayer', () => { + it('exposes methods that provide information about the KmlLayer', fakeAsync(() => { const kmlLayerSpy = createKmlLayerSpy(DEFAULT_KML_OPTIONS); - createKmlLayerConstructorSpy(kmlLayerSpy).and.callThrough(); + createKmlLayerConstructorSpy(kmlLayerSpy); const fixture = TestBed.createComponent(TestApp); const kmlLayerComponent = fixture.debugElement .query(By.directive(MapKmlLayer))! .injector.get(MapKmlLayer); fixture.detectChanges(); + flush(); kmlLayerComponent.getDefaultViewport(); expect(kmlLayerSpy.getDefaultViewport).toHaveBeenCalled(); @@ -103,28 +107,30 @@ describe('MapKmlLayer', () => { kmlLayerSpy.getZIndex.and.returnValue(3); expect(kmlLayerComponent.getZIndex()).toBe(3); - }); + })); - it('initializes KmlLayer event handlers', () => { + it('initializes KmlLayer event handlers', fakeAsync(() => { const kmlLayerSpy = createKmlLayerSpy(DEFAULT_KML_OPTIONS); - createKmlLayerConstructorSpy(kmlLayerSpy).and.callThrough(); + createKmlLayerConstructorSpy(kmlLayerSpy); const addSpy = kmlLayerSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).toHaveBeenCalledWith('click', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('defaultviewport_changed', jasmine.any(Function)); expect(addSpy).toHaveBeenCalledWith('status_changed', jasmine.any(Function)); - }); + })); - it('should be able to add an event listener after init', () => { + it('should be able to add an event listener after init', fakeAsync(() => { const kmlLayerSpy = createKmlLayerSpy(DEFAULT_KML_OPTIONS); - createKmlLayerConstructorSpy(kmlLayerSpy).and.callThrough(); + createKmlLayerConstructorSpy(kmlLayerSpy); const addSpy = kmlLayerSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).not.toHaveBeenCalledWith('defaultviewport_changed', jasmine.any(Function)); @@ -134,7 +140,7 @@ describe('MapKmlLayer', () => { expect(addSpy).toHaveBeenCalledWith('defaultviewport_changed', jasmine.any(Function)); subscription.unsubscribe(); - }); + })); }); @Component({ diff --git a/src/google-maps/map-kml-layer/map-kml-layer.ts b/src/google-maps/map-kml-layer/map-kml-layer.ts index d82926425e23..785293d9f58b 100644 --- a/src/google-maps/map-kml-layer/map-kml-layer.ts +++ b/src/google-maps/map-kml-layer/map-kml-layer.ts @@ -9,7 +9,16 @@ // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 /// -import {Directive, Input, NgZone, OnDestroy, OnInit, Output, inject} from '@angular/core'; +import { + Directive, + EventEmitter, + Input, + NgZone, + OnDestroy, + OnInit, + Output, + inject, +} from '@angular/core'; import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs'; import {map, take, takeUntil} from 'rxjs/operators'; @@ -70,6 +79,10 @@ export class MapKmlLayer implements OnInit, OnDestroy { @Output() readonly statusChanged: Observable = this._eventManager.getLazyEmitter('status_changed'); + /** Event emitted when the KML layer is initialized. */ + @Output() readonly kmlLayerInitialized: EventEmitter = + new EventEmitter(); + constructor( private readonly _map: GoogleMap, private _ngZone: NgZone, @@ -83,14 +96,20 @@ export class MapKmlLayer implements 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(() => (this.kmlLayer = new google.maps.KmlLayer(options))); - this._assertInitialized(); - this.kmlLayer.setMap(this._map.googleMap!); - this._eventManager.setTarget(this.kmlLayer); + this._ngZone.runOutsideAngular(async () => { + const map = await this._map._resolveMap(); + const layerConstructor = + google.maps.KmlLayer || + ((await google.maps.importLibrary('maps')) as google.maps.MapsLibrary).KmlLayer; + this.kmlLayer = new layerConstructor(options); + this._assertInitialized(); + this.kmlLayer.setMap(map); + this._eventManager.setTarget(this.kmlLayer); + this.kmlLayerInitialized.emit(this.kmlLayer); + this._watchForOptionsChanges(); + this._watchForUrlChanges(); + }); }); - - this._watchForOptionsChanges(); - this._watchForUrlChanges(); } } @@ -98,9 +117,7 @@ export class MapKmlLayer implements OnInit, OnDestroy { this._eventManager.destroy(); this._destroyed.next(); this._destroyed.complete(); - if (this.kmlLayer) { - this.kmlLayer.setMap(null); - } + this.kmlLayer?.setMap(null); } /** @@ -176,12 +193,6 @@ export class MapKmlLayer implements OnInit, OnDestroy { private _assertInitialized(): asserts this is {kmlLayer: google.maps.KmlLayer} { if (typeof ngDevMode === 'undefined' || ngDevMode) { - 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.', - ); - } if (!this.kmlLayer) { throw Error( 'Cannot interact with a Google Map KmlLayer before it has been ' + diff --git a/src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts b/src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts index 4cb61a53b10b..1274056c6a96 100644 --- a/src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts +++ b/src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts @@ -1,5 +1,5 @@ import {Component, ViewChild} from '@angular/core'; -import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; import {MapMarker} from '../map-marker/map-marker'; @@ -30,7 +30,7 @@ describe('MapMarkerClusterer', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); const markerSpy = createMarkerSpy({}); // The spy target function cannot be an arrow-function as this breaks when created @@ -40,8 +40,7 @@ describe('MapMarkerClusterer', () => { }); markerClustererSpy = createMarkerClustererSpy(); - markerClustererConstructorSpy = - createMarkerClustererConstructorSpy(markerClustererSpy).and.callThrough(); + markerClustererConstructorSpy = createMarkerClustererConstructorSpy(markerClustererSpy); fixture = TestBed.createComponent(TestApp); }); @@ -51,24 +50,19 @@ describe('MapMarkerClusterer', () => { (window as any).MarkerClusterer = undefined; }); - it('throws an error if the clustering library has not been loaded', () => { + it('throws an error if the clustering library has not been loaded', fakeAsync(() => { (window as any).MarkerClusterer = undefined; - markerClustererConstructorSpy = createMarkerClustererConstructorSpy( - markerClustererSpy, - false, - ).and.callThrough(); - - expect(() => fixture.detectChanges()).toThrow( - new Error( - 'MarkerClusterer class not found, cannot construct a marker cluster. ' + - 'Please install the MarkerClustererPlus library: ' + - 'https://github.com/googlemaps/js-markerclustererplus', - ), - ); - }); + markerClustererConstructorSpy = createMarkerClustererConstructorSpy(markerClustererSpy, false); + + expect(() => { + fixture.detectChanges(); + flush(); + }).toThrowError(/MarkerClusterer class not found, cannot construct a marker cluster/); + })); - it('initializes a Google Map Marker Clusterer', () => { + it('initializes a Google Map Marker Clusterer', fakeAsync(() => { fixture.detectChanges(); + flush(); expect(markerClustererConstructorSpy).toHaveBeenCalledWith(mapSpy, [], { ariaLabelFn: undefined, @@ -90,9 +84,9 @@ describe('MapMarkerClusterer', () => { zIndex: undefined, zoomOnClick: undefined, }); - }); + })); - it('sets marker clusterer inputs', () => { + it('sets marker clusterer inputs', fakeAsync(() => { fixture.componentInstance.ariaLabelFn = (testString: string) => testString; fixture.componentInstance.averageCenter = true; fixture.componentInstance.batchSize = 1; @@ -110,6 +104,7 @@ describe('MapMarkerClusterer', () => { fixture.componentInstance.zIndex = 6; fixture.componentInstance.zoomOnClick = true; fixture.detectChanges(); + flush(); expect(markerClustererConstructorSpy).toHaveBeenCalledWith(mapSpy, [], { ariaLabelFn: jasmine.any(Function), @@ -131,10 +126,11 @@ describe('MapMarkerClusterer', () => { zIndex: 6, zoomOnClick: true, }); - }); + })); - it('sets marker clusterer options', () => { + it('sets marker clusterer options', fakeAsync(() => { fixture.detectChanges(); + flush(); const options: MarkerClustererOptions = { enableRetinaIcons: true, gridSize: 1337, @@ -144,10 +140,11 @@ describe('MapMarkerClusterer', () => { fixture.componentInstance.options = options; fixture.detectChanges(); expect(markerClustererSpy.setOptions).toHaveBeenCalledWith(jasmine.objectContaining(options)); - }); + })); - it('gives precedence to specific inputs over options', () => { + it('gives precedence to specific inputs over options', fakeAsync(() => { fixture.detectChanges(); + flush(); const options: MarkerClustererOptions = { enableRetinaIcons: true, gridSize: 1337, @@ -170,19 +167,21 @@ describe('MapMarkerClusterer', () => { expect(markerClustererSpy.setOptions).toHaveBeenCalledWith( jasmine.objectContaining(expectedOptions), ); - }); + })); - it('sets Google Maps Markers in the MarkerClusterer', () => { + it('sets Google Maps Markers in the MarkerClusterer', fakeAsync(() => { fixture.detectChanges(); + flush(); expect(markerClustererSpy.addMarkers).toHaveBeenCalledWith([ anyMarkerMatcher, anyMarkerMatcher, ]); - }); + })); - it('updates Google Maps Markers in the Marker Clusterer', () => { + it('updates Google Maps Markers in the Marker Clusterer', fakeAsync(() => { fixture.detectChanges(); + flush(); expect(markerClustererSpy.addMarkers).toHaveBeenCalledWith([ anyMarkerMatcher, @@ -191,6 +190,7 @@ describe('MapMarkerClusterer', () => { fixture.componentInstance.state = 'state2'; fixture.detectChanges(); + flush(); expect(markerClustererSpy.addMarkers).toHaveBeenCalledWith([anyMarkerMatcher], true); expect(markerClustererSpy.removeMarkers).toHaveBeenCalledWith([anyMarkerMatcher], true); @@ -198,6 +198,7 @@ describe('MapMarkerClusterer', () => { fixture.componentInstance.state = 'state0'; fixture.detectChanges(); + flush(); expect(markerClustererSpy.addMarkers).toHaveBeenCalledWith([], true); expect(markerClustererSpy.removeMarkers).toHaveBeenCalledWith( @@ -205,10 +206,11 @@ describe('MapMarkerClusterer', () => { true, ); expect(markerClustererSpy.repaint).toHaveBeenCalledTimes(2); - }); + })); - it('exposes marker clusterer methods', () => { + it('exposes marker clusterer methods', fakeAsync(() => { fixture.detectChanges(); + flush(); const markerClustererComponent = fixture.componentInstance.markerClusterer; markerClustererComponent.fitMapToMarkers(5); @@ -275,10 +277,11 @@ describe('MapMarkerClusterer', () => { markerClustererSpy.getZoomOnClick.and.returnValue(true); expect(markerClustererComponent.getZoomOnClick()).toBe(true); - }); + })); - it('initializes marker clusterer event handlers', () => { + it('initializes marker clusterer event handlers', fakeAsync(() => { fixture.detectChanges(); + flush(); expect(markerClustererSpy.addListener).toHaveBeenCalledWith( 'clusteringbegin', @@ -289,7 +292,7 @@ describe('MapMarkerClusterer', () => { jasmine.any(Function), ); expect(markerClustererSpy.addListener).toHaveBeenCalledWith('click', jasmine.any(Function)); - }); + })); }); @Component({ diff --git a/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts b/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts index 911127cd3f49..9c3d1b20f7a4 100644 --- a/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts +++ b/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts @@ -14,6 +14,7 @@ import { ChangeDetectionStrategy, Component, ContentChildren, + EventEmitter, Input, NgZone, OnChanges, @@ -26,7 +27,7 @@ import { inject, } from '@angular/core'; import {Observable, Subject} from 'rxjs'; -import {takeUntil} from 'rxjs/operators'; +import {take, takeUntil} from 'rxjs/operators'; import {GoogleMap} from '../google-map/google-map'; import {MapEventManager} from '../map-event-manager'; @@ -207,45 +208,56 @@ export class MapMarkerClusterer implements OnInit, AfterContentInit, OnChanges, */ markerClusterer?: MarkerClustererInstance; + /** Event emitted when the clusterer is initialized. */ + @Output() readonly markerClustererInitialized: EventEmitter = + new EventEmitter(); + constructor( private readonly _googleMap: GoogleMap, private readonly _ngZone: NgZone, ) { - this._canInitialize = this._googleMap._isBrowser; + this._canInitialize = _googleMap._isBrowser; } ngOnInit() { if (this._canInitialize) { - if ( - typeof MarkerClusterer !== 'function' && - (typeof ngDevMode === 'undefined' || ngDevMode) - ) { - throw Error( - 'MarkerClusterer class not found, cannot construct a marker cluster. ' + - 'Please install the MarkerClustererPlus library: ' + - 'https://github.com/googlemaps/js-markerclustererplus', - ); - } + this._ngZone.runOutsideAngular(async () => { + const map = await this._googleMap._resolveMap(); + + if ( + typeof MarkerClusterer !== 'function' && + (typeof ngDevMode === 'undefined' || ngDevMode) + ) { + throw Error( + 'MarkerClusterer class not found, cannot construct a marker cluster. ' + + 'Please install the MarkerClustererPlus library: ' + + 'https://github.com/googlemaps/js-markerclustererplus', + ); + } - // 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.markerClusterer = new MarkerClusterer( - this._googleMap.googleMap!, - [], - this._combineOptions(), - ); - }); + // 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.markerClusterer = this._ngZone.runOutsideAngular(() => { + return new MarkerClusterer(map, [], this._combineOptions()); + }); - this._assertInitialized(); - this._eventManager.setTarget(this.markerClusterer); + this._assertInitialized(); + this._eventManager.setTarget(this.markerClusterer); + this.markerClustererInitialized.emit(this.markerClusterer); + }); } } ngAfterContentInit() { if (this._canInitialize) { - this._watchForMarkerChanges(); + if (this.markerClusterer) { + this._watchForMarkerChanges(); + } else { + this.markerClustererInitialized + .pipe(take(1), takeUntil(this._destroy)) + .subscribe(() => this._watchForMarkerChanges()); + } } } @@ -333,9 +345,7 @@ export class MapMarkerClusterer implements OnInit, AfterContentInit, OnChanges, this._destroy.next(); this._destroy.complete(); this._eventManager.destroy(); - if (this.markerClusterer) { - this.markerClusterer.setMap(null); - } + this.markerClusterer?.setMap(null); } fitMapToMarkers(padding: number | google.maps.Padding) { @@ -465,54 +475,56 @@ export class MapMarkerClusterer implements OnInit, AfterContentInit, OnChanges, private _watchForMarkerChanges() { this._assertInitialized(); - const initialMarkers: google.maps.Marker[] = []; - for (const marker of this._getInternalMarkers(this._markers.toArray())) { - this._currentMarkers.add(marker); - initialMarkers.push(marker); - } - this.markerClusterer.addMarkers(initialMarkers); + + this._ngZone.runOutsideAngular(async () => { + const initialMarkers: google.maps.Marker[] = []; + const markers = await this._getInternalMarkers(this._markers); + for (const marker of markers) { + this._currentMarkers.add(marker); + initialMarkers.push(marker); + } + this.markerClusterer.addMarkers(initialMarkers); + }); this._markers.changes .pipe(takeUntil(this._destroy)) .subscribe((markerComponents: MapMarker[]) => { this._assertInitialized(); - const newMarkers = new Set(this._getInternalMarkers(markerComponents)); - const markersToAdd: google.maps.Marker[] = []; - const markersToRemove: google.maps.Marker[] = []; - for (const marker of Array.from(newMarkers)) { - if (!this._currentMarkers.has(marker)) { - this._currentMarkers.add(marker); - markersToAdd.push(marker); + this._ngZone.runOutsideAngular(async () => { + const newMarkers = new Set( + await this._getInternalMarkers(markerComponents), + ); + const markersToAdd: google.maps.Marker[] = []; + const markersToRemove: google.maps.Marker[] = []; + for (const marker of Array.from(newMarkers)) { + if (!this._currentMarkers.has(marker)) { + this._currentMarkers.add(marker); + markersToAdd.push(marker); + } } - } - for (const marker of Array.from(this._currentMarkers)) { - if (!newMarkers.has(marker)) { - markersToRemove.push(marker); + for (const marker of Array.from(this._currentMarkers)) { + if (!newMarkers.has(marker)) { + markersToRemove.push(marker); + } } - } - this.markerClusterer.addMarkers(markersToAdd, true); - this.markerClusterer.removeMarkers(markersToRemove, true); - this.markerClusterer.repaint(); - for (const marker of markersToRemove) { - this._currentMarkers.delete(marker); - } + this.markerClusterer.addMarkers(markersToAdd, true); + this.markerClusterer.removeMarkers(markersToRemove, true); + this.markerClusterer.repaint(); + for (const marker of markersToRemove) { + this._currentMarkers.delete(marker); + } + }); }); } - private _getInternalMarkers(markers: MapMarker[]): google.maps.Marker[] { - return markers - .filter(markerComponent => !!markerComponent.marker) - .map(markerComponent => markerComponent.marker!); + private _getInternalMarkers( + markers: MapMarker[] | QueryList, + ): Promise { + return Promise.all(markers.map(markerComponent => markerComponent._resolveMarker())); } private _assertInitialized(): asserts this is {markerClusterer: MarkerClustererInstance} { if (typeof ngDevMode === 'undefined' || ngDevMode) { - if (!this._googleMap.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.', - ); - } if (!this.markerClusterer) { throw Error( 'Cannot interact with a MarkerClusterer before it has been initialized. ' + diff --git a/src/google-maps/map-marker/map-marker.spec.ts b/src/google-maps/map-marker/map-marker.spec.ts index c7d0ea79e560..3f0e102e10ea 100644 --- a/src/google-maps/map-marker/map-marker.spec.ts +++ b/src/google-maps/map-marker/map-marker.spec.ts @@ -1,5 +1,5 @@ import {Component, ViewChild} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; import { @@ -15,19 +15,20 @@ describe('MapMarker', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Map marker', () => { + it('initializes a Google Map marker', fakeAsync(() => { const markerSpy = createMarkerSpy(DEFAULT_MARKER_OPTIONS); - const markerConstructorSpy = createMarkerConstructorSpy(markerSpy).and.callThrough(); + const markerConstructorSpy = createMarkerConstructorSpy(markerSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(markerConstructorSpy).toHaveBeenCalledWith({ ...DEFAULT_MARKER_OPTIONS, @@ -38,9 +39,9 @@ describe('MapMarker', () => { visible: undefined, map: mapSpy, }); - }); + })); - it('sets marker inputs', () => { + it('sets marker inputs', fakeAsync(() => { const options: google.maps.MarkerOptions = { position: {lat: 3, lng: 5}, title: 'marker title', @@ -51,7 +52,7 @@ describe('MapMarker', () => { map: mapSpy, }; const markerSpy = createMarkerSpy(options); - const markerConstructorSpy = createMarkerConstructorSpy(markerSpy).and.callThrough(); + const markerConstructorSpy = createMarkerConstructorSpy(markerSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.position = options.position; @@ -61,11 +62,12 @@ describe('MapMarker', () => { fixture.componentInstance.icon = 'icon.png'; fixture.componentInstance.visible = false; fixture.detectChanges(); + flush(); expect(markerConstructorSpy).toHaveBeenCalledWith(options); - }); + })); - it('sets marker options, ignoring map', () => { + it('sets marker options, ignoring map', fakeAsync(() => { const options: google.maps.MarkerOptions = { position: {lat: 3, lng: 5}, title: 'marker title', @@ -75,16 +77,17 @@ describe('MapMarker', () => { visible: undefined, }; const markerSpy = createMarkerSpy(options); - const markerConstructorSpy = createMarkerConstructorSpy(markerSpy).and.callThrough(); + const markerConstructorSpy = createMarkerConstructorSpy(markerSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = options; fixture.detectChanges(); + flush(); expect(markerConstructorSpy).toHaveBeenCalledWith({...options, map: mapSpy}); - }); + })); - it('gives precedence to specific inputs over options', () => { + it('gives precedence to specific inputs over options', fakeAsync(() => { const options: google.maps.MarkerOptions = { position: {lat: 3, lng: 5}, title: 'marker title', @@ -102,7 +105,7 @@ describe('MapMarker', () => { visible: undefined, }; const markerSpy = createMarkerSpy(options); - const markerConstructorSpy = createMarkerConstructorSpy(markerSpy).and.callThrough(); + const markerConstructorSpy = createMarkerConstructorSpy(markerSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.position = expectedOptions.position; @@ -111,16 +114,18 @@ describe('MapMarker', () => { fixture.componentInstance.clickable = expectedOptions.clickable; fixture.componentInstance.options = options; fixture.detectChanges(); + flush(); expect(markerConstructorSpy).toHaveBeenCalledWith(expectedOptions); - }); + })); - it('exposes methods that provide information about the marker', () => { + it('exposes methods that provide information about the marker', fakeAsync(() => { const markerSpy = createMarkerSpy(DEFAULT_MARKER_OPTIONS); - createMarkerConstructorSpy(markerSpy).and.callThrough(); + createMarkerConstructorSpy(markerSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); const marker = fixture.componentInstance.marker; markerSpy.getAnimation.and.returnValue(null); @@ -158,15 +163,16 @@ describe('MapMarker', () => { markerSpy.getZIndex.and.returnValue(2); expect(marker.getZIndex()).toBe(2); - }); + })); - it('initializes marker event handlers', () => { + it('initializes marker event handlers', fakeAsync(() => { const markerSpy = createMarkerSpy(DEFAULT_MARKER_OPTIONS); - createMarkerConstructorSpy(markerSpy).and.callThrough(); + createMarkerConstructorSpy(markerSpy); const addSpy = markerSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).toHaveBeenCalledWith('click', jasmine.any(Function)); expect(addSpy).toHaveBeenCalledWith('position_changed', jasmine.any(Function)); @@ -189,15 +195,16 @@ describe('MapMarker', () => { expect(addSpy).not.toHaveBeenCalledWith('title_changed', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('visible_changed', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('zindex_changed', jasmine.any(Function)); - }); + })); - it('should be able to add an event listener after init', () => { + it('should be able to add an event listener after init', fakeAsync(() => { const markerSpy = createMarkerSpy(DEFAULT_MARKER_OPTIONS); - createMarkerConstructorSpy(markerSpy).and.callThrough(); + createMarkerConstructorSpy(markerSpy); const addSpy = markerSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).not.toHaveBeenCalledWith('flat_changed', jasmine.any(Function)); @@ -207,7 +214,7 @@ describe('MapMarker', () => { expect(addSpy).toHaveBeenCalledWith('flat_changed', jasmine.any(Function)); subscription.unsubscribe(); - }); + })); }); @Component({ diff --git a/src/google-maps/map-marker/map-marker.ts b/src/google-maps/map-marker/map-marker.ts index 0785779c9e7c..347f5b18d6af 100644 --- a/src/google-maps/map-marker/map-marker.ts +++ b/src/google-maps/map-marker/map-marker.ts @@ -19,8 +19,10 @@ import { OnChanges, SimpleChanges, inject, + EventEmitter, } from '@angular/core'; import {Observable} from 'rxjs'; +import {take} from 'rxjs/operators'; import {GoogleMap} from '../google-map/google-map'; import {MapEventManager} from '../map-event-manager'; @@ -264,6 +266,10 @@ export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { @Output() readonly zindexChanged: Observable = this._eventManager.getLazyEmitter('zindex_changed'); + /** Event emitted when the marker is initialized. */ + @Output() readonly markerInitialized: EventEmitter = + new EventEmitter(); + /** * The underlying google.maps.Marker object. * @@ -277,17 +283,25 @@ export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { ) {} 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(() => { - this.marker = new google.maps.Marker(this._combineOptions()); - }); + if (!this._googleMap._isBrowser) { + 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._googleMap._resolveMap(); + const markerConstructor = + google.maps.Marker || + ((await google.maps.importLibrary('marker')) as google.maps.MarkerLibrary).Marker; + + this.marker = new markerConstructor(this._combineOptions()); this._assertInitialized(); - this.marker.setMap(this._googleMap.googleMap!); + this.marker.setMap(map); this._eventManager.setTarget(this.marker); - } + this.markerInitialized.next(this.marker); + }); } ngOnChanges(changes: SimpleChanges) { @@ -325,10 +339,9 @@ export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { } ngOnDestroy() { + this.markerInitialized.complete(); this._eventManager.destroy(); - if (this.marker) { - this.marker.setMap(null); - } + this.marker?.setMap(null); } /** @@ -445,6 +458,11 @@ export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { return this.marker; } + /** Returns a promise that resolves when the marker has been initialized. */ + async _resolveMarker(): Promise { + return this.marker || this.markerInitialized.pipe(take(1)).toPromise(); + } + /** Creates a combined options object using the passed-in options and the individual inputs. */ private _combineOptions(): google.maps.MarkerOptions { const options = this._options || DEFAULT_MARKER_OPTIONS; @@ -462,12 +480,6 @@ export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { private _assertInitialized(): asserts this is {marker: google.maps.Marker} { if (typeof ngDevMode === 'undefined' || ngDevMode) { - if (!this._googleMap.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.', - ); - } if (!this.marker) { throw Error( 'Cannot interact with a Google Map Marker before it has been ' + diff --git a/src/google-maps/map-polygon/map-polygon.spec.ts b/src/google-maps/map-polygon/map-polygon.spec.ts index b55b2db04449..09d4ec277f29 100644 --- a/src/google-maps/map-polygon/map-polygon.spec.ts +++ b/src/google-maps/map-polygon/map-polygon.spec.ts @@ -1,5 +1,5 @@ import {Component, ViewChild} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; @@ -28,60 +28,64 @@ describe('MapPolygon', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Map Polygon', () => { + it('initializes a Google Map Polygon', fakeAsync(() => { const polygonSpy = createPolygonSpy({}); - const polygonConstructorSpy = createPolygonConstructorSpy(polygonSpy).and.callThrough(); + const polygonConstructorSpy = createPolygonConstructorSpy(polygonSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(polygonConstructorSpy).toHaveBeenCalledWith({paths: undefined}); expect(polygonSpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); - it('sets path from input', () => { + it('sets path from input', fakeAsync(() => { const paths: google.maps.LatLngLiteral[] = [{lat: 3, lng: 5}]; const options: google.maps.PolygonOptions = {paths}; const polygonSpy = createPolygonSpy(options); - const polygonConstructorSpy = createPolygonConstructorSpy(polygonSpy).and.callThrough(); + const polygonConstructorSpy = createPolygonConstructorSpy(polygonSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.paths = paths; fixture.detectChanges(); + flush(); expect(polygonConstructorSpy).toHaveBeenCalledWith(options); - }); + })); - it('gives precedence to path input over options', () => { + it('gives precedence to path input over options', fakeAsync(() => { const paths: google.maps.LatLngLiteral[] = [{lat: 3, lng: 5}]; const expectedOptions: google.maps.PolygonOptions = {...polygonOptions, paths}; const polygonSpy = createPolygonSpy(expectedOptions); - const polygonConstructorSpy = createPolygonConstructorSpy(polygonSpy).and.callThrough(); + const polygonConstructorSpy = createPolygonConstructorSpy(polygonSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = polygonOptions; fixture.componentInstance.paths = paths; fixture.detectChanges(); + flush(); expect(polygonConstructorSpy).toHaveBeenCalledWith(expectedOptions); - }); + })); - it('exposes methods that provide information about the Polygon', () => { + it('exposes methods that provide information about the Polygon', fakeAsync(() => { const polygonSpy = createPolygonSpy(polygonOptions); - createPolygonConstructorSpy(polygonSpy).and.callThrough(); + createPolygonConstructorSpy(polygonSpy); const fixture = TestBed.createComponent(TestApp); const polygonComponent = fixture.debugElement .query(By.directive(MapPolygon))! .injector.get(MapPolygon); fixture.detectChanges(); + flush(); polygonSpy.getDraggable.and.returnValue(true); expect(polygonComponent.getDraggable()).toBe(true); @@ -97,15 +101,16 @@ describe('MapPolygon', () => { polygonSpy.getVisible.and.returnValue(true); expect(polygonComponent.getVisible()).toBe(true); - }); + })); - it('initializes Polygon event handlers', () => { + it('initializes Polygon event handlers', fakeAsync(() => { const polygonSpy = createPolygonSpy(polygonOptions); - createPolygonConstructorSpy(polygonSpy).and.callThrough(); + createPolygonConstructorSpy(polygonSpy); const addSpy = polygonSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).toHaveBeenCalledWith('click', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('dblclick', jasmine.any(Function)); @@ -118,15 +123,16 @@ describe('MapPolygon', () => { expect(addSpy).not.toHaveBeenCalledWith('mouseover', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('mouseup', jasmine.any(Function)); expect(addSpy).toHaveBeenCalledWith('rightclick', jasmine.any(Function)); - }); + })); - it('should be able to add an event listener after init', () => { + it('should be able to add an event listener after init', fakeAsync(() => { const polygonSpy = createPolygonSpy(polygonOptions); - createPolygonConstructorSpy(polygonSpy).and.callThrough(); + createPolygonConstructorSpy(polygonSpy); const addSpy = polygonSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).not.toHaveBeenCalledWith('dragend', jasmine.any(Function)); @@ -136,7 +142,7 @@ describe('MapPolygon', () => { expect(addSpy).toHaveBeenCalledWith('dragend', jasmine.any(Function)); subscription.unsubscribe(); - }); + })); }); @Component({ diff --git a/src/google-maps/map-polygon/map-polygon.ts b/src/google-maps/map-polygon/map-polygon.ts index fca45ff94671..4e7be956760d 100644 --- a/src/google-maps/map-polygon/map-polygon.ts +++ b/src/google-maps/map-polygon/map-polygon.ts @@ -9,7 +9,16 @@ // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 /// -import {Directive, Input, OnDestroy, OnInit, Output, NgZone, inject} from '@angular/core'; +import { + Directive, + Input, + OnDestroy, + OnInit, + Output, + NgZone, + inject, + EventEmitter, +} from '@angular/core'; import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs'; import {map, take, takeUntil} from 'rxjs/operators'; @@ -128,6 +137,10 @@ export class MapPolygon implements OnInit, OnDestroy { @Output() readonly polygonRightclick: Observable = this._eventManager.getLazyEmitter('rightclick'); + /** Event emitted when the polygon is initialized. */ + @Output() readonly polygonInitialized: EventEmitter = + new EventEmitter(); + constructor( private readonly _map: GoogleMap, private readonly _ngZone: NgZone, @@ -141,16 +154,20 @@ export class MapPolygon implements 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(() => { - this.polygon = new google.maps.Polygon(options); + this._ngZone.runOutsideAngular(async () => { + const map = await this._map._resolveMap(); + const polygonConstructor = + google.maps.Polygon || + ((await google.maps.importLibrary('maps')) as google.maps.MapsLibrary).Polygon; + this.polygon = new polygonConstructor(options); + this._assertInitialized(); + this.polygon.setMap(map); + this._eventManager.setTarget(this.polygon); + this.polygonInitialized.emit(this.polygon); + this._watchForOptionsChanges(); + this._watchForPathChanges(); }); - this._assertInitialized(); - this.polygon.setMap(this._map.googleMap!); - this._eventManager.setTarget(this.polygon); }); - - this._watchForOptionsChanges(); - this._watchForPathChanges(); } } @@ -158,9 +175,7 @@ export class MapPolygon implements OnInit, OnDestroy { this._eventManager.destroy(); this._destroyed.next(); this._destroyed.complete(); - if (this.polygon) { - this.polygon.setMap(null); - } + this.polygon?.setMap(null); } /** @@ -234,12 +249,6 @@ export class MapPolygon implements OnInit, OnDestroy { private _assertInitialized(): asserts this is {polygon: google.maps.Polygon} { if (typeof ngDevMode === 'undefined' || ngDevMode) { - 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.', - ); - } if (!this.polygon) { throw Error( 'Cannot interact with a Google Map Polygon before it has been ' + diff --git a/src/google-maps/map-polyline/map-polyline.spec.ts b/src/google-maps/map-polyline/map-polyline.spec.ts index b8078e6bd88a..095be84dd9f1 100644 --- a/src/google-maps/map-polyline/map-polyline.spec.ts +++ b/src/google-maps/map-polyline/map-polyline.spec.ts @@ -1,5 +1,5 @@ import {Component, ViewChild} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; @@ -32,60 +32,64 @@ describe('MapPolyline', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Map Polyline', () => { + it('initializes a Google Map Polyline', fakeAsync(() => { const polylineSpy = createPolylineSpy({}); - const polylineConstructorSpy = createPolylineConstructorSpy(polylineSpy).and.callThrough(); + const polylineConstructorSpy = createPolylineConstructorSpy(polylineSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(polylineConstructorSpy).toHaveBeenCalledWith({path: undefined}); expect(polylineSpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); - it('sets path from input', () => { + it('sets path from input', fakeAsync(() => { const path: google.maps.LatLngLiteral[] = [{lat: 3, lng: 5}]; const options: google.maps.PolylineOptions = {path}; const polylineSpy = createPolylineSpy(options); - const polylineConstructorSpy = createPolylineConstructorSpy(polylineSpy).and.callThrough(); + const polylineConstructorSpy = createPolylineConstructorSpy(polylineSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.path = path; fixture.detectChanges(); + flush(); expect(polylineConstructorSpy).toHaveBeenCalledWith(options); - }); + })); - it('gives precedence to path input over options', () => { + it('gives precedence to path input over options', fakeAsync(() => { const path: google.maps.LatLngLiteral[] = [{lat: 3, lng: 5}]; const expectedOptions: google.maps.PolylineOptions = {...polylineOptions, path}; const polylineSpy = createPolylineSpy(expectedOptions); - const polylineConstructorSpy = createPolylineConstructorSpy(polylineSpy).and.callThrough(); + const polylineConstructorSpy = createPolylineConstructorSpy(polylineSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = polylineOptions; fixture.componentInstance.path = path; fixture.detectChanges(); + flush(); expect(polylineConstructorSpy).toHaveBeenCalledWith(expectedOptions); - }); + })); - it('exposes methods that provide information about the Polyline', () => { + it('exposes methods that provide information about the Polyline', fakeAsync(() => { const polylineSpy = createPolylineSpy(polylineOptions); - createPolylineConstructorSpy(polylineSpy).and.callThrough(); + createPolylineConstructorSpy(polylineSpy); const fixture = TestBed.createComponent(TestApp); const polylineComponent = fixture.debugElement .query(By.directive(MapPolyline))! .injector.get(MapPolyline); fixture.detectChanges(); + flush(); polylineSpy.getDraggable.and.returnValue(true); expect(polylineComponent.getDraggable()).toBe(true); @@ -98,15 +102,16 @@ describe('MapPolyline', () => { polylineSpy.getVisible.and.returnValue(true); expect(polylineComponent.getVisible()).toBe(true); - }); + })); - it('initializes Polyline event handlers', () => { + it('initializes Polyline event handlers', fakeAsync(() => { const polylineSpy = createPolylineSpy(polylineOptions); - createPolylineConstructorSpy(polylineSpy).and.callThrough(); + createPolylineConstructorSpy(polylineSpy); const addSpy = polylineSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).toHaveBeenCalledWith('click', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('dblclick', jasmine.any(Function)); @@ -119,15 +124,16 @@ describe('MapPolyline', () => { expect(addSpy).not.toHaveBeenCalledWith('mouseover', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('mouseup', jasmine.any(Function)); expect(addSpy).toHaveBeenCalledWith('rightclick', jasmine.any(Function)); - }); + })); - it('should be able to add an event listener after init', () => { + it('should be able to add an event listener after init', fakeAsync(() => { const polylineSpy = createPolylineSpy(polylineOptions); - createPolylineConstructorSpy(polylineSpy).and.callThrough(); + createPolylineConstructorSpy(polylineSpy); const addSpy = polylineSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).not.toHaveBeenCalledWith('dragend', jasmine.any(Function)); @@ -137,7 +143,7 @@ describe('MapPolyline', () => { expect(addSpy).toHaveBeenCalledWith('dragend', jasmine.any(Function)); subscription.unsubscribe(); - }); + })); }); @Component({ diff --git a/src/google-maps/map-polyline/map-polyline.ts b/src/google-maps/map-polyline/map-polyline.ts index 19be67a89c72..efa168a1dc21 100644 --- a/src/google-maps/map-polyline/map-polyline.ts +++ b/src/google-maps/map-polyline/map-polyline.ts @@ -9,7 +9,16 @@ // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 /// -import {Directive, Input, OnDestroy, OnInit, Output, NgZone, inject} from '@angular/core'; +import { + Directive, + Input, + OnDestroy, + OnInit, + Output, + NgZone, + inject, + EventEmitter, +} from '@angular/core'; import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs'; import {map, take, takeUntil} from 'rxjs/operators'; @@ -126,6 +135,10 @@ export class MapPolyline implements OnInit, OnDestroy { @Output() readonly polylineRightclick: Observable = this._eventManager.getLazyEmitter('rightclick'); + /** Event emitted when the polyline is initialized. */ + @Output() readonly polylineInitialized: EventEmitter = + new EventEmitter(); + constructor( private readonly _map: GoogleMap, private _ngZone: NgZone, @@ -139,14 +152,20 @@ export class MapPolyline implements 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(() => (this.polyline = new google.maps.Polyline(options))); - this._assertInitialized(); - this.polyline.setMap(this._map.googleMap!); - this._eventManager.setTarget(this.polyline); + this._ngZone.runOutsideAngular(async () => { + const map = await this._map._resolveMap(); + const polylineConstructor = + google.maps.Polyline || + ((await google.maps.importLibrary('maps')) as google.maps.MapsLibrary).Polyline; + this.polyline = new polylineConstructor(options); + this._assertInitialized(); + this.polyline.setMap(map); + this._eventManager.setTarget(this.polyline); + this.polylineInitialized.emit(this.polyline); + this._watchForOptionsChanges(); + this._watchForPathChanges(); + }); }); - - this._watchForOptionsChanges(); - this._watchForPathChanges(); } } @@ -154,9 +173,7 @@ export class MapPolyline implements OnInit, OnDestroy { this._eventManager.destroy(); this._destroyed.next(); this._destroyed.complete(); - if (this.polyline) { - this.polyline.setMap(null); - } + this.polyline?.setMap(null); } /** @@ -222,12 +239,6 @@ export class MapPolyline implements OnInit, OnDestroy { private _assertInitialized(): asserts this is {polyline: google.maps.Polyline} { if (typeof ngDevMode === 'undefined' || ngDevMode) { - 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.', - ); - } if (!this.polyline) { throw Error( 'Cannot interact with a Google Map Polyline before it has been ' + diff --git a/src/google-maps/map-rectangle/map-rectangle.spec.ts b/src/google-maps/map-rectangle/map-rectangle.spec.ts index 1d1bb44014b7..3ad2bf32a6fe 100644 --- a/src/google-maps/map-rectangle/map-rectangle.spec.ts +++ b/src/google-maps/map-rectangle/map-rectangle.spec.ts @@ -1,5 +1,5 @@ import {Component, ViewChild} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; @@ -24,60 +24,64 @@ describe('MapRectangle', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Map Rectangle', () => { + it('initializes a Google Map Rectangle', fakeAsync(() => { const rectangleSpy = createRectangleSpy({}); - const rectangleConstructorSpy = createRectangleConstructorSpy(rectangleSpy).and.callThrough(); + const rectangleConstructorSpy = createRectangleConstructorSpy(rectangleSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(rectangleConstructorSpy).toHaveBeenCalledWith({bounds: undefined}); expect(rectangleSpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); - it('sets bounds from input', () => { + it('sets bounds from input', fakeAsync(() => { const bounds: google.maps.LatLngBoundsLiteral = {east: 3, north: 5, west: -3, south: -5}; const options: google.maps.RectangleOptions = {bounds}; const rectangleSpy = createRectangleSpy(options); - const rectangleConstructorSpy = createRectangleConstructorSpy(rectangleSpy).and.callThrough(); + const rectangleConstructorSpy = createRectangleConstructorSpy(rectangleSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.bounds = bounds; fixture.detectChanges(); + flush(); expect(rectangleConstructorSpy).toHaveBeenCalledWith(options); - }); + })); - it('gives precedence to bounds input over options', () => { + it('gives precedence to bounds input over options', fakeAsync(() => { const bounds: google.maps.LatLngBoundsLiteral = {east: 3, north: 5, west: -3, south: -5}; const expectedOptions: google.maps.RectangleOptions = {...rectangleOptions, bounds}; const rectangleSpy = createRectangleSpy(expectedOptions); - const rectangleConstructorSpy = createRectangleConstructorSpy(rectangleSpy).and.callThrough(); + const rectangleConstructorSpy = createRectangleConstructorSpy(rectangleSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.options = rectangleOptions; fixture.componentInstance.bounds = bounds; fixture.detectChanges(); + flush(); expect(rectangleConstructorSpy).toHaveBeenCalledWith(expectedOptions); - }); + })); - it('exposes methods that provide information about the Rectangle', () => { + it('exposes methods that provide information about the Rectangle', fakeAsync(() => { const rectangleSpy = createRectangleSpy(rectangleOptions); - createRectangleConstructorSpy(rectangleSpy).and.callThrough(); + createRectangleConstructorSpy(rectangleSpy); const fixture = TestBed.createComponent(TestApp); const rectangleComponent = fixture.debugElement .query(By.directive(MapRectangle))! .injector.get(MapRectangle); fixture.detectChanges(); + flush(); rectangleComponent.getBounds(); expect(rectangleSpy.getBounds).toHaveBeenCalled(); @@ -90,15 +94,16 @@ describe('MapRectangle', () => { rectangleSpy.getVisible.and.returnValue(true); expect(rectangleComponent.getVisible()).toBe(true); - }); + })); - it('initializes Rectangle event handlers', () => { + it('initializes Rectangle event handlers', fakeAsync(() => { const rectangleSpy = createRectangleSpy(rectangleOptions); - createRectangleConstructorSpy(rectangleSpy).and.callThrough(); + createRectangleConstructorSpy(rectangleSpy); const addSpy = rectangleSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).toHaveBeenCalledWith('bounds_changed', jasmine.any(Function)); expect(addSpy).toHaveBeenCalledWith('click', jasmine.any(Function)); @@ -112,15 +117,16 @@ describe('MapRectangle', () => { expect(addSpy).not.toHaveBeenCalledWith('mouseover', jasmine.any(Function)); expect(addSpy).not.toHaveBeenCalledWith('mouseup', jasmine.any(Function)); expect(addSpy).toHaveBeenCalledWith('rightclick', jasmine.any(Function)); - }); + })); - it('should be able to add an event listener after init', () => { + it('should be able to add an event listener after init', fakeAsync(() => { const rectangleSpy = createRectangleSpy(rectangleOptions); - createRectangleConstructorSpy(rectangleSpy).and.callThrough(); + createRectangleConstructorSpy(rectangleSpy); const addSpy = rectangleSpy.addListener; const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(addSpy).not.toHaveBeenCalledWith('dragend', jasmine.any(Function)); @@ -130,7 +136,7 @@ describe('MapRectangle', () => { expect(addSpy).toHaveBeenCalledWith('dragend', jasmine.any(Function)); subscription.unsubscribe(); - }); + })); }); @Component({ diff --git a/src/google-maps/map-rectangle/map-rectangle.ts b/src/google-maps/map-rectangle/map-rectangle.ts index 20ae89dea92c..6adff31cec33 100644 --- a/src/google-maps/map-rectangle/map-rectangle.ts +++ b/src/google-maps/map-rectangle/map-rectangle.ts @@ -9,7 +9,16 @@ // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 /// -import {Directive, Input, OnDestroy, OnInit, Output, NgZone, inject} from '@angular/core'; +import { + Directive, + Input, + OnDestroy, + OnInit, + Output, + NgZone, + inject, + EventEmitter, +} from '@angular/core'; import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs'; import {map, take, takeUntil} from 'rxjs/operators'; @@ -135,6 +144,10 @@ export class MapRectangle implements OnInit, OnDestroy { @Output() readonly rectangleRightclick: Observable = this._eventManager.getLazyEmitter('rightclick'); + /** Event emitted when the rectangle is initialized. */ + @Output() readonly rectangleInitialized: EventEmitter = + new EventEmitter(); + constructor( private readonly _map: GoogleMap, private readonly _ngZone: NgZone, @@ -148,16 +161,20 @@ export class MapRectangle implements 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(() => { - this.rectangle = new google.maps.Rectangle(options); + this._ngZone.runOutsideAngular(async () => { + const map = await this._map._resolveMap(); + const rectangleConstructor = + google.maps.Rectangle || + ((await google.maps.importLibrary('maps')) as google.maps.MapsLibrary).Rectangle; + this.rectangle = new rectangleConstructor(options); + this._assertInitialized(); + this.rectangle.setMap(map); + this._eventManager.setTarget(this.rectangle); + this.rectangleInitialized.emit(this.rectangle); + this._watchForOptionsChanges(); + this._watchForBoundsChanges(); }); - this._assertInitialized(); - this.rectangle.setMap(this._map.googleMap!); - this._eventManager.setTarget(this.rectangle); }); - - this._watchForOptionsChanges(); - this._watchForBoundsChanges(); } } @@ -165,9 +182,7 @@ export class MapRectangle implements OnInit, OnDestroy { this._eventManager.destroy(); this._destroyed.next(); this._destroyed.complete(); - if (this.rectangle) { - this.rectangle.setMap(null); - } + this.rectangle?.setMap(null); } /** @@ -236,12 +251,6 @@ export class MapRectangle implements OnInit, OnDestroy { private _assertInitialized(): asserts this is {rectangle: google.maps.Rectangle} { if (typeof ngDevMode === 'undefined' || ngDevMode) { - 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.', - ); - } if (!this.rectangle) { throw Error( 'Cannot interact with a Google Map Rectangle before it has been initialized. ' + diff --git a/src/google-maps/map-traffic-layer/map-traffic-layer.spec.ts b/src/google-maps/map-traffic-layer/map-traffic-layer.spec.ts index cf97d24859ec..a11bc123e3d6 100644 --- a/src/google-maps/map-traffic-layer/map-traffic-layer.spec.ts +++ b/src/google-maps/map-traffic-layer/map-traffic-layer.spec.ts @@ -1,5 +1,5 @@ import {Component} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; import { @@ -17,25 +17,24 @@ describe('MapTrafficLayer', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Map Traffic Layer', () => { + it('initializes a Google Map Traffic Layer', fakeAsync(() => { const trafficLayerSpy = createTrafficLayerSpy(trafficLayerOptions); - const trafficLayerConstructorSpy = - createTrafficLayerConstructorSpy(trafficLayerSpy).and.callThrough(); - + const trafficLayerConstructorSpy = createTrafficLayerConstructorSpy(trafficLayerSpy); const fixture = TestBed.createComponent(TestApp); fixture.componentInstance.autoRefresh = false; fixture.detectChanges(); + flush(); expect(trafficLayerConstructorSpy).toHaveBeenCalledWith(trafficLayerOptions); expect(trafficLayerSpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); }); @Component({ diff --git a/src/google-maps/map-traffic-layer/map-traffic-layer.ts b/src/google-maps/map-traffic-layer/map-traffic-layer.ts index 853d9eadca24..e70818e83831 100644 --- a/src/google-maps/map-traffic-layer/map-traffic-layer.ts +++ b/src/google-maps/map-traffic-layer/map-traffic-layer.ts @@ -9,7 +9,7 @@ // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 /// -import {Directive, Input, NgZone, OnDestroy, OnInit} from '@angular/core'; +import {Directive, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output} from '@angular/core'; import {BehaviorSubject, Observable, Subject} from 'rxjs'; import {map, take, takeUntil} from 'rxjs/operators'; @@ -44,6 +44,10 @@ export class MapTrafficLayer implements OnInit, OnDestroy { this._autoRefresh.next(autoRefresh); } + /** Event emitted when the traffic layer is initialized. */ + @Output() readonly trafficLayerInitialized: EventEmitter = + new EventEmitter(); + constructor( private readonly _map: GoogleMap, private readonly _ngZone: NgZone, @@ -54,24 +58,25 @@ export class MapTrafficLayer implements OnInit, OnDestroy { this._combineOptions() .pipe(take(1)) .subscribe(options => { - // Create the object outside the zone so its events don't trigger change detection. - this._ngZone.runOutsideAngular(() => { - this.trafficLayer = new google.maps.TrafficLayer(options); + this._ngZone.runOutsideAngular(async () => { + const map = await this._map._resolveMap(); + const layerConstructor = + google.maps.TrafficLayer || + ((await google.maps.importLibrary('maps')) as google.maps.MapsLibrary).TrafficLayer; + this.trafficLayer = new layerConstructor(options); + this._assertInitialized(); + this.trafficLayer.setMap(map); + this.trafficLayerInitialized.emit(this.trafficLayer); + this._watchForAutoRefreshChanges(); }); - this._assertInitialized(); - this.trafficLayer.setMap(this._map.googleMap!); }); - - this._watchForAutoRefreshChanges(); } } ngOnDestroy() { this._destroyed.next(); this._destroyed.complete(); - if (this.trafficLayer) { - this.trafficLayer.setMap(null); - } + this.trafficLayer?.setMap(null); } private _combineOptions(): Observable { @@ -93,12 +98,6 @@ export class MapTrafficLayer implements OnInit, OnDestroy { } private _assertInitialized(): asserts this is {trafficLayer: google.maps.TrafficLayer} { - 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.', - ); - } if (!this.trafficLayer) { throw Error( 'Cannot interact with a Google Map Traffic Layer before it has been initialized. ' + diff --git a/src/google-maps/map-transit-layer/map-transit-layer.spec.ts b/src/google-maps/map-transit-layer/map-transit-layer.spec.ts index 395f1ef2ac4f..708821aa5b7d 100644 --- a/src/google-maps/map-transit-layer/map-transit-layer.spec.ts +++ b/src/google-maps/map-transit-layer/map-transit-layer.spec.ts @@ -1,5 +1,5 @@ import {Component} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flush} from '@angular/core/testing'; import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; import { @@ -16,24 +16,24 @@ describe('MapTransitLayer', () => { beforeEach(() => { mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy).and.callThrough(); + createMapConstructorSpy(mapSpy); }); afterEach(() => { (window.google as any) = undefined; }); - it('initializes a Google Map Transit Layer', () => { + it('initializes a Google Map Transit Layer', fakeAsync(() => { const transitLayerSpy = createTransitLayerSpy(); - const transitLayerConstructorSpy = - createTransitLayerConstructorSpy(transitLayerSpy).and.callThrough(); + const transitLayerConstructorSpy = createTransitLayerConstructorSpy(transitLayerSpy); const fixture = TestBed.createComponent(TestApp); fixture.detectChanges(); + flush(); expect(transitLayerConstructorSpy).toHaveBeenCalled(); expect(transitLayerSpy.setMap).toHaveBeenCalledWith(mapSpy); - }); + })); }); @Component({ diff --git a/src/google-maps/map-transit-layer/map-transit-layer.ts b/src/google-maps/map-transit-layer/map-transit-layer.ts index bc1a760b54d6..d0a3812e8840 100644 --- a/src/google-maps/map-transit-layer/map-transit-layer.ts +++ b/src/google-maps/map-transit-layer/map-transit-layer.ts @@ -9,7 +9,7 @@ // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 /// -import {Directive} from '@angular/core'; +import {Directive, EventEmitter, Output} from '@angular/core'; import {MapBaseLayer} from '../map-base-layer'; @@ -31,19 +31,25 @@ export class MapTransitLayer extends MapBaseLayer { */ transitLayer?: google.maps.TransitLayer; - protected override _initializeObject() { - this.transitLayer = new google.maps.TransitLayer(); + /** Event emitted when the transit layer is initialized. */ + @Output() readonly transitLayerInitialized: EventEmitter = + new EventEmitter(); + + protected override async _initializeObject() { + const layerConstructor = + google.maps.TransitLayer || + ((await google.maps.importLibrary('maps')) as google.maps.MapsLibrary).TransitLayer; + this.transitLayer = new layerConstructor(); + this.transitLayerInitialized.emit(this.transitLayer); } - protected override _setMap() { + protected override _setMap(map: google.maps.Map) { this._assertLayerInitialized(); - this.transitLayer.setMap(this._map.googleMap!); + this.transitLayer.setMap(map); } protected override _unsetMap() { - if (this.transitLayer) { - this.transitLayer.setMap(null); - } + this.transitLayer?.setMap(null); } private _assertLayerInitialized(): asserts this is {transitLayer: google.maps.TransitLayer} { diff --git a/src/google-maps/testing/fake-google-map-utils.ts b/src/google-maps/testing/fake-google-map-utils.ts index 0a7035c94d45..1c56ec6b320f 100644 --- a/src/google-maps/testing/fake-google-map-utils.ts +++ b/src/google-maps/testing/fake-google-map-utils.ts @@ -75,9 +75,11 @@ export function createMapConstructorSpy( apiLoaded = true, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const mapConstructorSpy = jasmine.createSpy('Map constructor', function () { - return mapSpy; - }); + const mapConstructorSpy = jasmine + .createSpy('Map constructor', function () { + return mapSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (apiLoaded) { testingWindow.google = { @@ -119,9 +121,11 @@ export function createMarkerConstructorSpy( markerSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const markerConstructorSpy = jasmine.createSpy('Marker constructor', function () { - return markerSpy; - }); + const markerConstructorSpy = jasmine + .createSpy('Marker constructor', function () { + return markerSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['Marker'] = markerConstructorSpy; @@ -148,6 +152,8 @@ export function createInfoWindowSpy( 'getZIndex', 'open', 'get', + 'setOptions', + 'setPosition', ]); infoWindowSpy.addListener.and.returnValue({remove: () => {}}); infoWindowSpy.open.and.callFake((config: any) => (anchor = config.anchor)); @@ -161,9 +167,11 @@ export function createInfoWindowConstructorSpy( infoWindowSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const infoWindowConstructorSpy = jasmine.createSpy('InfoWindow constructor', function () { - return infoWindowSpy; - }); + const infoWindowConstructorSpy = jasmine + .createSpy('InfoWindow constructor', function () { + return infoWindowSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['InfoWindow'] = infoWindowConstructorSpy; @@ -200,9 +208,11 @@ export function createPolylineConstructorSpy( polylineSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const polylineConstructorSpy = jasmine.createSpy('Polyline constructor', function () { - return polylineSpy; - }); + const polylineConstructorSpy = jasmine + .createSpy('Polyline constructor', function () { + return polylineSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['Polyline'] = polylineConstructorSpy; @@ -230,6 +240,7 @@ export function createPolygonSpy( 'setMap', 'setOptions', 'setPath', + 'setPaths', ]); polygonSpy.addListener.and.returnValue({remove: () => {}}); return polygonSpy; @@ -240,9 +251,11 @@ export function createPolygonConstructorSpy( polygonSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const polygonConstructorSpy = jasmine.createSpy('Polygon constructor', function () { - return polygonSpy; - }); + const polygonConstructorSpy = jasmine + .createSpy('Polygon constructor', function () { + return polygonSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['Polygon'] = polygonConstructorSpy; @@ -279,9 +292,11 @@ export function createRectangleConstructorSpy( rectangleSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const rectangleConstructorSpy = jasmine.createSpy('Rectangle constructor', function () { - return rectangleSpy; - }); + const rectangleConstructorSpy = jasmine + .createSpy('Rectangle constructor', function () { + return rectangleSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['Rectangle'] = rectangleConstructorSpy; @@ -320,9 +335,11 @@ export function createCircleConstructorSpy( circleSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const circleConstructorSpy = jasmine.createSpy('Circle constructor', function () { - return circleSpy; - }); + const circleConstructorSpy = jasmine + .createSpy('Circle constructor', function () { + return circleSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['Circle'] = circleConstructorSpy; @@ -362,9 +379,11 @@ export function createGroundOverlayConstructorSpy( groundOverlaySpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const groundOverlayConstructorSpy = jasmine.createSpy('GroundOverlay constructor', function () { - return groundOverlaySpy; - }); + const groundOverlayConstructorSpy = jasmine + .createSpy('GroundOverlay constructor', function () { + return groundOverlaySpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['GroundOverlay'] = groundOverlayConstructorSpy; @@ -402,9 +421,11 @@ export function createKmlLayerConstructorSpy( kmlLayerSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const kmlLayerConstructorSpy = jasmine.createSpy('KmlLayer constructor', function () { - return kmlLayerSpy; - }); + const kmlLayerConstructorSpy = jasmine + .createSpy('KmlLayer constructor', function () { + return kmlLayerSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['KmlLayer'] = kmlLayerConstructorSpy; @@ -434,9 +455,11 @@ export function createTrafficLayerConstructorSpy( trafficLayerSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const trafficLayerConstructorSpy = jasmine.createSpy('TrafficLayer constructor', function () { - return trafficLayerSpy; - }); + const trafficLayerConstructorSpy = jasmine + .createSpy('TrafficLayer constructor', function () { + return trafficLayerSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['TrafficLayer'] = trafficLayerConstructorSpy; @@ -461,9 +484,11 @@ export function createTransitLayerConstructorSpy( transitLayerSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const transitLayerConstructorSpy = jasmine.createSpy('TransitLayer constructor', function () { - return transitLayerSpy; - }); + const transitLayerConstructorSpy = jasmine + .createSpy('TransitLayer constructor', function () { + return transitLayerSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['TransitLayer'] = transitLayerConstructorSpy; @@ -488,9 +513,11 @@ export function createBicyclingLayerConstructorSpy( bicylingLayerSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const bicylingLayerConstructorSpy = jasmine.createSpy('BicyclingLayer constructor', function () { - return bicylingLayerSpy; - }); + const bicylingLayerConstructorSpy = jasmine + .createSpy('BicyclingLayer constructor', function () { + return bicylingLayerSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['BicyclingLayer'] = bicylingLayerConstructorSpy; @@ -560,12 +587,11 @@ export function createMarkerClustererConstructorSpy( apiLoaded = true, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const markerClustererConstructorSpy = jasmine.createSpy( - 'MarkerClusterer constructor', - function () { + const markerClustererConstructorSpy = jasmine + .createSpy('MarkerClusterer constructor', function () { return markerClustererSpy; - }, - ); + }) + .and.callThrough(); if (apiLoaded) { const testingWindow: TestingWindow = window; testingWindow['MarkerClusterer'] = markerClustererConstructorSpy; @@ -595,12 +621,11 @@ export function createDirectionsRendererConstructorSpy( directionsRendererSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const directionsRendererConstructorSpy = jasmine.createSpy( - 'DirectionsRenderer constructor', - function () { + const directionsRendererConstructorSpy = jasmine + .createSpy('DirectionsRenderer constructor', function () { return directionsRendererSpy; - }, - ); + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['DirectionsRenderer'] = directionsRendererConstructorSpy; @@ -625,12 +650,11 @@ export function createDirectionsServiceConstructorSpy( directionsServiceSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const directionsServiceConstructorSpy = jasmine.createSpy( - 'DirectionsService constructor', - function () { + const directionsServiceConstructorSpy = jasmine + .createSpy('DirectionsService constructor', function () { return directionsServiceSpy; - }, - ); + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['DirectionsService'] = directionsServiceConstructorSpy; @@ -663,9 +687,11 @@ export function createHeatmapLayerConstructorSpy( heatmapLayerSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const heatmapLayerConstructorSpy = jasmine.createSpy('HeatmapLayer constructor', function () { - return heatmapLayerSpy; - }); + const heatmapLayerConstructorSpy = jasmine + .createSpy('HeatmapLayer constructor', function () { + return heatmapLayerSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { if (!testingWindow.google.maps.visualization) { @@ -694,9 +720,11 @@ export function createLatLngConstructorSpy( latLngSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const latLngConstructorSpy = jasmine.createSpy('LatLng constructor', function () { - return latLngSpy; - }); + const latLngConstructorSpy = jasmine + .createSpy('LatLng constructor', function () { + return latLngSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['LatLng'] = latLngConstructorSpy; @@ -720,9 +748,11 @@ export function createGeocoderConstructorSpy( geocoderSpy: jasmine.SpyObj, ): jasmine.Spy { // The spy target function cannot be an arrow-function as this breaks when created through `new`. - const geocoderConstructorSpy = jasmine.createSpy('Geocoder constructor', function () { - return geocoderSpy; - }); + const geocoderConstructorSpy = jasmine + .createSpy('Geocoder constructor', function () { + return geocoderSpy; + }) + .and.callThrough(); const testingWindow: TestingWindow = window; if (testingWindow.google && testingWindow.google.maps) { testingWindow.google.maps['Geocoder'] = geocoderConstructorSpy; diff --git a/tools/public_api_guard/google-maps/google-maps.md b/tools/public_api_guard/google-maps/google-maps.md index b32c058e215b..bc9fa497a498 100644 --- a/tools/public_api_guard/google-maps/google-maps.md +++ b/tools/public_api_guard/google-maps/google-maps.md @@ -107,6 +107,7 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy { panTo(latLng: google.maps.LatLng | google.maps.LatLngLiteral): void; panToBounds(latLngBounds: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral, padding?: number | google.maps.Padding): void; readonly projectionChanged: Observable; + _resolveMap(): Promise; readonly tilesloaded: Observable; readonly tiltChanged: Observable; width: string | number | null; @@ -142,17 +143,17 @@ export interface MapAnchorPoint { export class MapBaseLayer implements OnInit, OnDestroy { constructor(_map: GoogleMap, _ngZone: NgZone); // (undocumented) - protected _initializeObject(): void; + protected _initializeObject(): Promise; // (undocumented) protected readonly _map: GoogleMap; // (undocumented) ngOnDestroy(): void; // (undocumented) - ngOnInit(): void; + ngOnInit(): Promise; // (undocumented) protected readonly _ngZone: NgZone; // (undocumented) - protected _setMap(): void; + protected _setMap(_map: google.maps.Map): void; // (undocumented) protected _unsetMap(): void; // (undocumented) @@ -164,14 +165,15 @@ export class MapBaseLayer implements OnInit, OnDestroy { // @public export class MapBicyclingLayer extends MapBaseLayer { bicyclingLayer?: google.maps.BicyclingLayer; + readonly bicyclingLayerInitialized: EventEmitter; // (undocumented) - protected _initializeObject(): void; + protected _initializeObject(): Promise; // (undocumented) - protected _setMap(): void; + protected _setMap(map: google.maps.Map): void; // (undocumented) protected _unsetMap(): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -194,6 +196,7 @@ export class MapCircle implements OnInit, OnDestroy { readonly circleDragend: Observable; // (undocumented) readonly circleDragstart: Observable; + readonly circleInitialized: EventEmitter; // (undocumented) readonly circleMousedown: Observable; // (undocumented) @@ -229,7 +232,7 @@ export class MapCircle implements OnInit, OnDestroy { // (undocumented) readonly radiusChanged: Observable; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -240,6 +243,7 @@ export class MapDirectionsRenderer implements OnInit, OnChanges, OnDestroy { set directions(directions: google.maps.DirectionsResult); readonly directionsChanged: Observable; directionsRenderer?: google.maps.DirectionsRenderer; + readonly directionsRendererInitialized: EventEmitter; getDirections(): google.maps.DirectionsResult | null; getPanel(): Node | null; getRouteIndex(): number; @@ -251,7 +255,7 @@ export class MapDirectionsRenderer implements OnInit, OnChanges, OnDestroy { ngOnInit(): void; set options(options: google.maps.DirectionsRendererOptions); // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -310,6 +314,7 @@ export class MapGroundOverlay implements OnInit, OnDestroy { getOpacity(): number; getUrl(): string; groundOverlay?: google.maps.GroundOverlay; + readonly groundOverlayInitialized: EventEmitter; readonly mapClick: Observable; readonly mapDblclick: Observable; // (undocumented) @@ -319,7 +324,7 @@ export class MapGroundOverlay implements OnInit, OnDestroy { set opacity(opacity: number); set url(url: string); // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -330,6 +335,7 @@ export class MapHeatmapLayer implements OnInit, OnChanges, OnDestroy { set data(data: HeatmapData); getData(): HeatmapData; heatmap?: google.maps.visualization.HeatmapLayer; + readonly heatmapInitialized: EventEmitter; // (undocumented) ngOnChanges(changes: SimpleChanges): void; // (undocumented) @@ -338,7 +344,7 @@ export class MapHeatmapLayer implements OnInit, OnChanges, OnDestroy { ngOnInit(): void; set options(options: Partial); // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -354,6 +360,7 @@ export class MapInfoWindow implements OnInit, OnDestroy { getPosition(): google.maps.LatLng | null; getZIndex(): number; infoWindow?: google.maps.InfoWindow; + readonly infoWindowInitialized: EventEmitter; // (undocumented) ngOnDestroy(): void; // (undocumented) @@ -366,7 +373,7 @@ export class MapInfoWindow implements OnInit, OnDestroy { readonly positionChanged: Observable; readonly zindexChanged: Observable; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -382,6 +389,7 @@ export class MapKmlLayer implements OnInit, OnDestroy { getZIndex(): number; readonly kmlClick: Observable; kmlLayer?: google.maps.KmlLayer; + readonly kmlLayerInitialized: EventEmitter; // (undocumented) ngOnDestroy(): void; // (undocumented) @@ -392,7 +400,7 @@ export class MapKmlLayer implements OnInit, OnDestroy { // (undocumented) set url(url: string); // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -433,6 +441,7 @@ export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { readonly mapMouseup: Observable; readonly mapRightclick: Observable; marker?: google.maps.Marker; + readonly markerInitialized: EventEmitter; // (undocumented) ngOnChanges(changes: SimpleChanges): void; // (undocumented) @@ -442,6 +451,7 @@ export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { set options(options: google.maps.MarkerOptions); set position(position: google.maps.LatLngLiteral | google.maps.LatLng); readonly positionChanged: Observable; + _resolveMarker(): Promise; readonly shapeChanged: Observable; set title(title: string); readonly titleChanged: Observable; @@ -449,7 +459,7 @@ export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { readonly visibleChanged: Observable; readonly zindexChanged: Observable; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -525,6 +535,7 @@ export class MapMarkerClusterer implements OnInit, AfterContentInit, OnChanges, // (undocumented) set imageSizes(imageSizes: number[]); markerClusterer?: MarkerClusterer; + readonly markerClustererInitialized: EventEmitter; // (undocumented) _markers: QueryList; // (undocumented) @@ -550,7 +561,7 @@ export class MapMarkerClusterer implements OnInit, AfterContentInit, OnChanges, // (undocumented) set zoomOnClick(zoomOnClick: boolean); // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -577,6 +588,7 @@ export class MapPolygon implements OnInit, OnDestroy { readonly polygonDrag: Observable; readonly polygonDragend: Observable; readonly polygonDragstart: Observable; + readonly polygonInitialized: EventEmitter; readonly polygonMousedown: Observable; readonly polygonMousemove: Observable; readonly polygonMouseout: Observable; @@ -584,7 +596,7 @@ export class MapPolygon implements OnInit, OnDestroy { readonly polygonMouseup: Observable; readonly polygonRightclick: Observable; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -610,6 +622,7 @@ export class MapPolyline implements OnInit, OnDestroy { readonly polylineDrag: Observable; readonly polylineDragend: Observable; readonly polylineDragstart: Observable; + readonly polylineInitialized: EventEmitter; readonly polylineMousedown: Observable; readonly polylineMousemove: Observable; readonly polylineMouseout: Observable; @@ -617,7 +630,7 @@ export class MapPolyline implements OnInit, OnDestroy { readonly polylineMouseup: Observable; readonly polylineRightclick: Observable; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -644,6 +657,7 @@ export class MapRectangle implements OnInit, OnDestroy { readonly rectangleDrag: Observable; readonly rectangleDragend: Observable; readonly rectangleDragstart: Observable; + readonly rectangleInitialized: EventEmitter; readonly rectangleMousedown: Observable; readonly rectangleMousemove: Observable; readonly rectangleMouseout: Observable; @@ -651,7 +665,7 @@ export class MapRectangle implements OnInit, OnDestroy { readonly rectangleMouseup: Observable; readonly rectangleRightclick: Observable; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -665,8 +679,9 @@ export class MapTrafficLayer implements OnInit, OnDestroy { // (undocumented) ngOnInit(): void; trafficLayer?: google.maps.TrafficLayer; + readonly trafficLayerInitialized: EventEmitter; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -674,14 +689,15 @@ export class MapTrafficLayer implements OnInit, OnDestroy { // @public export class MapTransitLayer extends MapBaseLayer { // (undocumented) - protected _initializeObject(): void; + protected _initializeObject(): Promise; // (undocumented) - protected _setMap(): void; + protected _setMap(map: google.maps.Map): void; transitLayer?: google.maps.TransitLayer; + readonly transitLayerInitialized: EventEmitter; // (undocumented) protected _unsetMap(): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; }