diff --git a/packages.bzl b/packages.bzl index f0cf5c7d29c7..7e5eace774be 100644 --- a/packages.bzl +++ b/packages.bzl @@ -80,12 +80,6 @@ MATERIAL_SCSS_LIBS = [ for p in MATERIAL_PACKAGES ] -GOOGLE_MAPS_PACKAGES = [ - "google-map", -] - -GOOGLE_MAPS_TARGETS = ["//src/google-maps"] + ["//src/google-maps/%s" % p for p in GOOGLE_MAPS_PACKAGES] - MATERIAL_EXPERIMENTAL_PACKAGES = [ "mdc-button", "mdc-card", diff --git a/src/dev-app/google-map/BUILD.bazel b/src/dev-app/google-map/BUILD.bazel index 00302ae45f41..3b0c062cb3b3 100644 --- a/src/dev-app/google-map/BUILD.bazel +++ b/src/dev-app/google-map/BUILD.bazel @@ -7,7 +7,7 @@ ng_module( srcs = glob(["**/*.ts"]), assets = ["google-map-demo.html"], deps = [ - "//src/google-maps/google-map", + "//src/google-maps", "@npm//@angular/router", ], ) diff --git a/src/dev-app/google-map/google-map-demo-module.ts b/src/dev-app/google-map/google-map-demo-module.ts index 5473fb36884c..aeaaeeb31f28 100644 --- a/src/dev-app/google-map/google-map-demo-module.ts +++ b/src/dev-app/google-map/google-map-demo-module.ts @@ -9,7 +9,7 @@ import {CommonModule} from '@angular/common'; import {HttpClientJsonpModule, HttpClientModule} from '@angular/common/http'; import {NgModule} from '@angular/core'; -import {GoogleMapModule} from '@angular/google-maps/google-map'; +import {GoogleMapsModule} from '@angular/google-maps'; import {RouterModule} from '@angular/router'; import {GoogleMapDemo} from './google-map-demo'; @@ -17,7 +17,7 @@ import {GoogleMapDemo} from './google-map-demo'; @NgModule({ imports: [ CommonModule, - GoogleMapModule, + GoogleMapsModule, HttpClientJsonpModule, HttpClientModule, RouterModule.forChild([{path: '', component: GoogleMapDemo}]), diff --git a/src/dev-app/google-map/google-map-demo.html b/src/dev-app/google-map/google-map-demo.html index 64fe1d08b0e1..588e0a77432d 100644 --- a/src/dev-app/google-map/google-map-demo.html +++ b/src/dev-app/google-map/google-map-demo.html @@ -4,7 +4,14 @@ [center]="center" [zoom]="zoom" (mapClick)="handleClick($event)" - (mapMousemove)="handleMove($event)"> + (mapMousemove)="handleMove($event)" + (mapRightclick)="handleRightclick()"> + + +
Latitude: {{display?.lat}}
Longitude: {{display?.lng}}
diff --git a/src/dev-app/google-map/google-map-demo.ts b/src/dev-app/google-map/google-map-demo.ts index 0f3d7f7a8492..163c8573f39e 100644 --- a/src/dev-app/google-map/google-map-demo.ts +++ b/src/dev-app/google-map/google-map-demo.ts @@ -19,6 +19,8 @@ export class GoogleMapDemo { isReady = false; center = {lat: 24, lng: 12}; + markerOptions = {draggable: false}; + markerPositions: google.maps.LatLngLiteral[] = []; zoom = 4; display?: google.maps.LatLngLiteral; @@ -30,10 +32,19 @@ export class GoogleMapDemo { } handleClick(event: google.maps.MouseEvent) { - this.center = event.latLng.toJSON(); + this.markerPositions.push(event.latLng.toJSON()); } handleMove(event: google.maps.MouseEvent) { this.display = event.latLng.toJSON(); } + + clickMarker(event: google.maps.MouseEvent) { + console.log(this.markerOptions); + this.markerOptions = {draggable: true}; + } + + handleRightclick() { + this.markerPositions.pop(); + } } diff --git a/src/dev-app/system-config.js b/src/dev-app/system-config.js index ea418484e985..14b940b34b72 100644 --- a/src/dev-app/system-config.js +++ b/src/dev-app/system-config.js @@ -55,10 +55,6 @@ var MATERIAL_PACKAGES = [ 'tree', ]; -var GOOGLE_MAPS_PACKAGES = [ - 'google-map', -]; - var MATERIAL_EXPERIMENTAL_PACKAGES = [ 'mdc-button', 'mdc-card', @@ -103,9 +99,7 @@ MATERIAL_EXPERIMENTAL_PACKAGES.forEach(function(pkgName) { MATERIAL_PACKAGES.forEach(function(pkgName) { configureEntryPoint('material', pkgName); }); -GOOGLE_MAPS_PACKAGES.forEach(function(pkgName) { - configureEntryPoint('google-maps', pkgName); -}); +configureEntryPoint('google-maps'); configureEntryPoint('youtube-player'); /** Configures the specified package and its entry-point. */ diff --git a/src/dev-app/tsconfig.json b/src/dev-app/tsconfig.json index 74ead8d0563b..abeff4c04762 100644 --- a/src/dev-app/tsconfig.json +++ b/src/dev-app/tsconfig.json @@ -14,7 +14,7 @@ "@angular/cdk-experimental/*": ["../cdk-experimental/*"], "@angular/cdk-experimental": ["../cdk-experimental"], "@angular/material-moment-adapter": ["../material-moment-adapter/public-api.ts"], - "@angular/google-maps/*": ["../google-maps/*"], + "@angular/google-maps": ["../google-maps"], "@angular/material-examples": ["../../dist/packages/material-examples"] } }, diff --git a/src/google-maps/BUILD.bazel b/src/google-maps/BUILD.bazel index c1dcec127f86..c6ffa7c4762d 100644 --- a/src/google-maps/BUILD.bazel +++ b/src/google-maps/BUILD.bazel @@ -1,9 +1,8 @@ package(default_visibility = ["//visibility:public"]) -load("//:packages.bzl", "GOOGLE_MAPS_PACKAGES", "GOOGLE_MAPS_TARGETS", "ROLLUP_GLOBALS") +load("//:packages.bzl", "ROLLUP_GLOBALS") load("//tools:defaults.bzl", "ng_module", "ng_package") -# Root "@angular/google-maps" entry-point that does not re-export individual entry-points. ng_module( name = "google-maps", srcs = glob( @@ -11,17 +10,14 @@ ng_module( exclude = ["**/*.spec.ts"], ), module_name = "@angular/google-maps", - deps = ["//src/google-maps/%s" % p for p in GOOGLE_MAPS_PACKAGES] + [ + deps = [ + "//src/google-maps/google-map", + "//src/google-maps/map-marker", "@npm//@angular/core", "@npm//@types/googlemaps", ], ) -filegroup( - name = "overviews", - srcs = ["//src/google-maps/%s:overview" % name for name in GOOGLE_MAPS_PACKAGES], -) - # Creates the @angular/google-maps package published to npm ng_package( name = "npm_package", @@ -29,5 +25,10 @@ ng_package( entry_point = ":public-api.ts", entry_point_name = "google-maps", globals = ROLLUP_GLOBALS, - deps = GOOGLE_MAPS_TARGETS, + deps = [":google-maps"], +) + +filegroup( + name = "source-files", + srcs = glob(["**/*.ts"]), ) diff --git a/src/google-maps/google-map/BUILD.bazel b/src/google-maps/google-map/BUILD.bazel index 29bfa1e34ca7..2a0e523a379f 100644 --- a/src/google-maps/google-map/BUILD.bazel +++ b/src/google-maps/google-map/BUILD.bazel @@ -2,7 +2,6 @@ package(default_visibility = ["//visibility:public"]) load( "//tools:defaults.bzl", - "markdown_to_html", "ng_module", "ng_test_library", "ng_web_test_suite", @@ -14,8 +13,8 @@ ng_module( ["**/*.ts"], exclude = ["**/*.spec.ts"], ), - module_name = "@angular/google-maps/google-map", deps = [ + "//src/google-maps/map-marker", "@npm//@angular/core", "@npm//@types/googlemaps", "@npm//rxjs", @@ -30,7 +29,8 @@ ng_test_library( ), deps = [ ":google-map", - "//src/google-maps/google-map/testing", + "//src/google-maps/map-marker", + "//src/google-maps/testing", "@npm//@angular/platform-browser", ], ) @@ -40,11 +40,6 @@ ng_web_test_suite( deps = [":unit_test_sources"], ) -markdown_to_html( - name = "overview", - srcs = [":google-map.md"], -) - filegroup( name = "source-files", srcs = glob(["**/*.ts"]), diff --git a/src/google-maps/google-map/google-map-module.ts b/src/google-maps/google-map/google-map-module.ts index 3d4c69dc21e9..486899c06199 100644 --- a/src/google-maps/google-map/google-map-module.ts +++ b/src/google-maps/google-map/google-map-module.ts @@ -6,11 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ - import {NgModule} from '@angular/core'; - import {GoogleMap} from './google-map'; +import {NgModule} from '@angular/core'; - @NgModule({ - exports: [GoogleMap], - declarations: [GoogleMap], - }) - export class GoogleMapModule {} +import {GoogleMap} from './google-map'; + +@NgModule({ + exports: [GoogleMap], + declarations: [GoogleMap], +}) +export class GoogleMapModule { +} diff --git a/src/google-maps/google-map/google-map.md b/src/google-maps/google-map/google-map.md deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/google-maps/google-map/google-map.spec.ts b/src/google-maps/google-map/google-map.spec.ts index 2b5590b1ce27..f3de7fc8ac95 100644 --- a/src/google-maps/google-map/google-map.spec.ts +++ b/src/google-maps/google-map/google-map.spec.ts @@ -2,6 +2,13 @@ import {Component} from '@angular/core'; import {async, TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; +import {MapMarker, MapMarkerModule} from '../map-marker/index'; +import { + createMapConstructorSpy, + createMapSpy, + TestingWindow +} from '../testing/fake-google-map-utils'; + import { DEFAULT_HEIGHT, DEFAULT_OPTIONS, @@ -10,11 +17,6 @@ import { GoogleMapModule, UpdatedGoogleMap } from './index'; -import { - createMapConstructorSpy, - createMapSpy, - TestingWindow -} from './testing/fake-google-map-utils'; /** Represents boundaries of a map to be used in tests. */ const testBounds: google.maps.LatLngBoundsLiteral = { @@ -36,7 +38,10 @@ describe('GoogleMap', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [GoogleMapModule], + imports: [ + GoogleMapModule, + MapMarkerModule, + ], declarations: [TestApp], }); })); @@ -237,6 +242,18 @@ describe('GoogleMap', () => { expect(mapSpy.addListener).not.toHaveBeenCalledWith('tilt_changed', jasmine.any(Function)); expect(mapSpy.addListener).not.toHaveBeenCalledWith('zoom_changed', jasmine.any(Function)); }); + + it('calls setMap on child marker components', () => { + mapSpy = createMapSpy(DEFAULT_OPTIONS); + createMapConstructorSpy(mapSpy).and.callThrough(); + + const fixture = TestBed.createComponent(TestApp); + const markerComponent = fixture.debugElement.query(By.directive(MapMarker)).componentInstance; + spyOn(markerComponent, '_setMap').and.callThrough(); + fixture.detectChanges(); + + expect(markerComponent._setMap).toHaveBeenCalledWith(mapSpy); + }); }); @Component({ @@ -246,9 +263,11 @@ describe('GoogleMap', () => { [center]="center" [zoom]="zoom" [options]="options" - (mapClick)="handleClick" - (centerChanged)="handleCenterChanged" - (mapRightclick)="handleRightclick">`, + (mapClick)="handleClick($event)" + (centerChanged)="handleCenterChanged()" + (mapRightclick)="handleRightclick($event)"> + + `, }) class TestApp { height?: string; diff --git a/src/google-maps/google-map/google-map.ts b/src/google-maps/google-map/google-map.ts index 6c26042c215a..6c3021434248 100644 --- a/src/google-maps/google-map/google-map.ts +++ b/src/google-maps/google-map/google-map.ts @@ -1,6 +1,8 @@ import { + AfterContentInit, ChangeDetectionStrategy, Component, + ContentChildren, ElementRef, EventEmitter, Input, @@ -8,10 +10,13 @@ import { OnDestroy, OnInit, Output, + QueryList, } from '@angular/core'; import {BehaviorSubject, combineLatest, Observable, Subject} from 'rxjs'; import {map, shareReplay, take, takeUntil} from 'rxjs/operators'; +import {MapMarker} from '../map-marker/index'; + interface GoogleMapsWindow extends Window { google?: typeof google; } @@ -44,9 +49,9 @@ export const DEFAULT_WIDTH = '500px'; @Component({ selector: 'google-map', changeDetection: ChangeDetectionStrategy.OnPush, - template: '
', + template: '
', }) -export class GoogleMap implements OnChanges, OnInit, OnDestroy { +export class GoogleMap implements OnChanges, OnInit, AfterContentInit, OnDestroy { @Input() height = DEFAULT_HEIGHT; @Input() width = DEFAULT_WIDTH; @@ -172,9 +177,13 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy { */ @Output() zoomChanged = new EventEmitter(); + @ContentChildren(MapMarker) _markers: QueryList; + private _mapEl: HTMLElement; private _googleMap!: UpdatedGoogleMap; + private _googleMapChanges!: Observable; + private readonly _listeners: google.maps.MapsEventListener[] = []; private readonly _options = new BehaviorSubject(DEFAULT_OPTIONS); @@ -204,13 +213,21 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy { const combinedOptionsChanges = this._combineOptions(); - const googleMapChanges = this._initializeMap(combinedOptionsChanges); - googleMapChanges.subscribe((googleMap: google.maps.Map) => { + this._googleMapChanges = this._initializeMap(combinedOptionsChanges); + this._googleMapChanges.subscribe((googleMap: google.maps.Map) => { this._googleMap = googleMap as UpdatedGoogleMap; + this._initializeEventHandlers(); }); - this._watchForOptionsChanges(googleMapChanges, combinedOptionsChanges); + this._watchForOptionsChanges(combinedOptionsChanges); + } + + ngAfterContentInit() { + for (const marker of this._markers.toArray()) { + marker._setMap(this._googleMap); + } + this._watchForMarkerChanges(); } ngOnDestroy() { @@ -391,9 +408,8 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy { } private _watchForOptionsChanges( - googleMapChanges: Observable, optionsChanges: Observable) { - combineLatest(googleMapChanges, optionsChanges) + combineLatest(this._googleMapChanges, optionsChanges) .pipe(takeUntil(this._destroy)) .subscribe(([googleMap, options]) => { googleMap.setOptions(options); @@ -445,4 +461,14 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy { })); } } + + private _watchForMarkerChanges() { + combineLatest(this._googleMapChanges, this._markers.changes) + .pipe(takeUntil(this._destroy)) + .subscribe(([googleMap, markers]) => { + for (let marker of markers) { + marker._setMap(googleMap); + } + }); + } } diff --git a/src/google-maps/google-map/index.ts b/src/google-maps/google-map/index.ts index 676ca90f1ffa..34b16fbeddf7 100644 --- a/src/google-maps/google-map/index.ts +++ b/src/google-maps/google-map/index.ts @@ -6,4 +6,5 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './public-api'; +export * from './google-map'; +export * from './google-map-module'; diff --git a/src/google-maps/google-map/testing/fake-google-map-utils.ts b/src/google-maps/google-map/testing/fake-google-map-utils.ts deleted file mode 100644 index ec25af092404..000000000000 --- a/src/google-maps/google-map/testing/fake-google-map-utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {UpdatedGoogleMap} from '../index'; - -/** Window interface for testing */ -export interface TestingWindow extends Window { - google?: { - maps: { - Map: jasmine.Spy; - }; - }; -} - -/** Creates a jasmine.SpyObj for a google.maps.Map. */ -export function createMapSpy(options: google.maps.MapOptions): jasmine.SpyObj { - const mapSpy = jasmine.createSpyObj('google.maps.Map', [ - 'setOptions', 'addListener', 'fitBounds', 'panBy', 'panTo', 'panToBounds', 'getBounds', - 'getCenter', 'getClickableIcons', 'getHeading', 'getMapTypeId', 'getProjection', - 'getStreetView', 'getTilt', 'getZoom' - ]); - mapSpy.addListener.and.returnValue({remove: () => {}}); - return mapSpy; -} - -/** Creates a jasmine.Spy to watch for the constructor of a google.maps.Map. */ -export function createMapConstructorSpy( - mapSpy: jasmine.SpyObj, apiLoaded = true): jasmine.Spy { - const mapConstructorSpy = - jasmine.createSpy('Map constructor', (_el: Element, _options: google.maps.MapOptions) => { - return mapSpy; - }); - const testingWindow: TestingWindow = window; - if (apiLoaded) { - testingWindow.google = { - maps: { - 'Map': mapConstructorSpy, - } - }; - } - return mapConstructorSpy; -} diff --git a/src/google-maps/google-map/tsconfig-build.json b/src/google-maps/google-map/tsconfig-build.json deleted file mode 100644 index 5204b9ba30de..000000000000 --- a/src/google-maps/google-map/tsconfig-build.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../tsconfig-build", - "files": [ - "public-api.ts", - "../typings.d.ts" - ], - "angularCompilerOptions": { - "annotateForClosureCompiler": true, - "strictMetadataEmit": true, - "flatModuleOutFile": "index.js", - "flatModuleId": "@angular/google-maps/google-map", - "skipTemplateCodegen": true, - "fullTemplateTypeCheck": true - } -} diff --git a/src/google-maps/google-maps-module.ts b/src/google-maps/google-maps-module.ts new file mode 100644 index 000000000000..d816f00546ad --- /dev/null +++ b/src/google-maps/google-maps-module.ts @@ -0,0 +1,25 @@ +/** + * @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 {MapMarker, MapMarkerModule} from './map-marker/index'; +import {GoogleMap, GoogleMapModule} from './google-map/index'; + +@NgModule({ + imports: [ + GoogleMapModule, + MapMarkerModule, + ], + exports: [ + GoogleMap, + MapMarker, + ], +}) +export class GoogleMapsModule { +} diff --git a/src/google-maps/map-marker/BUILD.bazel b/src/google-maps/map-marker/BUILD.bazel new file mode 100644 index 000000000000..65d075a3dfe0 --- /dev/null +++ b/src/google-maps/map-marker/BUILD.bazel @@ -0,0 +1,44 @@ +package(default_visibility = ["//visibility:public"]) + +load( + "//tools:defaults.bzl", + "ng_module", + "ng_test_library", + "ng_web_test_suite", +) + +ng_module( + name = "map-marker", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + deps = [ + "@npm//@angular/core", + "@npm//@types/googlemaps", + "@npm//rxjs", + ], +) + +ng_test_library( + name = "unit_test_sources", + srcs = glob( + ["**/*.spec.ts"], + exclude = ["**/*.e2e.spec.ts"], + ), + deps = [ + ":map-marker", + "//src/google-maps/testing", + "@npm//@angular/platform-browser", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [":unit_test_sources"], +) + +filegroup( + name = "source-files", + srcs = glob(["**/*.ts"]), +) diff --git a/src/google-maps/google-map/public-api.ts b/src/google-maps/map-marker/index.ts similarity index 75% rename from src/google-maps/google-map/public-api.ts rename to src/google-maps/map-marker/index.ts index 8e9058eaa1d7..6e68afdc7e09 100644 --- a/src/google-maps/google-map/public-api.ts +++ b/src/google-maps/map-marker/index.ts @@ -6,5 +6,5 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './google-map-module'; -export * from './google-map'; +export * from './map-marker'; +export * from './map-marker-module'; diff --git a/src/google-maps/map-marker/map-marker-module.ts b/src/google-maps/map-marker/map-marker-module.ts new file mode 100644 index 000000000000..6752cd28eae8 --- /dev/null +++ b/src/google-maps/map-marker/map-marker-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 {MapMarker} from './map-marker'; + +@NgModule({ + exports: [MapMarker], + declarations: [MapMarker], +}) +export class MapMarkerModule { +} diff --git a/src/google-maps/map-marker/map-marker.spec.ts b/src/google-maps/map-marker/map-marker.spec.ts new file mode 100644 index 000000000000..047fe764b02f --- /dev/null +++ b/src/google-maps/map-marker/map-marker.spec.ts @@ -0,0 +1,254 @@ +import {Component} from '@angular/core'; +import {async, TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; + +import { + createMarkerConstructorSpy, + createMarkerSpy, + TestingWindow +} from '../testing/fake-google-map-utils'; + +import {DEFAULT_MARKER_OPTIONS, MapMarker, MapMarkerModule} from './index'; + +describe('MapMarker', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [MapMarkerModule], + declarations: [TestApp], + }); + })); + + beforeEach(() => { + TestBed.compileComponents(); + }); + + afterEach(() => { + const testingWindow: TestingWindow = window; + delete testingWindow.google; + }); + + it('initializes a Google Map marker', () => { + const markerSpy = createMarkerSpy(DEFAULT_MARKER_OPTIONS); + const markerConstructorSpy = createMarkerConstructorSpy(markerSpy).and.callThrough(); + + const fixture = TestBed.createComponent(TestApp); + const fakeMap = {} as unknown as google.maps.Map; + const markerComponent = fixture.debugElement.query(By.directive(MapMarker)).componentInstance; + markerComponent._setMap(fakeMap); + fixture.detectChanges(); + + expect(markerConstructorSpy).toHaveBeenCalledWith({ + ...DEFAULT_MARKER_OPTIONS, + title: undefined, + label: undefined, + clickable: undefined, + map: fakeMap + }); + }); + + it('sets marker inputs', () => { + const fakeMap = {} as unknown as google.maps.Map; + const options: google.maps.MarkerOptions = { + position: {lat: 3, lng: 5}, + title: 'marker title', + label: 'marker label', + clickable: false, + map: fakeMap, + }; + const markerSpy = createMarkerSpy(options); + const markerConstructorSpy = createMarkerConstructorSpy(markerSpy).and.callThrough(); + + const fixture = TestBed.createComponent(TestApp); + fixture.componentInstance.position = options.position; + fixture.componentInstance.title = options.title; + fixture.componentInstance.label = options.label; + fixture.componentInstance.clickable = options.clickable; + const markerComponent = fixture.debugElement.query(By.directive(MapMarker)).componentInstance; + markerComponent._setMap(fakeMap); + fixture.detectChanges(); + + expect(markerConstructorSpy).toHaveBeenCalledWith(options); + }); + + it('sets marker options, ignoring map', () => { + const fakeMap = {} as unknown as google.maps.Map; + const options: google.maps.MarkerOptions = { + position: {lat: 3, lng: 5}, + title: 'marker title', + label: 'marker label', + clickable: false, + icon: 'icon name', + }; + const markerSpy = createMarkerSpy(options); + const markerConstructorSpy = createMarkerConstructorSpy(markerSpy).and.callThrough(); + + const fixture = TestBed.createComponent(TestApp); + fixture.componentInstance.options = options; + const markerComponent = fixture.debugElement.query(By.directive(MapMarker)).componentInstance; + markerComponent._setMap(fakeMap); + fixture.detectChanges(); + + expect(markerConstructorSpy).toHaveBeenCalledWith({...options, map: fakeMap}); + }); + + it('gives precedence to specific inputs over options', () => { + const fakeMap = {} as unknown as google.maps.Map; + const options: google.maps.MarkerOptions = { + position: {lat: 3, lng: 5}, + title: 'marker title', + label: 'marker label', + clickable: false, + icon: 'icon name', + }; + const expectedOptions: google.maps.MarkerOptions = { + position: {lat: 5, lng: 12}, + title: 'updated title', + label: 'updated label', + clickable: true, + icon: 'icon name', + map: fakeMap, + }; + const markerSpy = createMarkerSpy(options); + const markerConstructorSpy = createMarkerConstructorSpy(markerSpy).and.callThrough(); + + const fixture = TestBed.createComponent(TestApp); + fixture.componentInstance.position = expectedOptions.position; + fixture.componentInstance.title = expectedOptions.title; + fixture.componentInstance.label = expectedOptions.label; + fixture.componentInstance.clickable = expectedOptions.clickable; + fixture.componentInstance.options = options; + const markerComponent = fixture.debugElement.query(By.directive(MapMarker)).componentInstance; + markerComponent._setMap(fakeMap); + fixture.detectChanges(); + + expect(markerConstructorSpy).toHaveBeenCalledWith(expectedOptions); + }); + + it('sets the map on the marker only once', () => { + const markerSpy = createMarkerSpy(DEFAULT_MARKER_OPTIONS); + const markerConstructorSpy = createMarkerConstructorSpy(markerSpy).and.callThrough(); + + const fixture = TestBed.createComponent(TestApp); + const fakeMap = {} as unknown as google.maps.Map; + const fakeMap2 = {testValue: 'test'} as unknown as google.maps.Map; + const markerComponent = fixture.debugElement.query(By.directive(MapMarker)).componentInstance; + markerComponent._setMap(fakeMap); + markerComponent._setMap(fakeMap2); + fixture.detectChanges(); + + expect(markerConstructorSpy).toHaveBeenCalledWith({ + ...DEFAULT_MARKER_OPTIONS, + title: undefined, + label: undefined, + clickable: undefined, + map: fakeMap + }); + expect(markerSpy.setOptions).not.toHaveBeenCalled(); + }); + + it('exposes methods that provide information about the marker', () => { + const markerSpy = createMarkerSpy(DEFAULT_MARKER_OPTIONS); + createMarkerConstructorSpy(markerSpy).and.callThrough(); + + const fixture = TestBed.createComponent(TestApp); + const fakeMap = {} as unknown as google.maps.Map; + const markerComponent = fixture.debugElement.query(By.directive(MapMarker)).componentInstance; + markerComponent._setMap(fakeMap); + fixture.detectChanges(); + + markerSpy.getAnimation.and.returnValue(null); + expect(markerComponent.getAnimation()).toBe(null); + + markerSpy.getClickable.and.returnValue(true); + expect(markerComponent.getClickable()).toBe(true); + + markerSpy.getCursor.and.returnValue('cursor'); + expect(markerComponent.getCursor()).toBe('cursor'); + + markerSpy.getDraggable.and.returnValue(true); + expect(markerComponent.getDraggable()).toBe(true); + + markerSpy.getIcon.and.returnValue('icon'); + expect(markerComponent.getIcon()).toBe('icon'); + + markerSpy.getLabel.and.returnValue(null); + expect(markerComponent.getLabel()).toBe(null); + + markerSpy.getOpacity.and.returnValue(5); + expect(markerComponent.getOpacity()).toBe(5); + + markerSpy.getPosition.and.returnValue(null); + expect(markerComponent.getPosition()).toEqual(null); + + markerSpy.getShape.and.returnValue(null); + expect(markerComponent.getShape()).toBe(null); + + markerSpy.getTitle.and.returnValue('title'); + expect(markerComponent.getTitle()).toBe('title'); + + markerSpy.getVisible.and.returnValue(true); + expect(markerComponent.getVisible()).toBe(true); + + markerSpy.getZIndex.and.returnValue(2); + expect(markerComponent.getZIndex()).toBe(2); + }); + + it('initializes marker event handlers', () => { + const markerSpy = createMarkerSpy(DEFAULT_MARKER_OPTIONS); + createMarkerConstructorSpy(markerSpy).and.callThrough(); + + const fixture = TestBed.createComponent(TestApp); + const fakeMap = {} as unknown as google.maps.Map; + const markerComponent = fixture.debugElement.query(By.directive(MapMarker)).componentInstance; + markerComponent._setMap(fakeMap); + fixture.detectChanges(); + + expect(markerSpy.addListener) + .not.toHaveBeenCalledWith('animation_changed', jasmine.any(Function)); + expect(markerSpy.addListener).toHaveBeenCalledWith('click', jasmine.any(Function)); + expect(markerSpy.addListener) + .not.toHaveBeenCalledWith('clickable_changed', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('cursor_changed', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('dblclick', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('drag', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('dragend', jasmine.any(Function)); + expect(markerSpy.addListener) + .not.toHaveBeenCalledWith('draggable_changed', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('dragstart', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('flat_changed', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('icon_changed', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('mousedown', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('mouseout', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('mouseover', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('mouseup', jasmine.any(Function)); + expect(markerSpy.addListener).toHaveBeenCalledWith('position_changed', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('rightclick', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('shape_changed', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('title_changed', jasmine.any(Function)); + expect(markerSpy.addListener) + .not.toHaveBeenCalledWith('visible_changed', jasmine.any(Function)); + expect(markerSpy.addListener).not.toHaveBeenCalledWith('zindex_changed', jasmine.any(Function)); + }); +}); + +@Component({ + selector: 'test-app', + template: ``, +}) +class TestApp { + title?: string; + position?: google.maps.LatLng|google.maps.LatLngLiteral; + label?: string|google.maps.MarkerLabel; + clickable?: boolean; + options?: google.maps.MarkerOptions; + + handleClick() {} + + handlePositionChanged() {} +} diff --git a/src/google-maps/map-marker/map-marker.ts b/src/google-maps/map-marker/map-marker.ts new file mode 100644 index 000000000000..6117481e9caf --- /dev/null +++ b/src/google-maps/map-marker/map-marker.ts @@ -0,0 +1,389 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output +} from '@angular/core'; +import {BehaviorSubject, combineLatest, Observable, ReplaySubject, Subject} from 'rxjs'; +import {map, takeUntil} from 'rxjs/operators'; + +/** + * Default options for the Google Maps marker component. Displays a marker + * at the Googleplex. + */ +export const DEFAULT_MARKER_OPTIONS = { + position: {lat: 37.421995, lng: -122.084092}, +}; + +/** + * Angular component that renders a Google Maps marker via the Google Maps JavaScript API. + * @see developers.google.com/maps/documentation/javascript/reference/marker + */ +@Component({ + selector: 'map-marker', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MapMarker implements OnInit, OnDestroy { + @Input() + set options(options: google.maps.MarkerOptions) { + this._options.next(options || DEFAULT_MARKER_OPTIONS); + } + + @Input() + set title(title: string) { + this._title.next(title); + } + + @Input() + set position(position: google.maps.LatLngLiteral) { + this._position.next(position); + } + + @Input() + set label(label: string|google.maps.MarkerLabel) { + this._label.next(label); + } + + @Input() + set clickable(clickable: boolean) { + this._clickable.next(clickable); + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.animation_changed + */ + @Output() animationChanged = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.click + */ + @Output() mapClick = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.clickable_changed + */ + @Output() clickableChanged = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.cursor_changed + */ + @Output() cursorChanged = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.dblclick + */ + @Output() mapDblclick = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.drag + */ + @Output() mapDrag = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.dragend + */ + @Output() mapDragend = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.draggable_changed + */ + @Output() draggableChanged = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.dragstart + */ + @Output() mapDragstart = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.flat_changed + */ + @Output() flatChanged = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.icon_changed + */ + @Output() iconChanged = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.mousedown + */ + @Output() mapMousedown = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.mouseout + */ + @Output() mapMouseout = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.mouseover + */ + @Output() mapMouseover = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.mouseup + */ + @Output() mapMouseup = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.position_changed + */ + @Output() positionChanged = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.rightclick + */ + @Output() mapRightclick = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.shape_changed + */ + @Output() shapeChanged = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.title_changed + */ + @Output() titleChanged = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.visible_changed + */ + @Output() visibleChanged = new EventEmitter(); + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.zindex_changed + */ + @Output() zindexChanged = new EventEmitter(); + + private readonly _options = + new BehaviorSubject(DEFAULT_MARKER_OPTIONS); + private readonly _title = new BehaviorSubject(undefined); + private readonly _position = new BehaviorSubject(undefined); + private readonly _label = + new BehaviorSubject(undefined); + private readonly _clickable = new BehaviorSubject(undefined); + + private readonly _map = new ReplaySubject(1); + + private readonly _destroy = new Subject(); + + private readonly _listeners: google.maps.MapsEventListener[] = []; + + private _marker?: google.maps.Marker; + private _hasMap = false; + + ngOnInit() { + const combinedOptionsChanges = this._combineOptions(); + + combineLatest(this._map, combinedOptionsChanges) + .pipe(takeUntil(this._destroy)) + .subscribe(([googleMap, options]) => { + if (this._marker) { + this._marker.setOptions(options); + } else { + this._marker = new google.maps.Marker(options); + this._marker.setMap(googleMap); + this._initializeEventHandlers(); + } + }); + } + + ngOnDestroy() { + this._destroy.next(); + this._destroy.complete(); + for (let listener of this._listeners) { + listener.remove(); + } + if (this._marker) { + this._marker.setMap(null); + } + } + + _setMap(googleMap: google.maps.Map) { + if (!this._hasMap) { + this._map.next(googleMap); + this._hasMap = true; + } + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getAnimation + */ + getAnimation(): google.maps.Animation|null { + return this._marker!.getAnimation() || null; + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getClickable + */ + getClickable(): boolean { + return this._marker!.getClickable(); + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getCursor + */ + getCursor(): string|null { + return this._marker!.getCursor() || null; + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getDraggable + */ + getDraggable(): boolean { + return !!this._marker!.getDraggable(); + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getIcon + */ + getIcon(): string|google.maps.Icon|google.maps.Symbol|null { + return this._marker!.getIcon() || null; + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getLabel + */ + getLabel(): google.maps.MarkerLabel|null { + return this._marker!.getLabel() || null; + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getOpacity + */ + getOpacity(): number|null { + return this._marker!.getOpacity() || null; + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getPosition + */ + getPosition(): google.maps.LatLng|null { + return this._marker!.getPosition() || null; + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getShape + */ + getShape(): google.maps.MarkerShape|null { + return this._marker!.getShape() || null; + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getTitle + */ + getTitle(): string|null { + return this._marker!.getTitle() || null; + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getVisible + */ + getVisible(): boolean { + return this._marker!.getVisible(); + } + + /** + * See + * developers.google.com/maps/documentation/javascript/reference/marker#Marker.getZIndex + */ + getZIndex(): number|null { + return this._marker!.getZIndex() || null; + } + + private _combineOptions(): Observable { + return combineLatest( + this._options, this._title, this._position, this._label, this._clickable, this._map) + .pipe(map(([options, title, position, label, clickable, googleMap]) => { + const combinedOptions: google.maps.MarkerOptions = { + ...options, + title: title || options.title, + position: position || options.position, + label: label || options.label, + clickable: clickable !== undefined ? clickable : options.clickable, + map: googleMap || null, + }; + return combinedOptions; + })); + } + + private _initializeEventHandlers() { + const eventHandlers = new Map>([ + ['animation_changed', this.animationChanged], + ['clickable_changed', this.clickableChanged], + ['cursor_changed', this.cursorChanged], + ['draggable_changed', this.draggableChanged], + ['flat_changed', this.flatChanged], + ['icon_changed', this.iconChanged], + ['position_changed', this.positionChanged], + ['shape_changed', this.shapeChanged], + ['title_changed', this.titleChanged], + ['visible_changed', this.visibleChanged], + ['zindex_changed', this.zindexChanged], + ]); + const mouseEventHandlers = new Map>([ + ['click', this.mapClick], + ['dblclick', this.mapDblclick], + ['drag', this.mapDrag], + ['dragend', this.mapDragend], + ['dragstart', this.mapDragstart], + ['mousedown', this.mapMousedown], + ['mouseout', this.mapMouseout], + ['mouseover', this.mapMouseover], + ['mouseup', this.mapMouseup], + ['rightclick', this.mapRightclick], + ]); + + eventHandlers.forEach((eventHandler: EventEmitter, name: string) => { + if (eventHandler.observers.length > 0) { + this._listeners.push(this._marker!.addListener(name, () => { + eventHandler.emit(); + })); + } + }); + mouseEventHandlers.forEach( + (eventHandler: EventEmitter, name: string) => { + if (eventHandler.observers.length > 0) { + this._listeners.push( + this._marker!.addListener(name, (event: google.maps.MouseEvent) => { + eventHandler.emit(event); + })); + } + }); + } +} diff --git a/src/google-maps/public-api.ts b/src/google-maps/public-api.ts index 9d11682064d7..bd57b5427d97 100644 --- a/src/google-maps/public-api.ts +++ b/src/google-maps/public-api.ts @@ -6,4 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -export * from '@angular/google-maps/google-map'; +export {GoogleMap} from './google-map/index'; +export {MapMarker} from './map-marker/index'; +export * from './google-maps-module'; diff --git a/src/google-maps/google-map/testing/BUILD.bazel b/src/google-maps/testing/BUILD.bazel similarity index 100% rename from src/google-maps/google-map/testing/BUILD.bazel rename to src/google-maps/testing/BUILD.bazel diff --git a/src/google-maps/testing/fake-google-map-utils.ts b/src/google-maps/testing/fake-google-map-utils.ts new file mode 100644 index 000000000000..6d09a5fe3239 --- /dev/null +++ b/src/google-maps/testing/fake-google-map-utils.ts @@ -0,0 +1,68 @@ +import {UpdatedGoogleMap} from '../google-map/index'; + +/** Window interface for testing */ +export interface TestingWindow extends Window { + google?: { + maps: { + Map?: jasmine.Spy; + Marker?: jasmine.Spy; + }; + }; +} + +/** Creates a jasmine.SpyObj for a google.maps.Map. */ +export function createMapSpy(options: google.maps.MapOptions): jasmine.SpyObj { + const mapSpy = jasmine.createSpyObj('google.maps.Map', [ + 'setOptions', 'setMap', 'addListener', 'fitBounds', 'panBy', 'panTo', 'panToBounds', + 'getBounds', 'getCenter', 'getClickableIcons', 'getHeading', 'getMapTypeId', 'getProjection', + 'getStreetView', 'getTilt', 'getZoom' + ]); + mapSpy.addListener.and.returnValue({remove: () => {}}); + return mapSpy; +} + +/** Creates a jasmine.Spy to watch for the constructor of a google.maps.Map. */ +export function createMapConstructorSpy( + mapSpy: jasmine.SpyObj, apiLoaded = true): jasmine.Spy { + const mapConstructorSpy = + jasmine.createSpy('Map constructor', (_el: Element, _options: google.maps.MapOptions) => { + return mapSpy; + }); + const testingWindow: TestingWindow = window; + if (apiLoaded) { + testingWindow.google = { + maps: { + 'Map': mapConstructorSpy, + } + }; + } + return mapConstructorSpy; +} + +/** Creates a jasmine.SpyObj for a google.maps.Marker */ +export function createMarkerSpy(options: google.maps.MarkerOptions): + jasmine.SpyObj { + const markerSpy = jasmine.createSpyObj('google.maps.Marker', [ + 'setOptions', 'setMap', 'addListener', 'getAnimation', 'getClickable', 'getCursor', + 'getDraggable', 'getIcon', 'getLabel', 'getOpacity', 'getPosition', 'getShape', 'getTitle', + 'getVisible', 'getZIndex' + ]); + markerSpy.addListener.and.returnValue({remove: () => {}}); + return markerSpy; +} + +/** Creates a jasmine.Spy to watch for the constructor of a google.maps.Marker */ +export function createMarkerConstructorSpy(markerSpy: jasmine.SpyObj): + jasmine.Spy { + const markerConstructorSpy = + jasmine.createSpy('Marker constructor', (_options: google.maps.MarkerOptions) => { + return markerSpy; + }); + const testingWindow: TestingWindow = window; + testingWindow.google = { + maps: { + 'Marker': markerConstructorSpy, + }, + }; + return markerConstructorSpy; +} diff --git a/src/google-maps/tsconfig-build.json b/src/google-maps/tsconfig-build.json index 7b8486eb9abc..df3b99e4eef7 100644 --- a/src/google-maps/tsconfig-build.json +++ b/src/google-maps/tsconfig-build.json @@ -12,9 +12,6 @@ ".", "../../dist/packages/google-maps" ], - "paths": { - "@angular/google-maps/*": ["../../dist/packages/google-maps/*"] - }, "types": [ "googlemaps" ] diff --git a/src/google-maps/tsconfig-tests.json b/src/google-maps/tsconfig-tests.json index 713d01322f47..0f0829b28ac1 100644 --- a/src/google-maps/tsconfig-tests.json +++ b/src/google-maps/tsconfig-tests.json @@ -11,9 +11,6 @@ "jasmine", "googlemaps" ], - "paths": { - "@angular/google-maps/*": ["./*"] - } }, "angularCompilerOptions": { "strictMetadataEmit": true, diff --git a/src/google-maps/tsconfig.json b/src/google-maps/tsconfig.json index b654928fc517..31ad9496d92f 100644 --- a/src/google-maps/tsconfig.json +++ b/src/google-maps/tsconfig.json @@ -4,9 +4,7 @@ "compilerOptions": { "rootDir": "..", "baseUrl": ".", - "paths": { - "@angular/google-maps": ["./*"] - }, + "paths": {}, "types": [ "jasmine", "googlemaps"