Skip to content

Commit efe1666

Browse files
committed
feature #2861 [Map] Allows Map options customization in ux:map:pre-connect event (e.g.: zoom, options, bridgeOptions...) (Kocal)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Map] Allows Map options customization in `ux:map:pre-connect` event (e.g.: `zoom`, `options`, `bridgeOptions`...) | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Docs? | yes <!-- required for new features --> | Issues | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> | License | MIT <!-- Replace this notice by a description of your feature/bugfix. This will help reviewers and should be a good start for the documentation. Additionally (see https://symfony.com/releases): - Always add tests and ensure they pass. - For new features, provide some code snippets to help understand usage. - Features and deprecations must be submitted against branch main. - Update/add documentation as required (we can help!) - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry - Never break backward compatibility (see https://symfony.com/bc). --> This feature allows users to override the options passed to `L.Map()` or `new google.maps.Map()` trough the `ux:map:pre-connect` event. The configurable options are: - zoom - center - options - bridgeOptions, not injected by default, but can be defined on-the-fly This is a blocking step for #2810, but also for one of my need when I wanted to persist the zoom/center in the URL, to share my map (https://hugo.alliau.me/places 😛) : ```js _onMapPreConnect = (event) => { const { L } = event.detail; if (window.location.hash) { try { const state = Object.fromEntries(new URLSearchParams(window.location.hash.slice(1))); const zoom = Number(state.z); const center = state.center.split(",").map(Number); event.detail.zoom = zoom; event.detail.center = L.latLng(center[0], center[1]); } catch (e) { console.error("Invalid state in URL hash:", e); } } } _onMapConnect = (event) => { const { map } = event.detail; const updateState = () => { const center = map.getCenter(); const zoom = map.getZoom(); const state = { z: zoom, center: [center.lat.toFixed(5), center.lng.toFixed(5)], }; window.history.replaceState(state, "", `#${new URLSearchParams(state).toString()}`); }; map.addEventListener("zoom", () => updateState()); map.addEventListener("move", () => updateState()); }; ``` Commits ------- afc7db6 [Map] Add missing doc for Circle and Rectangle b0463b7 [Map] Allows Map options customization in `ux:map:pre-connect` event (e.g.: `zoom`, `options`, `bridgeOptions`...)
2 parents 523c591 + afc7db6 commit efe1666

File tree

12 files changed

+194
-82
lines changed

12 files changed

+194
-82
lines changed

src/Map/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@ $map->addRectangle(new Rectangle(
2323
```
2424

2525
- Deprecate property `rawOptions` from `ux:map:*:before-create` events, in favor of `bridgeOptions` instead.
26+
- Map options can now be configured and overridden through the `ux:map:pre-connect` event:
27+
```js
28+
this.element.addEventListener('ux:map:pre-connect', (event) => {
29+
// Override the map center and zoom
30+
event.detail.zoom = 10;
31+
event.detail.center = { lat: 48.856613, lng: 2.352222 };
32+
33+
// Override the normalized `*Options` PHP classes (e.g. `GoogleMapOptions` or `LeafletMapOptions`)
34+
console.log(event.detail.options);
35+
36+
// Override the options specific to the renderer bridge (e.g. `google.maps.MapOptions` or `L.MapOptions`)
37+
event.detail.bridgeOptions = {
38+
// ...
39+
};
40+
});
41+
```
2642

2743
## 2.26
2844

src/Map/assets/dist/abstract_map_controller.d.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ export type Icon = {
2626
type: typeof IconTypes.Svg;
2727
html: string;
2828
});
29+
export type MapDefinition<MapOptions, BridgeMapOptions> = {
30+
center: Point | null;
31+
zoom: number | null;
32+
options: MapOptions;
33+
bridgeOptions?: BridgeMapOptions;
34+
};
2935
export type MarkerDefinition<BridgeMarkerOptions, BridgeInfoWindowOptions> = WithIdentifier<{
3036
position: Point;
3137
title: string | null;
@@ -79,7 +85,7 @@ export type InfoWindowDefinition<BridgeInfoWindowOptions> = {
7985
bridgeOptions?: BridgeInfoWindowOptions;
8086
extra: Record<string, unknown>;
8187
};
82-
export default abstract class<MapOptions, BridgeMap, BridgeMarkerOptions, BridgeMarker, BridgeInfoWindowOptions, BridgeInfoWindow, BridgePolygonOptions, BridgePolygon, BridgePolylineOptions, BridgePolyline, BridgeCircleOptions, BridgeCircle, BridgeRectangleOptions, BridgeRectangle> extends Controller<HTMLElement> {
88+
export default abstract class<MapOptions, BridgeMapOptions, BridgeMap, BridgeMarkerOptions, BridgeMarker, BridgeInfoWindowOptions, BridgeInfoWindow, BridgePolygonOptions, BridgePolygon, BridgePolylineOptions, BridgePolyline, BridgeCircleOptions, BridgeCircle, BridgeRectangleOptions, BridgeRectangle> extends Controller<HTMLElement> {
8389
static values: {
8490
providerOptions: ObjectConstructor;
8591
center: ObjectConstructor;
@@ -136,10 +142,8 @@ export default abstract class<MapOptions, BridgeMap, BridgeMarkerOptions, Bridge
136142
polylinesValueChanged(): void;
137143
circlesValueChanged(): void;
138144
rectanglesValueChanged(): void;
139-
protected abstract doCreateMap({ center, zoom, options }: {
140-
center: Point | null;
141-
zoom: number | null;
142-
options: MapOptions;
145+
protected abstract doCreateMap({ definition }: {
146+
definition: MapDefinition<MapOptions, BridgeMapOptions>;
143147
}): BridgeMap;
144148
protected abstract doFitBoundsToMarkers(): void;
145149
protected abstract doCreateMarker({ definition }: {

src/Map/assets/dist/abstract_map_controller.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@ class default_1 extends Controller {
1717
this.isConnected = false;
1818
}
1919
connect() {
20-
const options = this.optionsValue;
21-
this.dispatchEvent('pre-connect', { options });
20+
const mapDefinition = {
21+
center: this.hasCenterValue ? this.centerValue : null,
22+
zoom: this.hasZoomValue ? this.zoomValue : null,
23+
options: this.optionsValue,
24+
};
25+
this.dispatchEvent('pre-connect', mapDefinition);
2226
this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this));
2327
this.createPolygon = this.createDrawingFactory('polygon', this.polygons, this.doCreatePolygon.bind(this));
2428
this.createPolyline = this.createDrawingFactory('polyline', this.polylines, this.doCreatePolyline.bind(this));
2529
this.createCircle = this.createDrawingFactory('circle', this.circles, this.doCreateCircle.bind(this));
2630
this.createRectangle = this.createDrawingFactory('rectangle', this.rectangles, this.doCreateRectangle.bind(this));
27-
this.map = this.doCreateMap({
28-
center: this.hasCenterValue ? this.centerValue : null,
29-
zoom: this.hasZoomValue ? this.zoomValue : null,
30-
options,
31-
});
31+
this.map = this.doCreateMap({ definition: mapDefinition });
3232
this.markersValue.forEach((definition) => this.createMarker({ definition }));
3333
this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
3434
this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));

src/Map/assets/src/abstract_map_controller.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ export type Icon = {
3737
}
3838
);
3939

40+
export type MapDefinition<MapOptions, BridgeMapOptions> = {
41+
center: Point | null;
42+
zoom: number | null;
43+
options: MapOptions;
44+
/**
45+
* Additional options passed to the Map constructor.
46+
* These options are specific to the Map Bridge, and can be defined through `ux:map:pre-connect` event.
47+
*/
48+
bridgeOptions?: BridgeMapOptions;
49+
};
50+
4051
export type MarkerDefinition<BridgeMarkerOptions, BridgeInfoWindowOptions> = WithIdentifier<{
4152
position: Point;
4253
title: string | null;
@@ -182,6 +193,7 @@ export type InfoWindowDefinition<BridgeInfoWindowOptions> = {
182193

183194
export default abstract class<
184195
MapOptions, // Normalized `*Options` PHP class from Bridge (to not be confused with the JS Map class options)
196+
BridgeMapOptions, // The options for the JavaScript Map class from Bridge
185197
BridgeMap, // The JavaScript Map class from Bridge (e.g.: `L.Map` for Leaflet, `google.maps.Map` for Google Maps)
186198
BridgeMarkerOptions, // The options for the JavaScript Marker class from Bridge
187199
BridgeMarker, // The JavaScript Marker class from Bridge
@@ -247,21 +259,20 @@ export default abstract class<
247259
protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
248260

249261
connect() {
250-
const options = this.optionsValue;
251-
252-
this.dispatchEvent('pre-connect', { options });
262+
const mapDefinition: MapDefinition<MapOptions, BridgeMapOptions> = {
263+
center: this.hasCenterValue ? this.centerValue : null,
264+
zoom: this.hasZoomValue ? this.zoomValue : null,
265+
options: this.optionsValue,
266+
};
267+
this.dispatchEvent('pre-connect', mapDefinition);
253268

254269
this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this));
255270
this.createPolygon = this.createDrawingFactory('polygon', this.polygons, this.doCreatePolygon.bind(this));
256271
this.createPolyline = this.createDrawingFactory('polyline', this.polylines, this.doCreatePolyline.bind(this));
257272
this.createCircle = this.createDrawingFactory('circle', this.circles, this.doCreateCircle.bind(this));
258273
this.createRectangle = this.createDrawingFactory('rectangle', this.rectangles, this.doCreateRectangle.bind(this));
259274

260-
this.map = this.doCreateMap({
261-
center: this.hasCenterValue ? this.centerValue : null,
262-
zoom: this.hasZoomValue ? this.zoomValue : null,
263-
options,
264-
});
275+
this.map = this.doCreateMap({ definition: mapDefinition });
265276
this.markersValue.forEach((definition) => this.createMarker({ definition }));
266277
this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
267278
this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
@@ -356,7 +367,7 @@ export default abstract class<
356367
//endregion
357368

358369
//region Abstract factory methods to be implemented by the concrete classes, they are specific to the map provider
359-
protected abstract doCreateMap({ center, zoom, options }: { center: Point | null; zoom: number | null; options: MapOptions }): BridgeMap;
370+
protected abstract doCreateMap({ definition }: { definition: MapDefinition<MapOptions, BridgeMapOptions> }): BridgeMap;
360371

361372
protected abstract doFitBoundsToMarkers(): void;
362373

src/Map/assets/test/abstract_map_controller.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class MyMapController extends AbstractMapController {
1212
});
1313
}
1414

15-
doCreateMap({ center, zoom, options }) {
16-
return { map: 'map', center, zoom, options };
15+
doCreateMap({ definition }) {
16+
return { map: 'map', center: definition.center, zoom: definition.zoom, options: definition.options };
1717
}
1818

1919
doCreateMarker({ definition }) {

src/Map/doc/index.rst

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Configuration is done in your ``config/packages/ux_map.yaml`` file:
3333
# without to manually configure it in each map instance (through "new GoogleOptions(mapId: 'your_map_id')").
3434
default_map_id: null
3535
36-
The ``UX_MAP_DSN`` environment variable configure which renderer to use.
36+
The ``UX_MAP_DSN`` environment variable configure which renderer (Bridge) to use.
3737

3838
Map renderers
3939
~~~~~~~~~~~~~
@@ -362,6 +362,10 @@ Symfony UX Map allows you to extend its default behavior using a custom Stimulus
362362
this.element.addEventListener('ux:map:polygon:after-create', this._onPolygonAfterCreate);
363363
this.element.addEventListener('ux:map:polyline:before-create', this._onPolylineBeforeCreate);
364364
this.element.addEventListener('ux:map:polyline:after-create', this._onPolylineAfterCreate);
365+
this.element.addEventListener('ux:map:circle:before-create', this._onCircleBeforeCreate);
366+
this.element.addEventListener('ux:map:circle:after-create', this._onCircleAfterCreate);
367+
this.element.addEventListener('ux:map:rectangle:before-create', this._onRectangleBeforeCreate);
368+
this.element.addEventListener('ux:map:rectangle:after-create', this._onRectangleAfterCreate);
365369
}
366370
367371
disconnect() {
@@ -376,14 +380,31 @@ Symfony UX Map allows you to extend its default behavior using a custom Stimulus
376380
this.element.removeEventListener('ux:map:polygon:after-create', this._onPolygonAfterCreate);
377381
this.element.removeEventListener('ux:map:polyline:before-create', this._onPolylineBeforeCreate);
378382
this.element.removeEventListener('ux:map:polyline:after-create', this._onPolylineAfterCreate);
383+
this.element.removeEventListener('ux:map:circle:before-create', this._onCircleBeforeCreate);
384+
this.element.removeEventListener('ux:map:circle:after-create', this._onCircleAfterCreate);
385+
this.element.removeEventListener('ux:map:rectangle:before-create', this._onRectangleBeforeCreate);
386+
this.element.removeEventListener('ux:map:rectangle:after-create', this._onRectangleAfterCreate);
379387
}
380388
381389
/**
382390
* This event is triggered when the map is not created yet
383391
* You can use this event to configure the map before it is created
384392
*/
385393
_onPreConnect(event) {
394+
// You can read or write the zoom level
395+
console.log(event.detail.zoom);
396+
397+
// You can read or write the center of the map
398+
console.log(event.detail.center);
399+
400+
// You can read or write map options, specific to the Bridge, it represents the normalized `*Options` PHP class (e.g. `GoogleOptions`, `LeafletOptions`)
386401
console.log(event.detail.options);
402+
403+
// Finally, you can also set Bridge-specific options that will be used when creating the map.
404+
event.detail.bridgeOptions = {
405+
preferCanvas: true, // e.g. for Leaflet (https://leafletjs.com/reference.html#map-prefercanvas)
406+
backgroundColor: '#f0f0f0', // e.g. for Google Maps (https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions.backgroundColor)
407+
}
387408
}
388409
389410
/**
@@ -442,6 +463,10 @@ Symfony UX Map allows you to extend its default behavior using a custom Stimulus
442463
console.log(event.detail.polygon);
443464
// ... or a polyline
444465
console.log(event.detail.polyline);
466+
// ... or a circle
467+
console.log(event.detail.circle);
468+
// ... or a rectangle
469+
console.log(event.detail.rectangle);
445470
}
446471
447472
/**
@@ -479,6 +504,26 @@ Symfony UX Map allows you to extend its default behavior using a custom Stimulus
479504
// The polyline instance
480505
console.log(event.detail.polyline);
481506
}
507+
508+
_onCircleBeforeCreate(event) {
509+
console.log(event.detail.definition);
510+
// { title: 'My circle', center: { lat: 48.8566, lng: 2.3522 }, radius: 1000, ... }
511+
}
512+
513+
_onCircleAfterCreate(event) {
514+
// The circle instance
515+
console.log(event.detail.circle);
516+
}
517+
518+
_onRectangleBeforeCreate(event) {
519+
console.log(event.detail.definition);
520+
// { title: 'My rectangle', southWest: { lat: 48.8566, lng: 2.3522 }, northEast: { lat: 45.7640, lng: 4.8357 }, ... }
521+
}
522+
523+
_onRectangleAfterCreate(event) {
524+
// The rectangle instance
525+
console.log(event.detail.rectangle);
526+
}
482527
}
483528
484529
Then, you can use this controller in your template:
@@ -521,6 +566,17 @@ events ``ux:map:*:before-create`` using the special ``bridgeOptions`` property:
521566
this.element.addEventListener('ux:map:info-window:before-create', this._onInfoWindowBeforeCreate);
522567
this.element.addEventListener('ux:map:polygon:before-create', this._onPolygonBeforeCreate);
523568
this.element.addEventListener('ux:map:polyline:before-create', this._onPolylineBeforeCreate);
569+
this.element.addEventListener('ux:map:circle:before-create', this._onCircleBeforeCreate);
570+
this.element.addEventListener('ux:map:rectangle:before-create', this._onRectangleBeforeCreate);
571+
}
572+
573+
disconnect() {
574+
this.element.removeEventListener('ux:map:marker:before-create', this._onMarkerBeforeCreate);
575+
this.element.removeEventListener('ux:map:info-window:before-create', this._onInfoWindowBeforeCreate);
576+
this.element.removeEventListener('ux:map:polygon:before-create', this._onPolygonBeforeCreate);
577+
this.element.removeEventListener('ux:map:polyline:before-create', this._onPolylineBeforeCreate);
578+
this.element.removeEventListener('ux:map:circle:before-create', this._onCircleBeforeCreate);
579+
this.element.removeEventListener('ux:map:rectangle:before-create', this._onRectangleBeforeCreate);
524580
}
525581
526582
_onMarkerBeforeCreate(event) {
@@ -578,6 +634,34 @@ events ``ux:map:*:before-create`` using the special ``bridgeOptions`` property:
578634
// ...
579635
};
580636
}
637+
638+
_onCircleBeforeCreate(event) {
639+
// When using Google Maps, to configure a `google.maps.Circle`
640+
event.detail.definition.bridgeOptions = {
641+
strokeColor: 'red',
642+
// ...
643+
};
644+
645+
// When using Leaflet, to configure a `L.Circle`
646+
event.detail.definition.bridgeOptions = {
647+
color: 'red',
648+
// ...
649+
};
650+
}
651+
652+
_onRectangleBeforeCreate(event) {
653+
// When using Google Maps, to configure a `google.maps.Rectangle`
654+
event.detail.definition.bridgeOptions = {
655+
strokeColor: 'red',
656+
// ...
657+
};
658+
659+
// When using Leaflet, to configure a `L.Rectangle`
660+
event.detail.definition.bridgeOptions = {
661+
color: 'red',
662+
// ...
663+
};
664+
}
581665
}
582666
583667
Advanced: Passing extra data from PHP to the Stimulus controller
@@ -591,7 +675,7 @@ These additional data points are defined and used exclusively by you; UX Map
591675
only forwards them to the Stimulus controller.
592676

593677
To pass extra data from PHP to the Stimulus controller, you must use the ``extra`` property
594-
available in ``Marker``, ``InfoWindow``, ``Polygon`` and ``Polyline`` instances::
678+
available in ``Marker``, ``InfoWindow``, ``Polygon``, ``Polyline``, ``Circle`` and ``Rectangle`` instances::
595679

596680
$map->addMarker(new Marker(
597681
position: new Point(48.822248, 2.337338),

src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import type { LoaderOptions } from '@googlemaps/js-api-loader';
2-
import type { CircleDefinition, Icon, InfoWindowDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition, RectangleDefinition } from '@symfony/ux-map';
2+
import type { CircleDefinition, Icon, InfoWindowDefinition, MapDefinition, MarkerDefinition, PolygonDefinition, PolylineDefinition, RectangleDefinition } from '@symfony/ux-map';
33
import AbstractMapController from '@symfony/ux-map';
44
type MapOptions = Pick<google.maps.MapOptions, 'mapId' | 'gestureHandling' | 'backgroundColor' | 'disableDoubleClickZoom' | 'zoomControl' | 'zoomControlOptions' | 'mapTypeControl' | 'mapTypeControlOptions' | 'streetViewControl' | 'streetViewControlOptions' | 'fullscreenControl' | 'fullscreenControlOptions'>;
5-
export default class extends AbstractMapController<MapOptions, google.maps.Map, google.maps.marker.AdvancedMarkerElementOptions, google.maps.marker.AdvancedMarkerElement, google.maps.InfoWindowOptions, google.maps.InfoWindow, google.maps.PolygonOptions, google.maps.Polygon, google.maps.PolylineOptions, google.maps.Polyline, google.maps.CircleOptions, google.maps.Circle, google.maps.RectangleOptions, google.maps.Rectangle> {
5+
export default class extends AbstractMapController<MapOptions, google.maps.MapOptions, google.maps.Map, google.maps.marker.AdvancedMarkerElementOptions, google.maps.marker.AdvancedMarkerElement, google.maps.InfoWindowOptions, google.maps.InfoWindow, google.maps.PolygonOptions, google.maps.Polygon, google.maps.PolylineOptions, google.maps.Polyline, google.maps.CircleOptions, google.maps.Circle, google.maps.RectangleOptions, google.maps.Rectangle> {
66
providerOptionsValue: Pick<LoaderOptions, 'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version' | 'libraries'>;
77
map: google.maps.Map;
8-
parser: DOMParser;
98
connect(): Promise<void>;
109
centerValueChanged(): void;
1110
zoomValueChanged(): void;
1211
protected dispatchEvent(name: string, payload?: Record<string, unknown>): void;
13-
protected doCreateMap({ center, zoom, options }: {
14-
center: Point | null;
15-
zoom: number | null;
16-
options: MapOptions;
12+
protected doCreateMap({ definition }: {
13+
definition: MapDefinition<MapOptions, google.maps.MapOptions>;
1714
}): google.maps.Map;
1815
protected doCreateMarker({ definition, }: {
1916
definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>;

0 commit comments

Comments
 (0)