diff --git a/src/dev-app/google-map/google-map-demo.html b/src/dev-app/google-map/google-map-demo.html
index 588e0a77432d..afaecc973b46 100644
--- a/src/dev-app/google-map/google-map-demo.html
+++ b/src/dev-app/google-map/google-map-demo.html
@@ -7,10 +7,12 @@
(mapMousemove)="handleMove($event)"
(mapRightclick)="handleRightclick()">
-
+
+ Testing 1 2 3
Latitude: {{display?.lat}}
diff --git a/src/dev-app/google-map/google-map-demo.ts b/src/dev-app/google-map/google-map-demo.ts
index 163c8573f39e..91d41c0ea06e 100644
--- a/src/dev-app/google-map/google-map-demo.ts
+++ b/src/dev-app/google-map/google-map-demo.ts
@@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Component} from '@angular/core';
import {HttpClient} from '@angular/common/http';
+import {Component, ViewChild} from '@angular/core';
+import {MapInfoWindow, MapMarker} from '@angular/google-maps';
/** Demo Component for @angular/google-maps/map */
@Component({
@@ -16,11 +17,14 @@ import {HttpClient} from '@angular/common/http';
templateUrl: 'google-map-demo.html',
})
export class GoogleMapDemo {
+ @ViewChild(MapInfoWindow, {static: false}) infoWindow: MapInfoWindow;
+
isReady = false;
center = {lat: 24, lng: 12};
markerOptions = {draggable: false};
markerPositions: google.maps.LatLngLiteral[] = [];
+ infoWindowPosition: google.maps.LatLngLiteral;
zoom = 4;
display?: google.maps.LatLngLiteral;
@@ -39,9 +43,8 @@ export class GoogleMapDemo {
this.display = event.latLng.toJSON();
}
- clickMarker(event: google.maps.MouseEvent) {
- console.log(this.markerOptions);
- this.markerOptions = {draggable: true};
+ clickMarker(marker: MapMarker) {
+ this.infoWindow.open(marker);
}
handleRightclick() {
diff --git a/src/google-maps/BUILD.bazel b/src/google-maps/BUILD.bazel
index 2437ae7a759e..0ee91e2d76bf 100644
--- a/src/google-maps/BUILD.bazel
+++ b/src/google-maps/BUILD.bazel
@@ -13,6 +13,7 @@ ng_module(
deps = [
"@npm//@angular/core",
"@npm//@types/googlemaps",
+ "@npm//rxjs",
],
)
diff --git a/src/google-maps/google-map/google-map.ts b/src/google-maps/google-map/google-map.ts
index 049c98d7f929..81963bf8f4b2 100644
--- a/src/google-maps/google-map/google-map.ts
+++ b/src/google-maps/google-map/google-map.ts
@@ -191,7 +191,7 @@ export class GoogleMap implements OnChanges, OnInit, AfterContentInit, OnDestroy
@ContentChildren(MapMarker) _markers: QueryList;
private _mapEl: HTMLElement;
- private _googleMap!: UpdatedGoogleMap;
+ _googleMap!: UpdatedGoogleMap;
private _googleMapChanges!: Observable;
diff --git a/src/google-maps/google-maps-module.ts b/src/google-maps/google-maps-module.ts
index d816f00546ad..28c81e47a38a 100644
--- a/src/google-maps/google-maps-module.ts
+++ b/src/google-maps/google-maps-module.ts
@@ -8,16 +8,19 @@
import {NgModule} from '@angular/core';
-import {MapMarker, MapMarkerModule} from './map-marker/index';
import {GoogleMap, GoogleMapModule} from './google-map/index';
+import {MapInfoWindow, MapInfoWindowModule} from './map-info-window/index';
+import {MapMarker, MapMarkerModule} from './map-marker/index';
@NgModule({
imports: [
GoogleMapModule,
+ MapInfoWindowModule,
MapMarkerModule,
],
exports: [
GoogleMap,
+ MapInfoWindow,
MapMarker,
],
})
diff --git a/src/google-maps/map-info-window/index.ts b/src/google-maps/map-info-window/index.ts
new file mode 100644
index 000000000000..46f6e6539a4d
--- /dev/null
+++ b/src/google-maps/map-info-window/index.ts
@@ -0,0 +1,10 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+export * from './map-info-window';
+export * from './map-info-window-module';
diff --git a/src/google-maps/map-info-window/map-info-window-module.ts b/src/google-maps/map-info-window/map-info-window-module.ts
new file mode 100644
index 000000000000..de7eed77138b
--- /dev/null
+++ b/src/google-maps/map-info-window/map-info-window-module.ts
@@ -0,0 +1,17 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {NgModule} from '@angular/core';
+import {MapInfoWindow} from './map-info-window';
+
+@NgModule({
+ exports: [MapInfoWindow],
+ declarations: [MapInfoWindow],
+})
+export class MapInfoWindowModule {
+}
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
new file mode 100644
index 000000000000..c7f488434c37
--- /dev/null
+++ b/src/google-maps/map-info-window/map-info-window.spec.ts
@@ -0,0 +1,185 @@
+import {Component} from '@angular/core';
+import {async, TestBed} from '@angular/core/testing';
+import {By} from '@angular/platform-browser';
+
+import {DEFAULT_OPTIONS, GoogleMapModule, UpdatedGoogleMap} from '../google-map/index';
+import {MapMarker} from '../map-marker/index';
+import {
+ createInfoWindowConstructorSpy,
+ createInfoWindowSpy,
+ createMapConstructorSpy,
+ createMapSpy,
+ TestingWindow
+} from '../testing/fake-google-map-utils';
+
+import {MapInfoWindow, MapInfoWindowModule} from './index';
+
+describe('MapInfoWindow', () => {
+ let mapSpy: jasmine.SpyObj;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ GoogleMapModule,
+ MapInfoWindowModule,
+ ],
+ declarations: [TestApp],
+ });
+ }));
+
+ beforeEach(() => {
+ TestBed.compileComponents();
+
+ mapSpy = createMapSpy(DEFAULT_OPTIONS);
+ createMapConstructorSpy(mapSpy).and.callThrough();
+ });
+
+ afterEach(() => {
+ const testingWindow: TestingWindow = window;
+ delete testingWindow.google;
+ });
+
+ it('initializes a Google Map Info Window', () => {
+ const infoWindowSpy = createInfoWindowSpy({});
+ const infoWindowConstructorSpy =
+ createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough();
+
+ const fixture = TestBed.createComponent(TestApp);
+ fixture.detectChanges();
+
+ expect(infoWindowConstructorSpy).toHaveBeenCalledWith({
+ position: undefined,
+ content: jasmine.any(Node),
+ });
+ });
+
+ it('sets position', () => {
+ const position: google.maps.LatLngLiteral = {lat: 5, lng: 7};
+ const infoWindowSpy = createInfoWindowSpy({position});
+ const infoWindowConstructorSpy =
+ createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough();
+
+ const fixture = TestBed.createComponent(TestApp);
+ fixture.componentInstance.position = position;
+ fixture.detectChanges();
+
+ expect(infoWindowConstructorSpy).toHaveBeenCalledWith({
+ position,
+ content: jasmine.any(Node),
+ });
+ });
+
+ it('sets options', () => {
+ 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 fixture = TestBed.createComponent(TestApp);
+ fixture.componentInstance.options = options;
+ fixture.detectChanges();
+
+ expect(infoWindowConstructorSpy).toHaveBeenCalledWith({
+ ...options,
+ content: jasmine.any(Node),
+ });
+ });
+
+ it('gives preference to position over options', () => {
+ const position: google.maps.LatLngLiteral = {lat: 5, lng: 7};
+ const options: google.maps.InfoWindowOptions = {
+ position: {lat: 3, lng: 5},
+ maxWidth: 50,
+ disableAutoPan: true,
+ };
+ const infoWindowSpy = createInfoWindowSpy({...options, position});
+ const infoWindowConstructorSpy =
+ createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough();
+
+ const fixture = TestBed.createComponent(TestApp);
+ fixture.componentInstance.options = options;
+ fixture.componentInstance.position = position;
+ fixture.detectChanges();
+
+ expect(infoWindowConstructorSpy).toHaveBeenCalledWith({
+ ...options,
+ position,
+ content: jasmine.any(Node),
+ });
+ });
+
+ it('exposes methods that change the configuration of the info window', () => {
+ const fakeMarker = {} as unknown as google.maps.Marker;
+ const fakeMarkerComponent = {_marker: fakeMarker} as unknown as MapMarker;
+ const infoWindowSpy = createInfoWindowSpy({});
+ createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough();
+
+ const fixture = TestBed.createComponent(TestApp);
+ const infoWindowComponent = fixture.debugElement.query(By.directive(
+ MapInfoWindow))!.injector.get(MapInfoWindow);
+ fixture.detectChanges();
+
+ infoWindowComponent.close();
+ expect(infoWindowSpy.close).toHaveBeenCalled();
+
+ infoWindowComponent.open(fakeMarkerComponent);
+ expect(infoWindowSpy.open).toHaveBeenCalledWith(mapSpy, fakeMarker);
+ });
+
+ it('exposes methods that provide information about the info window', () => {
+ const infoWindowSpy = createInfoWindowSpy({});
+ createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough();
+
+ const fixture = TestBed.createComponent(TestApp);
+ const infoWindowComponent = fixture.debugElement.query(By.directive(
+ MapInfoWindow))!.injector.get(MapInfoWindow);
+ fixture.detectChanges();
+
+ infoWindowSpy.getContent.and.returnValue('test content');
+ expect(infoWindowComponent.getContent()).toBe('test content');
+
+ infoWindowComponent.getPosition();
+ expect(infoWindowSpy.getPosition).toHaveBeenCalled();
+
+ infoWindowSpy.getZIndex.and.returnValue(5);
+ expect(infoWindowComponent.getZIndex()).toBe(5);
+ });
+
+ it('initializes info window event handlers', () => {
+ const infoWindowSpy = createInfoWindowSpy({});
+ createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough();
+
+ const fixture = TestBed.createComponent(TestApp);
+ fixture.detectChanges();
+
+ expect(infoWindowSpy.addListener).toHaveBeenCalledWith('closeclick', jasmine.any(Function));
+ expect(infoWindowSpy.addListener)
+ .not.toHaveBeenCalledWith('content_changed', jasmine.any(Function));
+ expect(infoWindowSpy.addListener).not.toHaveBeenCalledWith('domready', jasmine.any(Function));
+ expect(infoWindowSpy.addListener)
+ .not.toHaveBeenCalledWith('position_changed', jasmine.any(Function));
+ expect(infoWindowSpy.addListener)
+ .not.toHaveBeenCalledWith('zindex_changed', jasmine.any(Function));
+ });
+});
+
+@Component({
+ selector: 'test-app',
+ template: `
+
+ test content
+
+ `,
+})
+class TestApp {
+ position?: google.maps.LatLngLiteral;
+ options?: google.maps.InfoWindowOptions;
+
+ handleClose() {}
+}
diff --git a/src/google-maps/map-info-window/map-info-window.ts b/src/google-maps/map-info-window/map-info-window.ts
new file mode 100644
index 000000000000..11136f8c82e7
--- /dev/null
+++ b/src/google-maps/map-info-window/map-info-window.ts
@@ -0,0 +1,181 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {
+ Directive,
+ ElementRef,
+ EventEmitter,
+ Input,
+ OnDestroy,
+ OnInit,
+ Output,
+} from '@angular/core';
+import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs';
+import {map, takeUntil} from 'rxjs/operators';
+
+import {GoogleMap} from '../google-map/index';
+import {MapMarker} from '../map-marker/index';
+
+/**
+ * Angular component that renders a Google Maps info window via the Google Maps JavaScript API.
+ * @see developers.google.com/maps/documentation/javascript/reference/info-window
+ */
+@Directive({
+ selector: 'map-info-window',
+ host: {'style': 'display: none'},
+})
+export class MapInfoWindow implements OnInit, OnDestroy {
+ @Input()
+ set options(options: google.maps.InfoWindowOptions) {
+ this._options.next(options || {});
+ }
+
+ @Input()
+ set position(position: google.maps.LatLngLiteral) {
+ this._position.next(position);
+ }
+
+ /**
+ * See
+ * developers.google.com/maps/documentation/javascript/reference/info-window#InfoWindow.closeclick
+ */
+ @Output() closeclick = new EventEmitter();
+
+ /**
+ * See
+ * developers.google.com/maps/documentation/javascript/reference/info-window
+ * #InfoWindow.content_changed
+ */
+ @Output() contentChanged = new EventEmitter();
+
+ /**
+ * See
+ * developers.google.com/maps/documentation/javascript/reference/info-window#InfoWindow.domready
+ */
+ @Output() domready = new EventEmitter();
+
+ /**
+ * See
+ * developers.google.com/maps/documentation/javascript/reference/info-window
+ * #InfoWindow.position_changed
+ */
+ @Output() positionChanged = new EventEmitter();
+
+ /**
+ * See
+ * developers.google.com/maps/documentation/javascript/reference/info-window
+ * #InfoWindow.zindex_changed
+ */
+ @Output() zindexChanged = new EventEmitter();
+
+ private readonly _options = new BehaviorSubject({});
+ private readonly _position = new BehaviorSubject(undefined);
+
+ private readonly _listeners: google.maps.MapsEventListener[] = [];
+
+ private readonly _destroy = new Subject();
+
+ private _infoWindow?: google.maps.InfoWindow;
+
+ constructor(private readonly googleMap: GoogleMap, private _elementRef: ElementRef) {
+ }
+
+ ngOnInit() {
+ this._combineOptions().pipe(takeUntil(this._destroy)).subscribe(options => {
+ if (this._infoWindow) {
+ this._infoWindow.setOptions(options);
+ } else {
+ this._infoWindow = new google.maps.InfoWindow(options);
+ this._initializeEventHandlers();
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ this._destroy.next();
+ this._destroy.complete();
+ for (let listener of this._listeners) {
+ listener.remove();
+ }
+ this.close();
+ }
+
+ /**
+ * See developers.google.com/maps/documentation/javascript/reference/info-window#InfoWindow.close
+ */
+ close() {
+ if (this._infoWindow) {
+ this._infoWindow.close();
+ }
+ }
+
+ /**
+ * See
+ * developers.google.com/maps/documentation/javascript/reference/info-window#InfoWindow.getContent
+ */
+ getContent(): string|Node {
+ return this._infoWindow!.getContent();
+ }
+
+ /**
+ * See
+ * developers.google.com/maps/documentation/javascript/reference/info-window
+ * #InfoWindow.getPosition
+ */
+ getPosition(): google.maps.LatLng|null {
+ return this._infoWindow!.getPosition() || null;
+ }
+
+ /**
+ * See
+ * developers.google.com/maps/documentation/javascript/reference/info-window#InfoWindow.getZIndex
+ */
+ getZIndex(): number {
+ return this._infoWindow!.getZIndex();
+ }
+
+ /**
+ * Opens the MapInfoWindow using the provided MapMarker as the anchor. If the anchor is not set,
+ * then the position property of the options input is used instead.
+ */
+ open(anchor?: MapMarker) {
+ const marker = anchor ? anchor._marker : undefined;
+ if (this.googleMap._googleMap) {
+ this._elementRef.nativeElement.style.display = '';
+ this._infoWindow!.open(this.googleMap._googleMap, marker);
+ }
+ }
+
+ private _combineOptions(): Observable {
+ return combineLatest(this._options, this._position).pipe(map(([options, position]) => {
+ const combinedOptions: google.maps.InfoWindowOptions = {
+ ...options,
+ position: position || options.position,
+ content: this._elementRef.nativeElement,
+ };
+ return combinedOptions;
+ }));
+ }
+
+ private _initializeEventHandlers() {
+ const eventHandlers = new Map>([
+ ['closeclick', this.closeclick],
+ ['content_changed', this.contentChanged],
+ ['domready', this.domready],
+ ['position_changed', this.positionChanged],
+ ['zindex_changed', this.zindexChanged],
+ ]);
+ eventHandlers.forEach((eventHandler: EventEmitter, name: string) => {
+ if (eventHandler.observers.length > 0) {
+ this._listeners.push(this._infoWindow!.addListener(name, () => {
+ eventHandler.emit();
+ }));
+ }
+ });
+ }
+}
diff --git a/src/google-maps/map-marker/map-marker.ts b/src/google-maps/map-marker/map-marker.ts
index cbf69381c4c8..5ff6b6b4f8bd 100644
--- a/src/google-maps/map-marker/map-marker.ts
+++ b/src/google-maps/map-marker/map-marker.ts
@@ -204,9 +204,10 @@ export class MapMarker implements OnInit, OnDestroy {
private readonly _listeners: google.maps.MapsEventListener[] = [];
- private _marker?: google.maps.Marker;
private _hasMap = false;
+ _marker?: google.maps.Marker;
+
ngOnInit() {
const combinedOptionsChanges = this._combineOptions();
diff --git a/src/google-maps/public-api.ts b/src/google-maps/public-api.ts
index bd57b5427d97..a16afad5b4df 100644
--- a/src/google-maps/public-api.ts
+++ b/src/google-maps/public-api.ts
@@ -7,5 +7,6 @@
*/
export {GoogleMap} from './google-map/index';
+export {MapInfoWindow} from './map-info-window/index';
export {MapMarker} from './map-marker/index';
export * from './google-maps-module';
diff --git a/src/google-maps/testing/fake-google-map-utils.ts b/src/google-maps/testing/fake-google-map-utils.ts
index f0747e883b29..5f5988efc736 100644
--- a/src/google-maps/testing/fake-google-map-utils.ts
+++ b/src/google-maps/testing/fake-google-map-utils.ts
@@ -14,6 +14,7 @@ export interface TestingWindow extends Window {
maps: {
Map?: jasmine.Spy;
Marker?: jasmine.Spy;
+ InfoWindow?: jasmine.Spy;
};
};
}
@@ -74,3 +75,33 @@ export function createMarkerConstructorSpy(markerSpy: jasmine.SpyObj {
+ const infoWindowSpy = jasmine.createSpyObj(
+ 'google.maps.InfoWindow',
+ ['addListener', 'close', 'getContent', 'getPosition', 'getZIndex', 'open']);
+ infoWindowSpy.addListener.and.returnValue({remove: () => {}});
+ return infoWindowSpy;
+}
+
+/** Creates a jasmine.Spy to watch for the constructor of a google.maps.InfoWindow */
+export function createInfoWindowConstructorSpy(
+ infoWindowSpy: jasmine.SpyObj): jasmine.Spy {
+ const infoWindowConstructorSpy =
+ jasmine.createSpy('InfoWindow constructor', (_options: google.maps.InfoWindowOptions) => {
+ return infoWindowSpy;
+ });
+ const testingWindow: TestingWindow = window;
+ if (testingWindow.google && testingWindow.google.maps) {
+ testingWindow.google.maps['InfoWindow'] = infoWindowConstructorSpy;
+ } else {
+ testingWindow.google = {
+ maps: {
+ 'InfoWindow': infoWindowConstructorSpy,
+ },
+ };
+ }
+ return infoWindowConstructorSpy;
+}
diff --git a/stylelint-config.json b/stylelint-config.json
index 161526f6890e..7ad289801351 100644
--- a/stylelint-config.json
+++ b/stylelint-config.json
@@ -89,7 +89,7 @@
"no-eol-whitespace": true,
"max-line-length": 100,
"linebreaks": "unix",
- "selector-class-pattern": ["^_?(mat-|cdk-|example-|demo-|ng-|mdc-)", {
+ "selector-class-pattern": ["^_?(mat-|cdk-|example-|demo-|ng-|mdc-|map-)", {
"resolveNestedSelectors": true
}]
}