Skip to content

Commit

Permalink
[Map] Add support for libraries for Google Bridge, inject provider'…
Browse files Browse the repository at this point in the history
…s SDK (`L` or `google`) to dispatched events
  • Loading branch information
Kocal authored and kbond committed Aug 12, 2024
1 parent c3e42ab commit 2dbb169
Show file tree
Hide file tree
Showing 16 changed files with 261 additions and 86 deletions.
3 changes: 1 addition & 2 deletions src/Map/assets/dist/abstract_map_controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
protected map: Map;
protected markers: Array<Marker>;
protected infoWindows: Array<InfoWindow>;
initialize(): void;
connect(): void;
protected abstract doCreateMap({ center, zoom, options, }: {
center: Point | null;
Expand All @@ -53,5 +52,5 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
marker: Marker;
}): InfoWindow;
protected abstract doFitBoundsToMarkers(): void;
private dispatchEvent;
protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
}
4 changes: 0 additions & 4 deletions src/Map/assets/dist/abstract_map_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ class default_1 extends Controller {
this.markers = [];
this.infoWindows = [];
}
initialize() { }
connect() {
const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
this.dispatchEvent('pre-connect', { options });
Expand Down Expand Up @@ -35,9 +34,6 @@ class default_1 extends Controller {
this.infoWindows.push(infoWindow);
return infoWindow;
}
dispatchEvent(name, payload = {}) {
this.dispatch(name, { prefix: 'ux:map', detail: payload });
}
}
default_1.values = {
providerOptions: Object,
Expand Down
6 changes: 1 addition & 5 deletions src/Map/assets/src/abstract_map_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ export default abstract class<
protected markers: Array<Marker> = [];
protected infoWindows: Array<InfoWindow> = [];

initialize() {}

connect() {
const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;

Expand Down Expand Up @@ -136,7 +134,5 @@ export default abstract class<

protected abstract doFitBoundsToMarkers(): void;

private dispatchEvent(name: string, payload: Record<string, unknown> = {}): void {
this.dispatch(name, { prefix: 'ux:map', detail: payload });
}
protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
}
8 changes: 8 additions & 0 deletions src/Map/assets/test/abstract_map_controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@ import { Application } from '@hotwired/stimulus';
import { getByTestId, waitFor } from '@testing-library/dom';
import { clearDOM, mountDOM } from '@symfony/stimulus-testing';
import AbstractMapController from '../src/abstract_map_controller.ts';
import * as L from 'leaflet';

class MyMapController extends AbstractMapController {
protected dispatchEvent(name: string, payload: Record<string, unknown> = {}): void {
this.dispatch(name, {
prefix: 'ux:map',
detail: payload,
});
}

doCreateMap({ center, zoom, options }) {
return { map: 'map', center, zoom, options };
}
Expand Down
74 changes: 65 additions & 9 deletions src/Map/src/Bridge/Google/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@ UX_MAP_DSN=google://GOOGLE_MAPS_API_KEY@default
# With options
UX_MAP_DSN=google://GOOGLE_MAPS_API_KEY@default?version=weekly
UX_MAP_DSN=google://GOOGLE_MAPS_API_KEY@default?language=fr&region=FR
UX_MAP_DSN=google://GOOGLE_MAPS_API_KEY@default??libraries[]=geometry&libraries[]=places
```

Available options:

| Option | Description | Default |
|------------|------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------|
| `id` | The id of the script tag | `__googleMapsScriptId` |
| `language` | Force language, see [list of supported languages](https://developers.google.com/maps/faq#languagesupport) specified in the browser | The user's preferred language |
| `region` | Unicode region subtag identifiers compatible with [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1) | |
| `nonce` | Use a cryptographic nonce attribute | |
| `retries` | The number of script load retries | 3 |
| `url` | Custom url to load the Google Maps API script | `https://maps.googleapis.com/maps/api/js` |
| `version` | The release channels or version numbers | `weekly` |
| Option | Description | Default |
|-------------|------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------|
| `id` | The id of the script tag | `__googleMapsScriptId` |
| `language` | Force language, see [list of supported languages](https://developers.google.com/maps/faq#languagesupport) specified in the browser | The user's preferred language |
| `region` | Unicode region subtag identifiers compatible with [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1) | |
| `nonce` | Use a cryptographic nonce attribute | |
| `retries` | The number of script load retries | 3 |
| `url` | Custom url to load the Google Maps API script | `https://maps.googleapis.com/maps/api/js` |
| `version` | The release channels or version numbers | `weekly` |
| `libraries` | The additional libraries to load, see [list of supported libraries](https://googlemaps.github.io/js-api-loader/types/Library.html) | `['maps', 'marker']`, those two libraries are always loaded |

## Map options

Expand Down Expand Up @@ -78,6 +80,60 @@ $googleOptions = (new GoogleOptions())
// Add the custom options to the map
$map->options($googleOptions);
```
## Use cases

Below are some common or advanced use cases when using a map.

### Customize the marker

A common use case is to customize the marker. You can listen to the `ux:map:marker:before-create` event to customize the marker before it is created.

Assuming you have a map with a custom controller:
```twig
{{ render_map(map, {'data-controller': 'my-map' }) }}
```

You can create a Stimulus controller to customize the markers before they are created:
```js
// assets/controllers/my_map_controller.js
import {Controller} from "@hotwired/stimulus";

export default class extends Controller
{
connect() {
this.element.addEventListener('ux:map:marker:before-create', this._onMarkerBeforeCreate);
}

disconnect() {
// Always remove listeners when the controller is disconnected
this.element.removeEventListener('ux:map:marker:before-create', this._onMarkerBeforeCreate);
}

_onMarkerBeforeCreate(event) {
// You can access the marker definition and the google object
// Note: `definition.rawOptions` is the raw options object that will be passed to the `google.maps.Marker` constructor.
const { definition, google } = event.detail;

// 1. To use a custom image for the marker
const beachFlagImg = document.createElement("img");
// Note: instead of using an hardcoded URL, you can use the `extra` parameter from `new Marker()` (PHP) and access it here with `definition.extra`.
beachFlagImg.src = "https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png";
definition.rawOptions = {
content: beachFlagImg
}

// 2. To use a custom glyph for the marker
const pinElement = new google.maps.marker.PinElement({
// Note: instead of using an hardcoded URL, you can use the `extra` parameter from `new Marker()` (PHP) and access it here with `definition.extra`.
glyph: new URL('https://maps.gstatic.com/mapfiles/place_api/icons/v2/museum_pinlet.svg'),
glyphColor: "white",
});
definition.rawOptions = {
content: pinElement.element,
}
}
}
```

## Resources

Expand Down
3 changes: 2 additions & 1 deletion src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export default class extends AbstractMapController<MapOptions, google.maps.Map,
static values: {
providerOptions: ObjectConstructor;
};
providerOptionsValue: Pick<LoaderOptions, 'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version'>;
providerOptionsValue: Pick<LoaderOptions, 'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version' | 'libraries'>;
connect(): Promise<void>;
protected dispatchEvent(name: string, payload?: Record<string, unknown>): void;
protected doCreateMap({ center, zoom, options, }: {
center: Point | null;
zoom: number | null;
Expand Down
38 changes: 28 additions & 10 deletions src/Map/src/Bridge/Google/assets/dist/map_controller.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
import { Loader } from '@googlemaps/js-api-loader';

let loader;
let library;
let _google;
class default_1 extends AbstractMapController {
async connect() {
if (!loader) {
loader = new Loader(this.providerOptionsValue);
if (!_google) {
_google = { maps: {} };
let { libraries = [], ...loaderOptions } = this.providerOptionsValue;
const loader = new Loader(loaderOptions);
libraries = ['core', ...libraries.filter((library) => library !== 'core')];
const librariesImplementations = await Promise.all(libraries.map((library) => loader.importLibrary(library)));
librariesImplementations.map((libraryImplementation, index) => {
const library = libraries[index];
if (['marker', 'places', 'geometry', 'journeySharing', 'drawing', 'visualization'].includes(library)) {
_google.maps[library] = libraryImplementation;
}
else {
_google.maps = { ..._google.maps, ...libraryImplementation };
}
});
}
const { Map: _Map, InfoWindow } = await loader.importLibrary('maps');
const { AdvancedMarkerElement } = await loader.importLibrary('marker');
library = { _Map, AdvancedMarkerElement, InfoWindow };
super.connect();
}
dispatchEvent(name, payload = {}) {
this.dispatch(name, {
prefix: 'ux:map',
detail: {
...payload,
google: _google,
},
});
}
doCreateMap({ center, zoom, options, }) {
options.zoomControl = typeof options.zoomControlOptions !== 'undefined';
options.mapTypeControl = typeof options.mapTypeControlOptions !== 'undefined';
options.streetViewControl = typeof options.streetViewControlOptions !== 'undefined';
options.fullscreenControl = typeof options.fullscreenControlOptions !== 'undefined';
return new library._Map(this.element, {
return new _google.maps.Map(this.element, {
...options,
center,
zoom,
});
}
doCreateMarker(definition) {
const { position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
const marker = new library.AdvancedMarkerElement({
const marker = new _google.maps.marker.AdvancedMarkerElement({
position,
title,
...otherOptions,
Expand All @@ -40,7 +58,7 @@ class default_1 extends AbstractMapController {
}
doCreateInfoWindow({ definition, marker, }) {
const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;
const infoWindow = new library.InfoWindow({
const infoWindow = new _google.maps.InfoWindow({
headerContent: this.createTextOrElement(headerContent),
content: this.createTextOrElement(content),
...otherOptions,
Expand Down
55 changes: 39 additions & 16 deletions src/Map/src/Bridge/Google/assets/src/map_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,7 @@ type MapOptions = Pick<
| 'fullscreenControlOptions'
>;

let loader: Loader;
let library: {
_Map: typeof google.maps.Map;
AdvancedMarkerElement: typeof google.maps.marker.AdvancedMarkerElement;
InfoWindow: typeof google.maps.InfoWindow;
};
let _google: typeof google;

export default class extends AbstractMapController<
MapOptions,
Expand All @@ -47,21 +42,49 @@ export default class extends AbstractMapController<

declare providerOptionsValue: Pick<
LoaderOptions,
'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version'
'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version' | 'libraries'
>;

async connect() {
if (!loader) {
loader = new Loader(this.providerOptionsValue);
if (!_google) {
_google = { maps: {} };

let { libraries = [], ...loaderOptions } = this.providerOptionsValue;

const loader = new Loader(loaderOptions);

// We could have used `loader.load()` to correctly load libraries, but this method is deprecated in favor of `loader.importLibrary()`.
// But `loader.importLibrary()` is not a 1-1 replacement for `loader.load()`, we need to re-build the `google.maps` object ourselves,
// see https://github.com/googlemaps/js-api-loader/issues/837 for more information.
libraries = ['core', ...libraries.filter((library) => library !== 'core')]; // Ensure 'core' is loaded first
const librariesImplementations = await Promise.all(
libraries.map((library) => loader.importLibrary(library))
);
librariesImplementations.map((libraryImplementation, index) => {
const library = libraries[index];

// The following libraries are in a sub-namespace
if (['marker', 'places', 'geometry', 'journeySharing', 'drawing', 'visualization'].includes(library)) {
_google.maps[library] = libraryImplementation;
} else {
_google.maps = { ..._google.maps, ...libraryImplementation };
}
});
}

const { Map: _Map, InfoWindow } = await loader.importLibrary('maps');
const { AdvancedMarkerElement } = await loader.importLibrary('marker');
library = { _Map, AdvancedMarkerElement, InfoWindow };

super.connect();
}

protected dispatchEvent(name: string, payload: Record<string, unknown> = {}): void {
this.dispatch(name, {
prefix: 'ux:map',
detail: {
...payload,
google: _google,
},
});
}

protected doCreateMap({
center,
zoom,
Expand All @@ -77,7 +100,7 @@ export default class extends AbstractMapController<
options.streetViewControl = typeof options.streetViewControlOptions !== 'undefined';
options.fullscreenControl = typeof options.fullscreenControlOptions !== 'undefined';

return new library._Map(this.element, {
return new _google.maps.Map(this.element, {
...options,
center,
zoom,
Expand All @@ -89,7 +112,7 @@ export default class extends AbstractMapController<
): google.maps.marker.AdvancedMarkerElement {
const { position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;

const marker = new library.AdvancedMarkerElement({
const marker = new _google.maps.marker.AdvancedMarkerElement({
position,
title,
...otherOptions,
Expand All @@ -116,7 +139,7 @@ export default class extends AbstractMapController<
}): google.maps.InfoWindow {
const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;

const infoWindow = new library.InfoWindow({
const infoWindow = new _google.maps.InfoWindow({
headerContent: this.createTextOrElement(headerContent),
content: this.createTextOrElement(content),
...otherOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('GoogleMapsController', () => {
data-testid="map"
data-controller="check google"
style="height&#x3A;&#x20;700px&#x3B;&#x20;margin&#x3A;&#x20;10px"
data-google-provider-options-value="&#x7B;&quot;language&quot;&#x3A;&quot;fr&quot;,&quot;region&quot;&#x3A;&quot;FR&quot;,&quot;retries&quot;&#x3A;10,&quot;version&quot;&#x3A;&quot;weekly&quot;,&quot;apiKey&quot;&#x3A;&quot;&quot;&#x7D;"
data-google-provider-options-value="&#x7B;&quot;version&quot;&#x3A;&quot;weekly&quot;,&quot;libraries&quot;&#x3A;&#x5B;&quot;maps&quot;,&quot;marker&quot;&#x5D;,&quot;apiKey&quot;&#x3A;&quot;&quot;&#x7D;"
data-google-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;&quot;YOUR_MAP_ID&quot;,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControl&quot;&#x3A;true,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControl&quot;&#x3A;true,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControl&quot;&#x3A;true,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControl&quot;&#x3A;true,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;&#x7D;"
></div>
`);
Expand Down
Loading

0 comments on commit 2dbb169

Please sign in to comment.