From c478bcb1a81545dea963339d7cd6cc82094b9536 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 16 Oct 2024 11:26:52 +0200 Subject: [PATCH 1/3] feat(google-maps): deprecate marker cluster component Deprecates the existing `MapMarkerClusterer`, because it's based on a deprecated library that doesn't support advanced markers. A new clusterer component will be introduced that supports both regular markers and advanced ones, and is based on the current marker library. BREAKING CHANGE: * The new @googlemaps/markerclusterer API should be imported instead of the old one. Read more at: https://github.com/googlemaps/js-markerclusterer * The `MapMarkerClusterer` class has been renamed to `DeprecatedMapMarkerClusterer`. * The `map-marker-clusterer` selector has been changed to `deprecated-map-marker-clusterer`. --- src/dev-app/google-map/google-map-demo.html | 4 +- src/dev-app/google-map/google-map-demo.ts | 4 +- .../README.md | 15 +- .../deprecated-map-marker-clusterer.spec.ts} | 28 ++- .../deprecated-map-marker-clusterer.ts} | 14 +- .../deprecated-marker-clusterer-types.ts} | 0 src/google-maps/google-maps-module.ts | 4 +- src/google-maps/public-api.ts | 4 +- .../testing/fake-google-map-utils.ts | 24 +- .../kitchen-sink/kitchen-sink.html | 4 +- .../kitchen-sink/kitchen-sink.ts | 4 +- .../google-maps/google-maps.md | 206 +++++++++--------- 12 files changed, 162 insertions(+), 149 deletions(-) rename src/google-maps/{map-marker-clusterer => deprecated-map-marker-clusterer}/README.md (54%) rename src/google-maps/{map-marker-clusterer/map-marker-clusterer.spec.ts => deprecated-map-marker-clusterer/deprecated-map-marker-clusterer.spec.ts} (93%) rename src/google-maps/{map-marker-clusterer/map-marker-clusterer.ts => deprecated-map-marker-clusterer/deprecated-map-marker-clusterer.ts} (97%) rename src/google-maps/{map-marker-clusterer/marker-clusterer-types.ts => deprecated-map-marker-clusterer/deprecated-marker-clusterer-types.ts} (100%) diff --git a/src/dev-app/google-map/google-map-demo.html b/src/dev-app/google-map/google-map-demo.html index f157e6fa6146..e4961e0d43cc 100644 --- a/src/dev-app/google-map/google-map-demo.html +++ b/src/dev-app/google-map/google-map-demo.html @@ -10,7 +10,7 @@ (mapRightclick)="handleRightclick()" [mapTypeId]="mapTypeId" [mapId]="mapId"> - + @@ -20,7 +20,7 @@ [options]="markerOptions" (mapClick)="infoWindow.open(marker)"> } - + @if (hasAdvancedMarker) { | null = null; MapInfoWindow, MapKmlLayer, MapMarker, - MapMarkerClusterer, + DeprecatedMapMarkerClusterer, MapAdvancedMarker, MapPolygon, MapPolyline, diff --git a/src/google-maps/map-marker-clusterer/README.md b/src/google-maps/deprecated-map-marker-clusterer/README.md similarity index 54% rename from src/google-maps/map-marker-clusterer/README.md rename to src/google-maps/deprecated-map-marker-clusterer/README.md index 717b88a7e7d0..1179e5e3864e 100644 --- a/src/google-maps/map-marker-clusterer/README.md +++ b/src/google-maps/deprecated-map-marker-clusterer/README.md @@ -1,6 +1,9 @@ -#MapMarkerClusterer +# Deprecation warning ⚠️ +This component is based on the deprecated `@googlemaps/markerclustererplus` library. Use the `map-marker-clusterer` component instead. -The `MapMarkerClusterer` component wraps the [`MarkerClusterer` class](https://googlemaps.github.io/js-markerclustererplus/classes/markerclusterer.html) from the [Google Maps JavaScript MarkerClustererPlus Library](https://github.com/googlemaps/js-markerclustererplus). The `MapMarkerClusterer` component displays a cluster of markers that are children of the `` tag. Unlike the other Google Maps components, MapMarkerClusterer does not have an `options` input, so any input (listed in the [documentation](https://googlemaps.github.io/js-markerclustererplus/index.html) for the `MarkerClusterer` class) should be set directly. +## DeprecatedMapMarkerClusterer + +The `DeprecatedMapMarkerClusterer` component wraps the [`MarkerClusterer` class](https://googlemaps.github.io/js-markerclustererplus/classes/markerclusterer.html) from the [Google Maps JavaScript MarkerClustererPlus Library](https://github.com/googlemaps/js-markerclustererplus). The `DeprecatedMapMarkerClusterer` component displays a cluster of markers that are children of the `` tag. Unlike the other Google Maps components, MapMarkerClusterer does not have an `options` input, so any input (listed in the [documentation](https://googlemaps.github.io/js-markerclustererplus/index.html) for the `MarkerClusterer` class) should be set directly. ## Loading the Library @@ -17,12 +20,12 @@ Additional information can be found by looking at [Marker Clustering](https://de ```typescript // google-map-demo.component.ts import {Component} from '@angular/core'; -import {GoogleMap, MapMarker, MapMarkerClusterer} from '@angular/google-maps'; +import {GoogleMap, MapMarker, DeprecatedMapMarkerClusterer} from '@angular/google-maps'; @Component({ selector: 'google-map-demo', templateUrl: 'google-map-demo.html', - imports: [GoogleMap, MapMarker, MapMarkerClusterer], + imports: [GoogleMap, MapMarker, DeprecatedMapMarkerClusterer], }) export class GoogleMapDemo { center: google.maps.LatLngLiteral = {lat: 24, lng: 12}; @@ -45,10 +48,10 @@ export class GoogleMapDemo { [center]="center" [zoom]="zoom" (mapClick)="addMarker($event)"> - + @for (position of markerPositions; track position) { } - + ``` diff --git a/src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts b/src/google-maps/deprecated-map-marker-clusterer/deprecated-map-marker-clusterer.spec.ts similarity index 93% rename from src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts rename to src/google-maps/deprecated-map-marker-clusterer/deprecated-map-marker-clusterer.spec.ts index 132927ba0f06..9b1d297b9536 100644 --- a/src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts +++ b/src/google-maps/deprecated-map-marker-clusterer/deprecated-map-marker-clusterer.spec.ts @@ -6,21 +6,21 @@ import {MapMarker} from '../map-marker/map-marker'; import { createMapConstructorSpy, createMapSpy, - createMarkerClustererConstructorSpy, - createMarkerClustererSpy, + createDeprecatedMarkerClustererConstructorSpy, + createDeprecatedMarkerClustererSpy, createMarkerConstructorSpy, createMarkerSpy, } from '../testing/fake-google-map-utils'; -import {MapMarkerClusterer} from './map-marker-clusterer'; +import {DeprecatedMapMarkerClusterer} from './deprecated-map-marker-clusterer'; import { AriaLabelFn, Calculator, ClusterIconStyle, MarkerClusterer, MarkerClustererOptions, -} from './marker-clusterer-types'; +} from './deprecated-marker-clusterer-types'; -describe('MapMarkerClusterer', () => { +describe('DeprecatedMapMarkerClusterer', () => { let mapSpy: jasmine.SpyObj; let markerClustererSpy: jasmine.SpyObj; let markerClustererConstructorSpy: jasmine.Spy; @@ -39,8 +39,9 @@ describe('MapMarkerClusterer', () => { return createMarkerSpy({}); }); - markerClustererSpy = createMarkerClustererSpy(); - markerClustererConstructorSpy = createMarkerClustererConstructorSpy(markerClustererSpy); + markerClustererSpy = createDeprecatedMarkerClustererSpy(); + markerClustererConstructorSpy = + createDeprecatedMarkerClustererConstructorSpy(markerClustererSpy); fixture = TestBed.createComponent(TestApp); }); @@ -52,7 +53,10 @@ describe('MapMarkerClusterer', () => { it('throws an error if the clustering library has not been loaded', fakeAsync(() => { (window as any).MarkerClusterer = undefined; - markerClustererConstructorSpy = createMarkerClustererConstructorSpy(markerClustererSpy, false); + markerClustererConstructorSpy = createDeprecatedMarkerClustererConstructorSpy( + markerClustererSpy, + false, + ); expect(() => { fixture.detectChanges(); @@ -304,7 +308,7 @@ describe('MapMarkerClusterer', () => { selector: 'test-app', template: ` - { @if (state === 'state2') { } - + `, standalone: true, - imports: [GoogleMap, MapMarker, MapMarkerClusterer], + imports: [GoogleMap, MapMarker, DeprecatedMapMarkerClusterer], }) class TestApp { - @ViewChild(MapMarkerClusterer) markerClusterer: MapMarkerClusterer; + @ViewChild(DeprecatedMapMarkerClusterer) markerClusterer: DeprecatedMapMarkerClusterer; ariaLabelFn?: AriaLabelFn; averageCenter?: boolean; diff --git a/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts b/src/google-maps/deprecated-map-marker-clusterer/deprecated-map-marker-clusterer.ts similarity index 97% rename from src/google-maps/map-marker-clusterer/map-marker-clusterer.ts rename to src/google-maps/deprecated-map-marker-clusterer/deprecated-map-marker-clusterer.ts index 78d2130527d4..3c84386e5750 100644 --- a/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts +++ b/src/google-maps/deprecated-map-marker-clusterer/deprecated-map-marker-clusterer.ts @@ -39,7 +39,7 @@ import { ClusterIconStyle, MarkerClusterer as MarkerClustererInstance, MarkerClustererOptions, -} from './marker-clusterer-types'; +} from './deprecated-marker-clusterer-types'; /** Default options for a clusterer. */ const DEFAULT_CLUSTERER_OPTIONS: MarkerClustererOptions = {}; @@ -52,17 +52,23 @@ declare const MarkerClusterer: typeof MarkerClustererInstance; /** * Angular component for implementing a Google Maps Marker Clusterer. - * * See https://developers.google.com/maps/documentation/javascript/marker-clustering + * + * @deprecated This component is using a deprecated clustering implementation. Use the + * `map-marker-clusterer` component instead. + * @breaking-change 21.0.0 + * */ @Component({ - selector: 'map-marker-clusterer', + selector: 'deprecated-map-marker-clusterer', exportAs: 'mapMarkerClusterer', changeDetection: ChangeDetectionStrategy.OnPush, template: '', encapsulation: ViewEncapsulation.None, }) -export class MapMarkerClusterer implements OnInit, AfterContentInit, OnChanges, OnDestroy { +export class DeprecatedMapMarkerClusterer + implements OnInit, AfterContentInit, OnChanges, OnDestroy +{ private readonly _googleMap = inject(GoogleMap); private readonly _ngZone = inject(NgZone); private readonly _currentMarkers = new Set(); diff --git a/src/google-maps/map-marker-clusterer/marker-clusterer-types.ts b/src/google-maps/deprecated-map-marker-clusterer/deprecated-marker-clusterer-types.ts similarity index 100% rename from src/google-maps/map-marker-clusterer/marker-clusterer-types.ts rename to src/google-maps/deprecated-map-marker-clusterer/deprecated-marker-clusterer-types.ts diff --git a/src/google-maps/google-maps-module.ts b/src/google-maps/google-maps-module.ts index bd82f7eecc45..c373e61fd088 100644 --- a/src/google-maps/google-maps-module.ts +++ b/src/google-maps/google-maps-module.ts @@ -17,7 +17,7 @@ import {MapGroundOverlay} from './map-ground-overlay/map-ground-overlay'; import {MapInfoWindow} from './map-info-window/map-info-window'; import {MapKmlLayer} from './map-kml-layer/map-kml-layer'; import {MapMarker} from './map-marker/map-marker'; -import {MapMarkerClusterer} from './map-marker-clusterer/map-marker-clusterer'; +import {DeprecatedMapMarkerClusterer} from './deprecated-map-marker-clusterer/deprecated-map-marker-clusterer'; import {MapPolygon} from './map-polygon/map-polygon'; import {MapPolyline} from './map-polyline/map-polyline'; import {MapRectangle} from './map-rectangle/map-rectangle'; @@ -38,7 +38,7 @@ const COMPONENTS = [ MapKmlLayer, MapMarker, MapAdvancedMarker, - MapMarkerClusterer, + DeprecatedMapMarkerClusterer, MapPolygon, MapPolyline, MapRectangle, diff --git a/src/google-maps/public-api.ts b/src/google-maps/public-api.ts index ef7f2e3ff36c..b83f5cfd7587 100644 --- a/src/google-maps/public-api.ts +++ b/src/google-maps/public-api.ts @@ -22,7 +22,7 @@ export {MapInfoWindow} from './map-info-window/map-info-window'; export {MapKmlLayer} from './map-kml-layer/map-kml-layer'; export {MapMarker} from './map-marker/map-marker'; export {MapAdvancedMarker} from './map-advanced-marker/map-advanced-marker'; -export {MapMarkerClusterer} from './map-marker-clusterer/map-marker-clusterer'; +export {DeprecatedMapMarkerClusterer} from './deprecated-map-marker-clusterer/deprecated-map-marker-clusterer'; export {MapPolygon} from './map-polygon/map-polygon'; export {MapPolyline} from './map-polyline/map-polyline'; export {MapRectangle} from './map-rectangle/map-rectangle'; @@ -35,5 +35,5 @@ export { ClusterIconStyle, AriaLabelFn, Calculator, -} from './map-marker-clusterer/marker-clusterer-types'; +} from './deprecated-map-marker-clusterer/deprecated-marker-clusterer-types'; export {MapEventManager} from './map-event-manager'; diff --git a/src/google-maps/testing/fake-google-map-utils.ts b/src/google-maps/testing/fake-google-map-utils.ts index 0f7f59fbdf00..1e8f5f293798 100644 --- a/src/google-maps/testing/fake-google-map-utils.ts +++ b/src/google-maps/testing/fake-google-map-utils.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {MarkerClusterer} from '../map-marker-clusterer/marker-clusterer-types'; +import {MarkerClusterer as DeprecatedMarkerClusterer} from '../deprecated-map-marker-clusterer/deprecated-marker-clusterer-types'; // The global `window` variable is typed as an intersection of `Window` and `globalThis`. // We re-declare `window` here and omit `globalThis` as it is typed with the actual Google @@ -574,8 +574,8 @@ export function createBicyclingLayerConstructorSpy( } /** Creates a jasmine.SpyObj for a MarkerClusterer */ -export function createMarkerClustererSpy(): jasmine.SpyObj { - const markerClustererSpy = jasmine.createSpyObj('MarkerClusterer', [ +export function createDeprecatedMarkerClustererSpy(): jasmine.SpyObj { + const deprecatedMarkerClustererSpy = jasmine.createSpyObj('DeprecatedMarkerClusterer', [ 'addListener', 'addMarkers', 'fitMapToMarkers', @@ -619,26 +619,26 @@ export function createMarkerClustererSpy(): jasmine.SpyObj { 'setZoomOnClick', 'setOptions', ]); - markerClustererSpy.addListener.and.returnValue({remove: () => {}}); - return markerClustererSpy; + deprecatedMarkerClustererSpy.addListener.and.returnValue({remove: () => {}}); + return deprecatedMarkerClustererSpy; } /** Creates a jasmine.Spy to watch for the constructor of a MarkerClusterer */ -export function createMarkerClustererConstructorSpy( - markerClustererSpy: jasmine.SpyObj, +export function createDeprecatedMarkerClustererConstructorSpy( + deprecatedMarkerClustererSpy: jasmine.SpyObj, 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 () { - return markerClustererSpy; + const deprecatedMarkerClustererConstructorSpy = jasmine + .createSpy('DeprecatedMarkerClusterer constructor', function () { + return deprecatedMarkerClustererSpy; }) .and.callThrough(); if (apiLoaded) { const testingWindow: TestingWindow = window; - testingWindow['MarkerClusterer'] = markerClustererConstructorSpy; + testingWindow['MarkerClusterer'] = deprecatedMarkerClustererConstructorSpy; } - return markerClustererConstructorSpy; + return deprecatedMarkerClustererConstructorSpy; } /** Creates a jasmine.SpyObj for DirectionsRenderer */ diff --git a/src/universal-app/kitchen-sink/kitchen-sink.html b/src/universal-app/kitchen-sink/kitchen-sink.html index 793698ed6df2..52cfff367412 100644 --- a/src/universal-app/kitchen-sink/kitchen-sink.html +++ b/src/universal-app/kitchen-sink/kitchen-sink.html @@ -641,13 +641,13 @@

Google Map

]" > - - +

Popover edit

diff --git a/src/universal-app/kitchen-sink/kitchen-sink.ts b/src/universal-app/kitchen-sink/kitchen-sink.ts index 20317b2f0b14..f4050a3d4a16 100644 --- a/src/universal-app/kitchen-sink/kitchen-sink.ts +++ b/src/universal-app/kitchen-sink/kitchen-sink.ts @@ -16,7 +16,7 @@ import { MapInfoWindow, MapKmlLayer, MapMarker, - MapMarkerClusterer, + DeprecatedMapMarkerClusterer, MapPolygon, MapPolyline, MapRectangle, @@ -196,7 +196,7 @@ export class TestEntryComponent {} MapKmlLayer, MapMarker, MapAdvancedMarker, - MapMarkerClusterer, + DeprecatedMapMarkerClusterer, MapPolygon, MapPolyline, MapRectangle, diff --git a/tools/public_api_guard/google-maps/google-maps.md b/tools/public_api_guard/google-maps/google-maps.md index a49299bbaa01..b65c4afdb221 100644 --- a/tools/public_api_guard/google-maps/google-maps.md +++ b/tools/public_api_guard/google-maps/google-maps.md @@ -55,6 +55,108 @@ export interface ClusterIconStyle { width: number; } +// @public @deprecated +export class DeprecatedMapMarkerClusterer implements OnInit, AfterContentInit, OnChanges, OnDestroy { + constructor(...args: unknown[]); + // (undocumented) + ariaLabelFn: AriaLabelFn; + // (undocumented) + set averageCenter(averageCenter: boolean); + // (undocumented) + batchSize?: number; + // (undocumented) + set batchSizeIE(batchSizeIE: number); + // (undocumented) + set calculator(calculator: Calculator); + // (undocumented) + set clusterClass(clusterClass: string); + readonly clusterClick: Observable; + readonly clusteringbegin: Observable; + readonly clusteringend: Observable; + // (undocumented) + set enableRetinaIcons(enableRetinaIcons: boolean); + // (undocumented) + fitMapToMarkers(padding: number | google.maps.Padding): void; + // (undocumented) + getAverageCenter(): boolean; + // (undocumented) + getBatchSizeIE(): number; + // (undocumented) + getCalculator(): Calculator; + // (undocumented) + getClusterClass(): string; + // (undocumented) + getClusters(): Cluster[]; + // (undocumented) + getEnableRetinaIcons(): boolean; + // (undocumented) + getGridSize(): number; + // (undocumented) + getIgnoreHidden(): boolean; + // (undocumented) + getImageExtension(): string; + // (undocumented) + getImagePath(): string; + // (undocumented) + getImageSizes(): number[]; + // (undocumented) + getMaxZoom(): number; + // (undocumented) + getMinimumClusterSize(): number; + // (undocumented) + getStyles(): ClusterIconStyle[]; + // (undocumented) + getTitle(): string; + // (undocumented) + getTotalClusters(): number; + // (undocumented) + getTotalMarkers(): number; + // (undocumented) + getZIndex(): number; + // (undocumented) + getZoomOnClick(): boolean; + // (undocumented) + set gridSize(gridSize: number); + // (undocumented) + set ignoreHidden(ignoreHidden: boolean); + // (undocumented) + set imageExtension(imageExtension: string); + // (undocumented) + set imagePath(imagePath: string); + // (undocumented) + set imageSizes(imageSizes: number[]); + markerClusterer?: MarkerClusterer; + readonly markerClustererInitialized: EventEmitter; + // (undocumented) + _markers: QueryList; + // (undocumented) + set maxZoom(maxZoom: number); + // (undocumented) + set minimumClusterSize(minimumClusterSize: number); + // (undocumented) + ngAfterContentInit(): void; + // (undocumented) + ngOnChanges(changes: SimpleChanges): void; + // (undocumented) + ngOnDestroy(): void; + // (undocumented) + ngOnInit(): void; + // (undocumented) + set options(options: MarkerClustererOptions); + // (undocumented) + set styles(styles: ClusterIconStyle[]); + // (undocumented) + set title(title: string); + // (undocumented) + set zIndex(zIndex: number); + // (undocumented) + set zoomOnClick(zoomOnClick: boolean); + // (undocumented) + static ɵcmp: i0.ɵɵComponentDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + // @public export class GoogleMap implements OnChanges, OnInit, OnDestroy { constructor(...args: unknown[]); @@ -127,7 +229,7 @@ export class GoogleMapsModule { // (undocumented) static ɵinj: i0.ɵɵInjectorDeclaration; // (undocumented) - static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; } // @public @@ -498,108 +600,6 @@ export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { static ɵfac: i0.ɵɵFactoryDeclaration; } -// @public -export class MapMarkerClusterer implements OnInit, AfterContentInit, OnChanges, OnDestroy { - constructor(...args: unknown[]); - // (undocumented) - ariaLabelFn: AriaLabelFn; - // (undocumented) - set averageCenter(averageCenter: boolean); - // (undocumented) - batchSize?: number; - // (undocumented) - set batchSizeIE(batchSizeIE: number); - // (undocumented) - set calculator(calculator: Calculator); - // (undocumented) - set clusterClass(clusterClass: string); - readonly clusterClick: Observable; - readonly clusteringbegin: Observable; - readonly clusteringend: Observable; - // (undocumented) - set enableRetinaIcons(enableRetinaIcons: boolean); - // (undocumented) - fitMapToMarkers(padding: number | google.maps.Padding): void; - // (undocumented) - getAverageCenter(): boolean; - // (undocumented) - getBatchSizeIE(): number; - // (undocumented) - getCalculator(): Calculator; - // (undocumented) - getClusterClass(): string; - // (undocumented) - getClusters(): Cluster[]; - // (undocumented) - getEnableRetinaIcons(): boolean; - // (undocumented) - getGridSize(): number; - // (undocumented) - getIgnoreHidden(): boolean; - // (undocumented) - getImageExtension(): string; - // (undocumented) - getImagePath(): string; - // (undocumented) - getImageSizes(): number[]; - // (undocumented) - getMaxZoom(): number; - // (undocumented) - getMinimumClusterSize(): number; - // (undocumented) - getStyles(): ClusterIconStyle[]; - // (undocumented) - getTitle(): string; - // (undocumented) - getTotalClusters(): number; - // (undocumented) - getTotalMarkers(): number; - // (undocumented) - getZIndex(): number; - // (undocumented) - getZoomOnClick(): boolean; - // (undocumented) - set gridSize(gridSize: number); - // (undocumented) - set ignoreHidden(ignoreHidden: boolean); - // (undocumented) - set imageExtension(imageExtension: string); - // (undocumented) - set imagePath(imagePath: string); - // (undocumented) - set imageSizes(imageSizes: number[]); - markerClusterer?: MarkerClusterer; - readonly markerClustererInitialized: EventEmitter; - // (undocumented) - _markers: QueryList; - // (undocumented) - set maxZoom(maxZoom: number); - // (undocumented) - set minimumClusterSize(minimumClusterSize: number); - // (undocumented) - ngAfterContentInit(): void; - // (undocumented) - ngOnChanges(changes: SimpleChanges): void; - // (undocumented) - ngOnDestroy(): void; - // (undocumented) - ngOnInit(): void; - // (undocumented) - set options(options: MarkerClustererOptions); - // (undocumented) - set styles(styles: ClusterIconStyle[]); - // (undocumented) - set title(title: string); - // (undocumented) - set zIndex(zIndex: number); - // (undocumented) - set zoomOnClick(zoomOnClick: boolean); - // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; - // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; -} - // @public export class MapPolygon implements OnInit, OnDestroy { constructor(...args: unknown[]); From f2342bdb0ca5aba34ece006731686a57b7f4ce51 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 16 Oct 2024 14:43:10 +0200 Subject: [PATCH 2/3] feat(google-maps): implement new marker clusterer Adds a new `MapMarkerClusterer` component that is based on the most up-to-date clustering library, and supports both regular and advanced markers. Fixes #23695. --- src/dev-app/google-map/google-map-demo.html | 35 +-- src/dev-app/google-map/google-map-demo.ts | 15 +- src/google-maps/google-maps-module.ts | 2 + .../map-advanced-marker.ts | 21 +- .../map-marker-clusterer/README.md | 51 ++++ .../map-marker-clusterer-types.ts | 176 ++++++++++++++ .../map-marker-clusterer.spec.ts | 209 ++++++++++++++++ .../map-marker-clusterer.ts | 229 ++++++++++++++++++ src/google-maps/map-marker/map-marker.ts | 9 +- src/google-maps/marker-utilities.ts | 20 ++ src/google-maps/public-api.ts | 1 + .../testing/fake-google-map-utils.ts | 41 ++++ .../google-maps/google-maps.md | 30 ++- 13 files changed, 799 insertions(+), 40 deletions(-) create mode 100644 src/google-maps/map-marker-clusterer/README.md create mode 100644 src/google-maps/map-marker-clusterer/map-marker-clusterer-types.ts create mode 100644 src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts create mode 100644 src/google-maps/map-marker-clusterer/map-marker-clusterer.ts create mode 100644 src/google-maps/marker-utilities.ts diff --git a/src/dev-app/google-map/google-map-demo.html b/src/dev-app/google-map/google-map-demo.html index e4961e0d43cc..45a2d0787c7c 100644 --- a/src/dev-app/google-map/google-map-demo.html +++ b/src/dev-app/google-map/google-map-demo.html @@ -10,26 +10,25 @@ (mapRightclick)="handleRightclick()" [mapTypeId]="mapTypeId" [mapId]="mapId"> - - + + @for (markerPosition of markerPositions; track markerPosition) { - + (mapClick)="infoWindow.open(marker)">
} - - @if (hasAdvancedMarker) { + + @if (hasCustomContentMarker) { - @@ -216,18 +215,8 @@
-
- -
-
diff --git a/src/dev-app/google-map/google-map-demo.ts b/src/dev-app/google-map/google-map-demo.ts index 3c17404cd61e..031cd9a859c5 100644 --- a/src/dev-app/google-map/google-map-demo.ts +++ b/src/dev-app/google-map/google-map-demo.ts @@ -25,13 +25,12 @@ import { MapHeatmapLayer, MapInfoWindow, MapKmlLayer, - MapMarker, - DeprecatedMapMarkerClusterer, MapPolygon, MapPolyline, MapRectangle, MapTrafficLayer, MapTransitLayer, + MapMarkerClusterer, } from '@angular/google-maps'; const POLYLINE_PATH: google.maps.LatLngLiteral[] = [ @@ -75,8 +74,7 @@ let apiLoadingPromise: Promise | null = null; MapHeatmapLayer, MapInfoWindow, MapKmlLayer, - MapMarker, - DeprecatedMapMarkerClusterer, + MapMarkerClusterer, MapAdvancedMarker, MapPolygon, MapPolyline, @@ -98,7 +96,6 @@ export class GoogleMapDemo { center = {lat: 24, lng: 12}; mapAdvancedMarkerPosition = {lat: 22, lng: 21}; - markerOptions = {draggable: false}; markerPositions: google.maps.LatLngLiteral[] = []; zoom = 4; display?: google.maps.LatLngLiteral; @@ -153,17 +150,13 @@ export class GoogleMapDemo { isTrafficLayerDisplayed = false; isTransitLayerDisplayed = false; isBicyclingLayerDisplayed = false; - hasAdvancedMarker = false; - hasAdvancedMarkerCustomContent = true; + hasCustomContentMarker = false; // This is necessary for testing advanced markers. It seems like any value works locally. mapId = '123'; mapTypeId: google.maps.MapTypeId; mapTypeIds = ['hybrid', 'roadmap', 'satellite', 'terrain'] as google.maps.MapTypeId[]; - markerClustererImagePath = - 'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m'; - directionsResult?: google.maps.DirectionsResult; constructor() { @@ -262,7 +255,7 @@ export class GoogleMapDemo { if (!apiLoadingPromise) { apiLoadingPromise = this._loadScript( - 'https://unpkg.com/@googlemaps/markerclustererplus/dist/index.min.js', + 'https://unpkg.com/@googlemaps/markerclusterer/dist/index.min.js', ); } diff --git a/src/google-maps/google-maps-module.ts b/src/google-maps/google-maps-module.ts index c373e61fd088..d8fad333f37c 100644 --- a/src/google-maps/google-maps-module.ts +++ b/src/google-maps/google-maps-module.ts @@ -25,6 +25,7 @@ import {MapTrafficLayer} from './map-traffic-layer/map-traffic-layer'; import {MapTransitLayer} from './map-transit-layer/map-transit-layer'; import {MapHeatmapLayer} from './map-heatmap-layer/map-heatmap-layer'; import {MapAdvancedMarker} from './map-advanced-marker/map-advanced-marker'; +import {MapMarkerClusterer} from './map-marker-clusterer/map-marker-clusterer'; const COMPONENTS = [ GoogleMap, @@ -44,6 +45,7 @@ const COMPONENTS = [ MapRectangle, MapTrafficLayer, MapTransitLayer, + MapMarkerClusterer, ]; @NgModule({ diff --git a/src/google-maps/map-advanced-marker/map-advanced-marker.ts b/src/google-maps/map-advanced-marker/map-advanced-marker.ts index 812807d7479d..246b955b0815 100644 --- a/src/google-maps/map-advanced-marker/map-advanced-marker.ts +++ b/src/google-maps/map-advanced-marker/map-advanced-marker.ts @@ -24,8 +24,10 @@ import { import {GoogleMap} from '../google-map/google-map'; import {MapEventManager} from '../map-event-manager'; -import {Observable} from 'rxjs'; import {MapAnchorPoint} from '../map-anchor-point'; +import {MAP_MARKER, MarkerDirective} from '../marker-utilities'; +import {Observable} from 'rxjs'; +import {take} from 'rxjs/operators'; /** * Default options for the Google Maps marker component. Displays a marker @@ -43,8 +45,16 @@ export const DEFAULT_MARKER_OPTIONS = { @Directive({ selector: 'map-advanced-marker', exportAs: 'mapAdvancedMarker', + providers: [ + { + provide: MAP_MARKER, + useExisting: MapAdvancedMarker, + }, + ], }) -export class MapAdvancedMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { +export class MapAdvancedMarker + implements OnInit, OnChanges, OnDestroy, MapAnchorPoint, MarkerDirective +{ private readonly _googleMap = inject(GoogleMap); private _ngZone = inject(NgZone); private _eventManager = new MapEventManager(inject(NgZone)); @@ -262,6 +272,13 @@ export class MapAdvancedMarker implements OnInit, OnChanges, OnDestroy, MapAncho return this.advancedMarker; } + /** Returns a promise that resolves when the marker has been initialized. */ + _resolveMarker(): Promise { + return this.advancedMarker + ? Promise.resolve(this.advancedMarker) + : this.markerInitialized.pipe(take(1)).toPromise(); + } + /** Creates a combined options object using the passed-in options and the individual inputs. */ private _combineOptions(): google.maps.marker.AdvancedMarkerElementOptions { const options = this._options || DEFAULT_MARKER_OPTIONS; diff --git a/src/google-maps/map-marker-clusterer/README.md b/src/google-maps/map-marker-clusterer/README.md new file mode 100644 index 000000000000..a901018c8bde --- /dev/null +++ b/src/google-maps/map-marker-clusterer/README.md @@ -0,0 +1,51 @@ +# MapMarkerClusterer + +The `MapMarkerClusterer` component wraps the [`MarkerClusterer` class](https://googlemaps.github.io/js-markerclusterer/classes/MarkerClusterer.html) from the [Google Maps JavaScript MarkerClusterer Library](https://github.com/googlemaps/js-markerclusterer). The `MapMarkerClusterer` component displays a cluster of markers that are children of the `` tag. Unlike the other Google Maps components, MapMarkerClusterer does not have an `options` input, so any input (listed in the [documentation](https://googlemaps.github.io/js-markerclusterer/) for the `MarkerClusterer` class) should be set directly. + +## Loading the Library + +Like the Google Maps JavaScript API, the MarkerClusterer library needs to be loaded separately. This can be accomplished by using this script tag: + +```html + +``` + +Additional information can be found by looking at [Marker Clustering](https://developers.google.com/maps/documentation/javascript/marker-clustering) in the Google Maps JavaScript API documentation. + +## Example + +```typescript +// google-map-demo.component.ts +import {Component} from '@angular/core'; +import {GoogleMap, MapMarkerClusterer, MapAdvancedMarker} from '@angular/google-maps'; + +@Component({ + selector: 'google-map-demo', + templateUrl: 'google-map-demo.html', + imports: [GoogleMap, MapMarkerClusterer, MapAdvancedMarker], +}) +export class GoogleMapDemo { + center: google.maps.LatLngLiteral = {lat: 24, lng: 12}; + zoom = 4; + markerPositions: google.maps.LatLngLiteral[] = []; + + addMarker(event: google.maps.MapMouseEvent) { + this.markerPositions.push(event.latLng.toJSON()); + } +} +``` + +```html + + + @for (markerPosition of markerPositions; track $index) { + + } + + +``` diff --git a/src/google-maps/map-marker-clusterer/map-marker-clusterer-types.ts b/src/google-maps/map-marker-clusterer/map-marker-clusterer-types.ts new file mode 100644 index 000000000000..3983f0b80210 --- /dev/null +++ b/src/google-maps/map-marker-clusterer/map-marker-clusterer-types.ts @@ -0,0 +1,176 @@ +/** + * @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.dev/license + */ + +/// + +import {Marker} from '../marker-utilities'; + +// This file duplicates the necessary types from the `@googlemaps/markerclusterer` +// package which isn't available for use internally. + +// tslint:disable + +export interface ClusterOptions { + position?: google.maps.LatLng | google.maps.LatLngLiteral; + markers?: Marker[]; +} + +export declare class Cluster { + marker?: Marker; + readonly markers?: Marker[]; + protected _position: google.maps.LatLng; + constructor({markers, position}: ClusterOptions); + get bounds(): google.maps.LatLngBounds | undefined; + get position(): google.maps.LatLng; + /** + * Get the count of **visible** markers. + */ + get count(): number; + /** + * Add a marker to the cluster. + */ + push(marker: Marker): void; + /** + * Cleanup references and remove marker from map. + */ + delete(): void; +} + +export declare class MarkerClusterer extends google.maps.OverlayView { + onClusterClick: onClusterClickHandler; + protected algorithm: Algorithm; + protected clusters: Cluster[]; + protected markers: Marker[]; + protected renderer: Renderer; + protected map: google.maps.Map | null; + protected idleListener: google.maps.MapsEventListener; + constructor({ + map, + markers, + algorithmOptions, + algorithm, + renderer, + onClusterClick, + }: MarkerClustererOptions); + addMarker(marker: Marker, noDraw?: boolean): void; + addMarkers(markers: Marker[], noDraw?: boolean): void; + removeMarker(marker: Marker, noDraw?: boolean): boolean; + removeMarkers(markers: Marker[], noDraw?: boolean): boolean; + clearMarkers(noDraw?: boolean): void; + render(): void; + onAdd(): void; + onRemove(): void; + protected reset(): void; + protected renderClusters(): void; +} + +export type onClusterClickHandler = ( + event: google.maps.MapMouseEvent, + cluster: Cluster, + map: google.maps.Map, +) => void; + +export interface MarkerClustererOptions { + markers?: Marker[]; + /** + * An algorithm to cluster markers. Default is {@link SuperClusterAlgorithm}. Must + * provide a `calculate` method accepting {@link AlgorithmInput} and returning + * an array of {@link Cluster}. + */ + algorithm?: Algorithm; + algorithmOptions?: AlgorithmOptions; + map?: google.maps.Map | null; + /** + * An object that converts a {@link Cluster} into a `google.maps.Marker`. + * Default is {@link DefaultRenderer}. + */ + renderer?: Renderer; + onClusterClick?: onClusterClickHandler; +} + +export declare enum MarkerClustererEvents { + CLUSTERING_BEGIN = 'clusteringbegin', + CLUSTERING_END = 'clusteringend', + CLUSTER_CLICK = 'click', +} + +export declare const defaultOnClusterClickHandler: onClusterClickHandler; + +export interface Renderer { + /** + * Turn a {@link Cluster} into a `Marker`. + * + * Below is a simple example to create a marker with the number of markers in the cluster as a label. + * + * ```typescript + * return new google.maps.Marker({ + * position, + * label: String(markers.length), + * }); + * ``` + */ + render(cluster: Cluster, stats: ClusterStats, map: google.maps.Map): Marker; +} + +export declare class ClusterStats { + readonly markers: { + sum: number; + }; + readonly clusters: { + count: number; + markers: { + mean: number; + sum: number; + min: number; + max: number; + }; + }; + constructor(markers: Marker[], clusters: Cluster[]); +} + +export interface Algorithm { + /** + * Calculates an array of {@link Cluster}. + */ + calculate: ({markers, map}: AlgorithmInput) => AlgorithmOutput; +} + +export interface AlgorithmOptions { + maxZoom?: number; +} + +export interface AlgorithmInput { + /** + * The map containing the markers and clusters. + */ + map: google.maps.Map; + /** + * An array of markers to be clustered. + * + * There are some specific edge cases to be aware of including the following: + * * Markers that are not visible. + */ + markers: Marker[]; + /** + * The `mapCanvasProjection` enables easy conversion from lat/lng to pixel. + * + * @see [MapCanvasProjection](https://developers.google.com/maps/documentation/javascript/reference/overlay-view#MapCanvasProjection) + */ + mapCanvasProjection: google.maps.MapCanvasProjection; +} + +export interface AlgorithmOutput { + /** + * The clusters returned based upon the {@link AlgorithmInput}. + */ + clusters: Cluster[]; + /** + * A boolean flag indicating that the clusters have not changed. + */ + changed?: boolean; +} 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 new file mode 100644 index 000000000000..01629f6ed952 --- /dev/null +++ b/src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts @@ -0,0 +1,209 @@ +import {Component, ViewChild} from '@angular/core'; +import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; +import type {MarkerClusterer, Renderer, Algorithm} from './map-marker-clusterer-types'; + +import {DEFAULT_OPTIONS} from '../google-map/google-map'; +import {GoogleMapsModule} from '../google-maps-module'; +import { + createMapConstructorSpy, + createMapSpy, + createMarkerClustererConstructorSpy, + createMarkerClustererSpy, + createAdvancedMarkerSpy, + createAdvancedMarkerConstructorSpy, +} from '../testing/fake-google-map-utils'; +import {MapMarkerClusterer} from './map-marker-clusterer'; + +describe('MapMarkerClusterer', () => { + let mapSpy: jasmine.SpyObj; + let markerClustererSpy: jasmine.SpyObj; + let markerClustererConstructorSpy: jasmine.Spy; + let fixture: ComponentFixture; + const anyMarkerMatcher = jasmine.any( + Object, + ) as unknown as google.maps.marker.AdvancedMarkerElement; + + beforeEach(() => { + mapSpy = createMapSpy(DEFAULT_OPTIONS); + createMapConstructorSpy(mapSpy).and.callThrough(); + + const markerSpy = createAdvancedMarkerSpy({}); + // The spy target function cannot be an arrow-function as this breaks when created + // through `new`. + createAdvancedMarkerConstructorSpy(markerSpy).and.callFake(function () { + return createAdvancedMarkerSpy({}); + }); + + markerClustererSpy = createMarkerClustererSpy(); + markerClustererConstructorSpy = + createMarkerClustererConstructorSpy(markerClustererSpy).and.callThrough(); + + fixture = TestBed.createComponent(TestApp); + }); + + afterEach(() => { + (window.google as any) = undefined; + (window as any).markerClusterer = undefined; + }); + + 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(); + flush(); + }).toThrowError(/MarkerClusterer class not found, cannot construct a marker cluster/); + })); + + it('initializes a Google Map Marker Clusterer', fakeAsync(() => { + fixture.detectChanges(); + flush(); + + expect(markerClustererConstructorSpy).toHaveBeenCalledWith({ + map: mapSpy, + renderer: undefined, + algorithm: undefined, + onClusterClick: jasmine.any(Function), + }); + })); + + it('sets marker clusterer inputs', fakeAsync(() => { + fixture.componentInstance.algorithm = {name: 'custom'} as any; + fixture.componentInstance.renderer = {render: () => null!}; + fixture.detectChanges(); + flush(); + + expect(markerClustererConstructorSpy).toHaveBeenCalledWith({ + map: mapSpy, + algorithm: fixture.componentInstance.algorithm, + renderer: fixture.componentInstance.renderer, + onClusterClick: jasmine.any(Function), + }); + })); + + it('recreates the clusterer if the options change', fakeAsync(() => { + fixture.componentInstance.algorithm = {name: 'custom1'} as any; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); + + expect(markerClustererConstructorSpy).toHaveBeenCalledWith({ + map: mapSpy, + algorithm: jasmine.objectContaining({name: 'custom1'}), + renderer: undefined, + onClusterClick: jasmine.any(Function), + }); + + fixture.componentInstance.algorithm = {name: 'custom2'} as any; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); + + expect(markerClustererConstructorSpy).toHaveBeenCalledWith({ + map: mapSpy, + algorithm: jasmine.objectContaining({name: 'custom2'}), + renderer: undefined, + onClusterClick: jasmine.any(Function), + }); + })); + + 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', fakeAsync(() => { + fixture.detectChanges(); + flush(); + + expect(markerClustererSpy.addMarkers).toHaveBeenCalledWith([ + anyMarkerMatcher, + anyMarkerMatcher, + ]); + + fixture.componentInstance.state = 'state2'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); + + expect(markerClustererSpy.addMarkers).toHaveBeenCalledWith([anyMarkerMatcher], true); + expect(markerClustererSpy.removeMarkers).toHaveBeenCalledWith([anyMarkerMatcher], true); + expect(markerClustererSpy.render).toHaveBeenCalledTimes(1); + + fixture.componentInstance.state = 'state0'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + flush(); + + expect(markerClustererSpy.addMarkers).toHaveBeenCalledWith([], true); + expect(markerClustererSpy.removeMarkers).toHaveBeenCalledWith( + [anyMarkerMatcher, anyMarkerMatcher], + true, + ); + expect(markerClustererSpy.render).toHaveBeenCalledTimes(2); + })); + + it('initializes event handlers on the map related to clustering', fakeAsync(() => { + fixture.detectChanges(); + flush(); + + expect(mapSpy.addListener).toHaveBeenCalledWith('clusteringbegin', jasmine.any(Function)); + expect(mapSpy.addListener).not.toHaveBeenCalledWith('clusteringend', jasmine.any(Function)); + })); + + it('emits to clusterClick when the `onClusterClick` callback is invoked', fakeAsync(() => { + fixture.detectChanges(); + flush(); + + expect(fixture.componentInstance.onClusterClick).not.toHaveBeenCalled(); + + const callback = markerClustererConstructorSpy.calls.mostRecent().args[0].onClusterClick; + callback({}, {}, {}); + fixture.detectChanges(); + flush(); + + expect(fixture.componentInstance.onClusterClick).toHaveBeenCalledTimes(1); + })); +}); + +@Component({ + selector: 'test-app', + standalone: true, + imports: [GoogleMapsModule], + template: ` + + + @if (state === 'state1') { + + } + @if (state === 'state1' || state === 'state2') { + + } + @if (state === 'state2') { + + } + + + `, +}) +class TestApp { + @ViewChild(MapMarkerClusterer) markerClusterer: MapMarkerClusterer; + renderer: Renderer; + algorithm: Algorithm; + state = 'state1'; + onClusteringBegin = jasmine.createSpy('onclusteringbegin spy'); + onClusterClick = jasmine.createSpy('clusterClick spy'); +} diff --git a/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts b/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts new file mode 100644 index 000000000000..8113ab961e9c --- /dev/null +++ b/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts @@ -0,0 +1,229 @@ +/** + * @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.dev/license + */ + +// Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 +/// + +import { + ChangeDetectionStrategy, + Component, + ContentChildren, + EventEmitter, + inject, + Input, + NgZone, + OnChanges, + OnDestroy, + OnInit, + Output, + QueryList, + SimpleChanges, + ViewEncapsulation, +} from '@angular/core'; +import {Observable, Subscription} from 'rxjs'; +import type { + Cluster, + MarkerClusterer, + onClusterClickHandler, + Renderer, + Algorithm, +} from './map-marker-clusterer-types'; + +import {GoogleMap} from '../google-map/google-map'; +import {MapEventManager} from '../map-event-manager'; +import {MAP_MARKER, Marker, MarkerDirective} from '../marker-utilities'; + +declare const markerClusterer: { + MarkerClusterer: typeof MarkerClusterer; + defaultOnClusterClickHandler: onClusterClickHandler; +}; + +/** + * Angular component for implementing a Google Maps Marker Clusterer. + * + * See https://developers.google.com/maps/documentation/javascript/marker-clustering + */ +@Component({ + selector: 'map-marker-clusterer', + exportAs: 'mapMarkerClusterer', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + encapsulation: ViewEncapsulation.None, +}) +export class MapMarkerClusterer implements OnInit, OnChanges, OnDestroy { + private readonly _googleMap = inject(GoogleMap); + private readonly _ngZone = inject(NgZone); + private readonly _currentMarkers = new Set(); + private readonly _closestMapEventManager = new MapEventManager(this._ngZone); + private _markersSubscription = Subscription.EMPTY; + + /** Whether the clusterer is allowed to be initialized. */ + private readonly _canInitialize = this._googleMap._isBrowser; + + /** + * Used to customize how the marker cluster is rendered. + * See https://googlemaps.github.io/js-markerclusterer/interfaces/Renderer.html. + */ + @Input() + renderer: Renderer; + + /** + * Algorithm used to cluster the markers. + * See https://googlemaps.github.io/js-markerclusterer/interfaces/Algorithm.html. + */ + @Input() + algorithm: Algorithm; + + /** Emits when clustering has started. */ + @Output() readonly clusteringbegin: Observable = + this._closestMapEventManager.getLazyEmitter('clusteringbegin'); + + /** Emits when clustering is done. */ + @Output() readonly clusteringend: Observable = + this._closestMapEventManager.getLazyEmitter('clusteringend'); + + /** Emits when a cluster has been clicked. */ + @Output() + readonly clusterClick: EventEmitter = new EventEmitter(); + + /** Event emitted when the marker clusterer is initialized. */ + @Output() readonly markerClustererInitialized: EventEmitter = + new EventEmitter(); + + @ContentChildren(MAP_MARKER, {descendants: true}) _markers: QueryList; + + /** Underlying MarkerClusterer object used to interact with Google Maps. */ + markerClusterer?: MarkerClusterer; + + async ngOnInit() { + if (this._canInitialize) { + await this._createCluster(); + + // The `clusteringbegin` and `clusteringend` events are + // emitted on the map so that's why set it as the target. + this._closestMapEventManager.setTarget(this._googleMap.googleMap!); + } + } + + async ngOnChanges(changes: SimpleChanges) { + const change = changes['renderer'] || changes['algorithm']; + + // Since the options are set in the constructor, we have to recreate the cluster if they change. + if (this.markerClusterer && change && !change.isFirstChange()) { + await this._createCluster(); + } + } + + ngOnDestroy() { + this._markersSubscription.unsubscribe(); + this._closestMapEventManager.destroy(); + this._destroyCluster(); + } + + private async _createCluster() { + if (!markerClusterer?.MarkerClusterer && (typeof ngDevMode === 'undefined' || ngDevMode)) { + throw Error( + 'MarkerClusterer class not found, cannot construct a marker cluster. ' + + 'Please install the MarkerClusterer library: ' + + 'https://github.com/googlemaps/js-markerclusterer', + ); + } + + const map = await this._googleMap._resolveMap(); + this._destroyCluster(); + + // 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.MarkerClusterer({ + map, + renderer: this.renderer, + algorithm: this.algorithm, + onClusterClick: (event, cluster, map) => { + if (this.clusterClick.observers.length) { + this._ngZone.run(() => this.clusterClick.emit(cluster)); + } else { + markerClusterer.defaultOnClusterClickHandler(event, cluster, map); + } + }, + }); + this.markerClustererInitialized.emit(this.markerClusterer); + }); + + await this._watchForMarkerChanges(); + } + + private async _watchForMarkerChanges() { + this._assertInitialized(); + const initialMarkers: Marker[] = []; + const markers = await this._getInternalMarkers(this._markers.toArray()); + + for (const marker of markers) { + this._currentMarkers.add(marker); + initialMarkers.push(marker); + } + this.markerClusterer.addMarkers(initialMarkers); + + this._markersSubscription.unsubscribe(); + this._markersSubscription = this._markers.changes.subscribe( + async (markerComponents: MarkerDirective[]) => { + this._assertInitialized(); + const newMarkers = new Set(await this._getInternalMarkers(markerComponents)); + const markersToAdd: Marker[] = []; + const markersToRemove: 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); + } + } + this.markerClusterer.addMarkers(markersToAdd, true); + this.markerClusterer.removeMarkers(markersToRemove, true); + this.markerClusterer.render(); + for (const marker of markersToRemove) { + this._currentMarkers.delete(marker); + } + }, + ); + } + + private _destroyCluster() { + // TODO(crisbeto): the naming here seems odd, but the `MarkerCluster` method isn't + // exposed. All this method seems to do at the time of writing is to call into `reset`. + // See: https://github.com/googlemaps/js-markerclusterer/blob/main/src/markerclusterer.ts#L205 + this.markerClusterer?.onRemove(); + this.markerClusterer = undefined; + } + + private _getInternalMarkers(markers: MarkerDirective[]): Promise { + return Promise.all(markers.map(marker => marker._resolveMarker())); + } + + private _assertInitialized(): asserts this is {markerClusterer: MarkerClusterer} { + 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. ' + + 'Please wait for the MarkerClusterer to load before trying to interact with it.', + ); + } + } + } +} diff --git a/src/google-maps/map-marker/map-marker.ts b/src/google-maps/map-marker/map-marker.ts index 66f07f9d227c..b1d1c22fc164 100644 --- a/src/google-maps/map-marker/map-marker.ts +++ b/src/google-maps/map-marker/map-marker.ts @@ -27,6 +27,7 @@ import {take} from 'rxjs/operators'; import {GoogleMap} from '../google-map/google-map'; import {MapEventManager} from '../map-event-manager'; import {MapAnchorPoint} from '../map-anchor-point'; +import {MAP_MARKER, MarkerDirective} from '../marker-utilities'; /** * Default options for the Google Maps marker component. Displays a marker @@ -44,8 +45,14 @@ export const DEFAULT_MARKER_OPTIONS = { @Directive({ selector: 'map-marker', exportAs: 'mapMarker', + providers: [ + { + provide: MAP_MARKER, + useExisting: MapMarker, + }, + ], }) -export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { +export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint, MarkerDirective { private readonly _googleMap = inject(GoogleMap); private _ngZone = inject(NgZone); private _eventManager = new MapEventManager(inject(NgZone)); diff --git a/src/google-maps/marker-utilities.ts b/src/google-maps/marker-utilities.ts new file mode 100644 index 000000000000..5a37cc4f42c8 --- /dev/null +++ b/src/google-maps/marker-utilities.ts @@ -0,0 +1,20 @@ +/** + * @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.dev/license + */ + +import {InjectionToken} from '@angular/core'; + +/** Marker types from the Google Maps API. */ +export type Marker = google.maps.Marker | google.maps.marker.AdvancedMarkerElement; + +/** Interface that should be implemented by directives that wrap marker APIs. */ +export interface MarkerDirective { + _resolveMarker(): Promise; +} + +/** Token that marker directives can use to expose themselves to the clusterer. */ +export const MAP_MARKER = new InjectionToken('MAP_MARKER'); diff --git a/src/google-maps/public-api.ts b/src/google-maps/public-api.ts index b83f5cfd7587..98cc770f2a1a 100644 --- a/src/google-maps/public-api.ts +++ b/src/google-maps/public-api.ts @@ -23,6 +23,7 @@ export {MapKmlLayer} from './map-kml-layer/map-kml-layer'; export {MapMarker} from './map-marker/map-marker'; export {MapAdvancedMarker} from './map-advanced-marker/map-advanced-marker'; export {DeprecatedMapMarkerClusterer} from './deprecated-map-marker-clusterer/deprecated-map-marker-clusterer'; +export {MapMarkerClusterer} from './map-marker-clusterer/map-marker-clusterer'; export {MapPolygon} from './map-polygon/map-polygon'; export {MapPolyline} from './map-polyline/map-polyline'; export {MapRectangle} from './map-rectangle/map-rectangle'; diff --git a/src/google-maps/testing/fake-google-map-utils.ts b/src/google-maps/testing/fake-google-map-utils.ts index 1e8f5f293798..89b32072ca6f 100644 --- a/src/google-maps/testing/fake-google-map-utils.ts +++ b/src/google-maps/testing/fake-google-map-utils.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import type {MarkerClusterer} from '../map-marker-clusterer/map-marker-clusterer-types'; import {MarkerClusterer as DeprecatedMarkerClusterer} from '../deprecated-map-marker-clusterer/deprecated-marker-clusterer-types'; // The global `window` variable is typed as an intersection of `Window` and `globalThis`. @@ -43,6 +44,10 @@ export interface TestingWindow extends Window { }; }; MarkerClusterer?: jasmine.Spy; + markerClusterer?: { + MarkerClusterer?: jasmine.Spy; + defaultOnClusterClickHandler?: jasmine.Spy; + }; } /** Creates a jasmine.SpyObj for a google.maps.Map. */ @@ -573,6 +578,42 @@ export function createBicyclingLayerConstructorSpy( return bicylingLayerConstructorSpy; } +/** Creates a jasmine.SpyObj for a MarkerClusterer */ +export function createMarkerClustererSpy(): jasmine.SpyObj { + return jasmine.createSpyObj('MarkerClusterer', [ + 'addMarker', + 'addMarkers', + 'removeMarker', + 'removeMarkers', + 'clearMarkers', + 'render', + 'onAdd', + 'onRemove', + ]); +} + +/** Creates a jasmine.Spy to watch for the constructor of a MarkerClusterer */ +export function createMarkerClustererConstructorSpy( + markerClustererSpy: jasmine.SpyObj, + 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 () { + return markerClustererSpy; + }, + ); + if (apiLoaded) { + const testingWindow: TestingWindow = window; + testingWindow.markerClusterer = { + MarkerClusterer: markerClustererConstructorSpy, + defaultOnClusterClickHandler: jasmine.createSpy('defaultOnClusterClickHandler'), + }; + } + return markerClustererConstructorSpy; +} + /** Creates a jasmine.SpyObj for a MarkerClusterer */ export function createDeprecatedMarkerClustererSpy(): jasmine.SpyObj { const deprecatedMarkerClustererSpy = jasmine.createSpyObj('DeprecatedMarkerClusterer', [ diff --git a/tools/public_api_guard/google-maps/google-maps.md b/tools/public_api_guard/google-maps/google-maps.md index b65c4afdb221..28c956a8ed3c 100644 --- a/tools/public_api_guard/google-maps/google-maps.md +++ b/tools/public_api_guard/google-maps/google-maps.md @@ -229,14 +229,14 @@ export class GoogleMapsModule { // (undocumented) static ɵinj: i0.ɵɵInjectorDeclaration; // (undocumented) - static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; } // @public export type HeatmapData = google.maps.MVCArray | (google.maps.LatLng | google.maps.visualization.WeightedLocation | google.maps.LatLngLiteral)[]; // @public -export class MapAdvancedMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { +export class MapAdvancedMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint, MarkerDirective { constructor(...args: unknown[]); advancedMarker: google.maps.marker.AdvancedMarkerElement; set content(content: Node | google.maps.marker.PinElement | null); @@ -261,6 +261,7 @@ export class MapAdvancedMarker implements OnInit, OnChanges, OnDestroy, MapAncho ngOnInit(): void; set options(options: google.maps.marker.AdvancedMarkerElementOptions); set position(position: google.maps.LatLngLiteral | google.maps.LatLng | google.maps.LatLngAltitude | google.maps.LatLngAltitudeLiteral); + _resolveMarker(): Promise; set title(title: string); set zIndex(zIndex: number); // (undocumented) @@ -542,7 +543,7 @@ export class MapKmlLayer implements OnInit, OnDestroy { } // @public -export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { +export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint, MarkerDirective { constructor(...args: unknown[]); readonly animationChanged: Observable; set clickable(clickable: boolean); @@ -600,6 +601,29 @@ export class MapMarker implements OnInit, OnChanges, OnDestroy, MapAnchorPoint { static ɵfac: i0.ɵɵFactoryDeclaration; } +// @public +export class MapMarkerClusterer implements OnInit, OnChanges, OnDestroy { + algorithm: Algorithm_2; + readonly clusterClick: EventEmitter; + readonly clusteringbegin: Observable; + readonly clusteringend: Observable; + markerClusterer?: MarkerClusterer_2; + readonly markerClustererInitialized: EventEmitter; + // (undocumented) + _markers: QueryList; + // (undocumented) + ngOnChanges(changes: SimpleChanges): Promise; + // (undocumented) + ngOnDestroy(): void; + // (undocumented) + ngOnInit(): Promise; + renderer: Renderer; + // (undocumented) + static ɵcmp: i0.ɵɵComponentDeclaration; + // (undocumented) + static ɵfac: i0.ɵɵFactoryDeclaration; +} + // @public export class MapPolygon implements OnInit, OnDestroy { constructor(...args: unknown[]); From a0742c9841d668de34854d6cc8f47de2ce7c5d8b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 17 Oct 2024 14:26:13 +0200 Subject: [PATCH 3/3] fix(google-maps): add schematic to switch to the new clusterer name Since the clusterer's class and tag were renamed, we need to migrated existing users to the new name. --- src/google-maps/package.json | 7 +- src/google-maps/schematics/BUILD.bazel | 1 + src/google-maps/schematics/migration.json | 9 + .../schematics/ng-update/BUILD.bazel | 79 +++++++ src/google-maps/schematics/ng-update/index.ts | 141 ++++++++++++ .../schematics/ng-update/tsconfig.json | 7 + .../ng-update/v19-ng-update.spec.ts | 213 ++++++++++++++++++ src/tsconfig-legacy.json | 5 + 8 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 src/google-maps/schematics/migration.json create mode 100644 src/google-maps/schematics/ng-update/BUILD.bazel create mode 100644 src/google-maps/schematics/ng-update/index.ts create mode 100644 src/google-maps/schematics/ng-update/tsconfig.json create mode 100644 src/google-maps/schematics/ng-update/v19-ng-update.spec.ts diff --git a/src/google-maps/package.json b/src/google-maps/package.json index a03e1f78dbbe..5f38bff98174 100644 --- a/src/google-maps/package.json +++ b/src/google-maps/package.json @@ -27,5 +27,10 @@ }, "sideEffects": false, "schematics": "./schematics/collection.json", - "ng-update": {} + "ng-update": { + "migrations": "./schematics/migration.json", + "packageGroup": [ + "@angular/google-maps" + ] + } } diff --git a/src/google-maps/schematics/BUILD.bazel b/src/google-maps/schematics/BUILD.bazel index 84fafe225944..72c95a675a7d 100644 --- a/src/google-maps/schematics/BUILD.bazel +++ b/src/google-maps/schematics/BUILD.bazel @@ -30,5 +30,6 @@ pkg_npm( deps = [ ":schematics", ":schematics_assets", + "//src/google-maps/schematics/ng-update:ng_update_index", ], ) diff --git a/src/google-maps/schematics/migration.json b/src/google-maps/schematics/migration.json new file mode 100644 index 000000000000..742d51a5bcb5 --- /dev/null +++ b/src/google-maps/schematics/migration.json @@ -0,0 +1,9 @@ +{ + "schematics": { + "migration-v19": { + "version": "19.0.0-0", + "description": "Updates the Angular Google Maps package to v19", + "factory": "./ng-update/index_bundled#updateToV19" + } + } +} diff --git a/src/google-maps/schematics/ng-update/BUILD.bazel b/src/google-maps/schematics/ng-update/BUILD.bazel new file mode 100644 index 000000000000..3228023d033b --- /dev/null +++ b/src/google-maps/schematics/ng-update/BUILD.bazel @@ -0,0 +1,79 @@ +load("//tools:defaults.bzl", "esbuild", "jasmine_node_test", "spec_bundle", "ts_library") + +## THIS ONE IS ESM +# By default everything is ESM +# ESBUild needs ESM for bundling. Cannot reliably use CJS as input. +ts_library( + name = "ng_update_lib", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + # Schematics can not yet run in ESM module. For now we continue to use CommonJS. + # TODO(ESM): remove this once the Angular CLI supports ESM schematics. + devmode_module = "commonjs", + deps = [ + "@npm//@angular-devkit/core", + "@npm//@angular-devkit/schematics", + "@npm//@schematics/angular", + "@npm//@types/node", + "@npm//typescript", + ], +) + +esbuild( + name = "ng_update_index", + entry_point = ":index.ts", + external = [ + "@schematics/angular", + "@angular-devkit/schematics", + "@angular-devkit/core", + "typescript", + ], + # TODO: Switch to ESM when Angular CLI supports it. + format = "cjs", + output = "index_bundled.js", + platform = "node", + target = "es2015", + visibility = ["//src/google-maps/schematics:__pkg__"], + deps = [":ng_update_lib"], +) + +ts_library( + name = "test_lib", + testonly = True, + srcs = glob(["**/*.spec.ts"]), + deps = [ + ":ng_update_lib", + "@npm//@angular-devkit/core", + "@npm//@angular-devkit/schematics", + "@npm//@bazel/runfiles", + "@npm//@types/jasmine", + "@npm//@types/node", + "@npm//@types/shelljs", + ], +) + +spec_bundle( + name = "spec_bundle", + external = [ + "*/paths.js", + "shelljs", + "@angular-devkit/core/node", + ], + platform = "cjs-legacy", + target = "es2020", + deps = [":test_lib"], +) + +jasmine_node_test( + name = "test", + data = [ + ":ng_update_index", + "//src/google-maps/schematics:schematics_assets", + "@npm//shelljs", + ], + deps = [ + ":spec_bundle", + ], +) diff --git a/src/google-maps/schematics/ng-update/index.ts b/src/google-maps/schematics/ng-update/index.ts new file mode 100644 index 000000000000..9fcd69c841c0 --- /dev/null +++ b/src/google-maps/schematics/ng-update/index.ts @@ -0,0 +1,141 @@ +/** + * @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.dev/license + */ + +import {Path} from '@angular-devkit/core'; +import {Rule, Tree} from '@angular-devkit/schematics'; +import ts from 'typescript'; + +/** Tag name of the clusterer component. */ +const TAG_NAME = 'map-marker-clusterer'; + +/** Module from which the clusterer is being imported. */ +const MODULE_NAME = '@angular/google-maps'; + +/** Old name of the clusterer class. */ +const CLASS_NAME = 'MapMarkerClusterer'; + +/** New name of the clusterer class. */ +const DEPRECATED_CLASS_NAME = 'DeprecatedMapMarkerClusterer'; + +/** Entry point for the migration schematics with target of Angular Material v19 */ +export function updateToV19(): Rule { + return tree => { + tree.visit(path => { + if (path.endsWith('.html')) { + const content = tree.readText(path); + + if (content.includes('<' + TAG_NAME)) { + tree.overwrite(path, migrateHtml(content)); + } + } else if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { + migrateTypeScript(path, tree); + } + }); + }; +} + +/** Migrates an HTML template from the old tag name to the new one. */ +function migrateHtml(content: string): string { + return content + .replace(/ { + const symbolName = element.propertyName || element.name; + + if (ts.isIdentifier(symbolName) && symbolName.text === CLASS_NAME) { + results.push(element); + } + }); + } + } else { + node.forEachChild(walk); + } + }); + + // Sort the results in reverse order to make applying the updates easier. + return results.sort((a, b) => b.getStart() - a.getStart()); +} diff --git a/src/google-maps/schematics/ng-update/tsconfig.json b/src/google-maps/schematics/ng-update/tsconfig.json new file mode 100644 index 000000000000..78f1753e68b0 --- /dev/null +++ b/src/google-maps/schematics/ng-update/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "esnext", + "target": "es2015" + } +} diff --git a/src/google-maps/schematics/ng-update/v19-ng-update.spec.ts b/src/google-maps/schematics/ng-update/v19-ng-update.spec.ts new file mode 100644 index 000000000000..57ff61787fe9 --- /dev/null +++ b/src/google-maps/schematics/ng-update/v19-ng-update.spec.ts @@ -0,0 +1,213 @@ +import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core'; +import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing'; +import {HostTree} from '@angular-devkit/schematics'; +import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; +import {runfiles} from '@bazel/runfiles'; +import shx from 'shelljs'; + +describe('v19 migration', () => { + let runner: SchematicTestRunner; + let host: TempScopedNodeJsSyncHost; + let tree: UnitTestTree; + let tmpDirPath: string; + + function writeFile(filePath: string, contents: string) { + host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); + } + + function runMigration() { + return runner.runSchematic('migration-v19', {}, tree); + } + + function stripWhitespace(value: string) { + return value.replace(/\s/g, ''); + } + + beforeEach(() => { + runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../migration.json')); + host = new TempScopedNodeJsSyncHost(); + tree = new UnitTestTree(new HostTree(host)); + + writeFile( + '/angular.json', + JSON.stringify({ + version: 1, + projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}, + }), + ); + + tmpDirPath = getSystemPath(host.root); + shx.cd(tmpDirPath); + }); + + it('should migrate the clusterer in HTML files', async () => { + writeFile( + '/my-comp.html', + ` +
+ + + + + +
+ +
+
+ + @for (marker of markers; track $index) { + + } + +
+
+ `, + ); + + await runMigration(); + + const content = tree.readContent('/my-comp.html'); + + expect(stripWhitespace(content)).toBe( + stripWhitespace(` +
+ + + + + +
+ +
+
+ + @for (marker of markers; track $index) { + + } + +
+
+ `), + ); + }); + + it('should migrate the clusterer in a TypeScript file', async () => { + writeFile( + '/my-comp.ts', + ` + import {Component} from '@angular/core'; + import {MapMarkerClusterer, MapMarker} from '@angular/google-maps'; + + @Component({ + template: '', + imports: [MapMarkerClusterer, MapMarker] + }) + export class MyComp {} + `, + ); + + await runMigration(); + + const content = tree.readContent('/my-comp.ts'); + + expect(stripWhitespace(content)).toBe( + stripWhitespace(` + import {Component} from '@angular/core'; + import {DeprecatedMapMarkerClusterer as MapMarkerClusterer, MapMarker} from '@angular/google-maps'; + + @Component({ + template: '', + imports: [MapMarkerClusterer, MapMarker] + }) + export class MyComp {} + `), + ); + }); + + it('should migrate an aliased clusterer in a TypeScript file', async () => { + writeFile( + '/my-comp.ts', + ` + import {Component} from '@angular/core'; + import {MapMarkerClusterer as MyClusterer, MapMarker} from '@angular/google-maps'; + + @Component({ + template: '', + imports: [MyClusterer, MapMarker] + }) + export class MyComp {} + `, + ); + + await runMigration(); + + const content = tree.readContent('/my-comp.ts'); + + expect(stripWhitespace(content)).toBe( + stripWhitespace(` + import {Component} from '@angular/core'; + import {DeprecatedMapMarkerClusterer as MyClusterer, MapMarker} from '@angular/google-maps'; + + @Component({ + template: '', + imports: [MyClusterer, MapMarker] + }) + export class MyComp {} + `), + ); + }); + + it('should migrate a re-exported clusterer', async () => { + writeFile( + '/index.ts', + ` + export {MapMarkerClusterer} from '@angular/google-maps'; + export {MapMarkerClusterer as AliasedMapMarkerClusterer} from '@angular/google-maps'; + `, + ); + + await runMigration(); + + const content = tree.readContent('/index.ts'); + + expect(stripWhitespace(content)).toBe( + stripWhitespace(` + export {DeprecatedMapMarkerClusterer as MapMarkerClusterer} from '@angular/google-maps'; + export {DeprecatedMapMarkerClusterer as AliasedMapMarkerClusterer} from '@angular/google-maps'; + `), + ); + }); + + it('should not migrate an import outside of the Angular module', async () => { + writeFile( + '/my-comp.ts', + ` + import {Component} from '@angular/core'; + import {MapMarkerClusterer} from '@not-angular/google-maps'; + + @Component({ + template: '', + imports: [MapMarkerClusterer] + }) + export class MyComp {} + `, + ); + + await runMigration(); + + const content = tree.readContent('/my-comp.ts'); + + expect(stripWhitespace(content)).toBe( + stripWhitespace(` + import {Component} from '@angular/core'; + import {MapMarkerClusterer} from '@not-angular/google-maps'; + + @Component({ + template: '', + imports: [MapMarkerClusterer] + }) + export class MyComp {} + `), + ); + }); +}); diff --git a/src/tsconfig-legacy.json b/src/tsconfig-legacy.json index 9decccd4c33c..d667d62584e4 100644 --- a/src/tsconfig-legacy.json +++ b/src/tsconfig-legacy.json @@ -18,6 +18,11 @@ "./cdk/schematics/**/*", "./material/schematics/**/*", "./components-examples/**/*", + "./google-maps/schematics/**/*", + "./youtube-player/schematics/**/*", + "./material-moment-adapter/schematics/**/*", + "./material-luxon-adapter/schematics/**/*", + "./material-date-fns-adapter/schematics/**/*", "./material/core/theming/tests/**/*" ], "angularCompilerOptions": {