Skip to content

[Map] Allows Map options customization in ux:map:pre-connect event (e.g.: zoom, options, bridgeOptions...) #2861

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/Map/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ $map->addRectangle(new Rectangle(
```

- Deprecate property `rawOptions` from `ux:map:*:before-create` events, in favor of `bridgeOptions` instead.
- Map options can now be configured and overridden through the `ux:map:pre-connect` event:
```js
this.element.addEventListener('ux:map:pre-connect', (event) => {
// Override the map center and zoom
event.detail.zoom = 10;
event.detail.center = { lat: 48.856613, lng: 2.352222 };

// Override the normalized `*Options` PHP classes (e.g. `GoogleMapOptions` or `LeafletMapOptions`)
console.log(event.detail.options);

// Override the options specific to the renderer bridge (e.g. `google.maps.MapOptions` or `L.MapOptions`)
event.detail.bridgeOptions = {
// ...
};
});
```

## 2.26

Expand Down
14 changes: 9 additions & 5 deletions src/Map/assets/dist/abstract_map_controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export type Icon = {
type: typeof IconTypes.Svg;
html: string;
});
export type MapDefinition<MapOptions, BridgeMapOptions> = {
center: Point | null;
zoom: number | null;
options: MapOptions;
bridgeOptions?: BridgeMapOptions;
};
export type MarkerDefinition<BridgeMarkerOptions, BridgeInfoWindowOptions> = WithIdentifier<{
position: Point;
title: string | null;
Expand Down Expand Up @@ -79,7 +85,7 @@ export type InfoWindowDefinition<BridgeInfoWindowOptions> = {
bridgeOptions?: BridgeInfoWindowOptions;
extra: Record<string, unknown>;
};
export default abstract class<MapOptions, BridgeMap, BridgeMarkerOptions, BridgeMarker, BridgeInfoWindowOptions, BridgeInfoWindow, BridgePolygonOptions, BridgePolygon, BridgePolylineOptions, BridgePolyline, BridgeCircleOptions, BridgeCircle, BridgeRectangleOptions, BridgeRectangle> extends Controller<HTMLElement> {
export default abstract class<MapOptions, BridgeMapOptions, BridgeMap, BridgeMarkerOptions, BridgeMarker, BridgeInfoWindowOptions, BridgeInfoWindow, BridgePolygonOptions, BridgePolygon, BridgePolylineOptions, BridgePolyline, BridgeCircleOptions, BridgeCircle, BridgeRectangleOptions, BridgeRectangle> extends Controller<HTMLElement> {
static values: {
providerOptions: ObjectConstructor;
center: ObjectConstructor;
Expand Down Expand Up @@ -136,10 +142,8 @@ export default abstract class<MapOptions, BridgeMap, BridgeMarkerOptions, Bridge
polylinesValueChanged(): void;
circlesValueChanged(): void;
rectanglesValueChanged(): void;
protected abstract doCreateMap({ center, zoom, options }: {
center: Point | null;
zoom: number | null;
options: MapOptions;
protected abstract doCreateMap({ definition }: {
definition: MapDefinition<MapOptions, BridgeMapOptions>;
}): BridgeMap;
protected abstract doFitBoundsToMarkers(): void;
protected abstract doCreateMarker({ definition }: {
Expand Down
14 changes: 7 additions & 7 deletions src/Map/assets/dist/abstract_map_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ class default_1 extends Controller {
this.isConnected = false;
}
connect() {
const options = this.optionsValue;
this.dispatchEvent('pre-connect', { options });
const mapDefinition = {
center: this.hasCenterValue ? this.centerValue : null,
zoom: this.hasZoomValue ? this.zoomValue : null,
options: this.optionsValue,
};
this.dispatchEvent('pre-connect', mapDefinition);
this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this));
this.createPolygon = this.createDrawingFactory('polygon', this.polygons, this.doCreatePolygon.bind(this));
this.createPolyline = this.createDrawingFactory('polyline', this.polylines, this.doCreatePolyline.bind(this));
this.createCircle = this.createDrawingFactory('circle', this.circles, this.doCreateCircle.bind(this));
this.createRectangle = this.createDrawingFactory('rectangle', this.rectangles, this.doCreateRectangle.bind(this));
this.map = this.doCreateMap({
center: this.hasCenterValue ? this.centerValue : null,
zoom: this.hasZoomValue ? this.zoomValue : null,
options,
});
this.map = this.doCreateMap({ definition: mapDefinition });
this.markersValue.forEach((definition) => this.createMarker({ definition }));
this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
Expand Down
29 changes: 20 additions & 9 deletions src/Map/assets/src/abstract_map_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ export type Icon = {
}
);

export type MapDefinition<MapOptions, BridgeMapOptions> = {
center: Point | null;
zoom: number | null;
options: MapOptions;
/**
* Additional options passed to the Map constructor.
* These options are specific to the Map Bridge, and can be defined through `ux:map:pre-connect` event.
*/
bridgeOptions?: BridgeMapOptions;
};

export type MarkerDefinition<BridgeMarkerOptions, BridgeInfoWindowOptions> = WithIdentifier<{
position: Point;
title: string | null;
Expand Down Expand Up @@ -182,6 +193,7 @@ export type InfoWindowDefinition<BridgeInfoWindowOptions> = {

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

connect() {
const options = this.optionsValue;

this.dispatchEvent('pre-connect', { options });
const mapDefinition: MapDefinition<MapOptions, BridgeMapOptions> = {
center: this.hasCenterValue ? this.centerValue : null,
zoom: this.hasZoomValue ? this.zoomValue : null,
options: this.optionsValue,
};
this.dispatchEvent('pre-connect', mapDefinition);

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

this.map = this.doCreateMap({
center: this.hasCenterValue ? this.centerValue : null,
zoom: this.hasZoomValue ? this.zoomValue : null,
options,
});
this.map = this.doCreateMap({ definition: mapDefinition });
this.markersValue.forEach((definition) => this.createMarker({ definition }));
this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
Expand Down Expand Up @@ -356,7 +367,7 @@ export default abstract class<
//endregion

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

protected abstract doFitBoundsToMarkers(): void;

Expand Down
4 changes: 2 additions & 2 deletions src/Map/assets/test/abstract_map_controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class MyMapController extends AbstractMapController {
});
}

doCreateMap({ center, zoom, options }) {
return { map: 'map', center, zoom, options };
doCreateMap({ definition }) {
return { map: 'map', center: definition.center, zoom: definition.zoom, options: definition.options };
}

doCreateMarker({ definition }) {
Expand Down
88 changes: 86 additions & 2 deletions src/Map/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Configuration is done in your ``config/packages/ux_map.yaml`` file:
# without to manually configure it in each map instance (through "new GoogleOptions(mapId: 'your_map_id')").
default_map_id: null

The ``UX_MAP_DSN`` environment variable configure which renderer to use.
The ``UX_MAP_DSN`` environment variable configure which renderer (Bridge) to use.

Map renderers
~~~~~~~~~~~~~
Expand Down Expand Up @@ -362,6 +362,10 @@ Symfony UX Map allows you to extend its default behavior using a custom Stimulus
this.element.addEventListener('ux:map:polygon:after-create', this._onPolygonAfterCreate);
this.element.addEventListener('ux:map:polyline:before-create', this._onPolylineBeforeCreate);
this.element.addEventListener('ux:map:polyline:after-create', this._onPolylineAfterCreate);
this.element.addEventListener('ux:map:circle:before-create', this._onCircleBeforeCreate);
this.element.addEventListener('ux:map:circle:after-create', this._onCircleAfterCreate);
this.element.addEventListener('ux:map:rectangle:before-create', this._onRectangleBeforeCreate);
this.element.addEventListener('ux:map:rectangle:after-create', this._onRectangleAfterCreate);
}

disconnect() {
Expand All @@ -376,14 +380,31 @@ Symfony UX Map allows you to extend its default behavior using a custom Stimulus
this.element.removeEventListener('ux:map:polygon:after-create', this._onPolygonAfterCreate);
this.element.removeEventListener('ux:map:polyline:before-create', this._onPolylineBeforeCreate);
this.element.removeEventListener('ux:map:polyline:after-create', this._onPolylineAfterCreate);
this.element.removeEventListener('ux:map:circle:before-create', this._onCircleBeforeCreate);
this.element.removeEventListener('ux:map:circle:after-create', this._onCircleAfterCreate);
this.element.removeEventListener('ux:map:rectangle:before-create', this._onRectangleBeforeCreate);
this.element.removeEventListener('ux:map:rectangle:after-create', this._onRectangleAfterCreate);
}

/**
* This event is triggered when the map is not created yet
* You can use this event to configure the map before it is created
*/
_onPreConnect(event) {
// You can read or write the zoom level
console.log(event.detail.zoom);

// You can read or write the center of the map
console.log(event.detail.center);

// You can read or write map options, specific to the Bridge, it represents the normalized `*Options` PHP class (e.g. `GoogleOptions`, `LeafletOptions`)
console.log(event.detail.options);

// Finally, you can also set Bridge-specific options that will be used when creating the map.
event.detail.bridgeOptions = {
preferCanvas: true, // e.g. for Leaflet (https://leafletjs.com/reference.html#map-prefercanvas)
backgroundColor: '#f0f0f0', // e.g. for Google Maps (https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions.backgroundColor)
}
}

/**
Expand Down Expand Up @@ -442,6 +463,10 @@ Symfony UX Map allows you to extend its default behavior using a custom Stimulus
console.log(event.detail.polygon);
// ... or a polyline
console.log(event.detail.polyline);
// ... or a circle
console.log(event.detail.circle);
// ... or a rectangle
console.log(event.detail.rectangle);
}

/**
Expand Down Expand Up @@ -479,6 +504,26 @@ Symfony UX Map allows you to extend its default behavior using a custom Stimulus
// The polyline instance
console.log(event.detail.polyline);
}

_onCircleBeforeCreate(event) {
console.log(event.detail.definition);
// { title: 'My circle', center: { lat: 48.8566, lng: 2.3522 }, radius: 1000, ... }
}

_onCircleAfterCreate(event) {
// The circle instance
console.log(event.detail.circle);
}

_onRectangleBeforeCreate(event) {
console.log(event.detail.definition);
// { title: 'My rectangle', southWest: { lat: 48.8566, lng: 2.3522 }, northEast: { lat: 45.7640, lng: 4.8357 }, ... }
}

_onRectangleAfterCreate(event) {
// The rectangle instance
console.log(event.detail.rectangle);
}
}

Then, you can use this controller in your template:
Expand Down Expand Up @@ -521,6 +566,17 @@ events ``ux:map:*:before-create`` using the special ``bridgeOptions`` property:
this.element.addEventListener('ux:map:info-window:before-create', this._onInfoWindowBeforeCreate);
this.element.addEventListener('ux:map:polygon:before-create', this._onPolygonBeforeCreate);
this.element.addEventListener('ux:map:polyline:before-create', this._onPolylineBeforeCreate);
this.element.addEventListener('ux:map:circle:before-create', this._onCircleBeforeCreate);
this.element.addEventListener('ux:map:rectangle:before-create', this._onRectangleBeforeCreate);
}

disconnect() {
this.element.removeEventListener('ux:map:marker:before-create', this._onMarkerBeforeCreate);
this.element.removeEventListener('ux:map:info-window:before-create', this._onInfoWindowBeforeCreate);
this.element.removeEventListener('ux:map:polygon:before-create', this._onPolygonBeforeCreate);
this.element.removeEventListener('ux:map:polyline:before-create', this._onPolylineBeforeCreate);
this.element.removeEventListener('ux:map:circle:before-create', this._onCircleBeforeCreate);
this.element.removeEventListener('ux:map:rectangle:before-create', this._onRectangleBeforeCreate);
}

_onMarkerBeforeCreate(event) {
Expand Down Expand Up @@ -578,6 +634,34 @@ events ``ux:map:*:before-create`` using the special ``bridgeOptions`` property:
// ...
};
}

_onCircleBeforeCreate(event) {
// When using Google Maps, to configure a `google.maps.Circle`
event.detail.definition.bridgeOptions = {
strokeColor: 'red',
// ...
};

// When using Leaflet, to configure a `L.Circle`
event.detail.definition.bridgeOptions = {
color: 'red',
// ...
};
}

_onRectangleBeforeCreate(event) {
// When using Google Maps, to configure a `google.maps.Rectangle`
event.detail.definition.bridgeOptions = {
strokeColor: 'red',
// ...
};

// When using Leaflet, to configure a `L.Rectangle`
event.detail.definition.bridgeOptions = {
color: 'red',
// ...
};
}
}

Advanced: Passing extra data from PHP to the Stimulus controller
Expand All @@ -591,7 +675,7 @@ These additional data points are defined and used exclusively by you; UX Map
only forwards them to the Stimulus controller.

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

$map->addMarker(new Marker(
position: new Point(48.822248, 2.337338),
Expand Down
11 changes: 4 additions & 7 deletions src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import type { LoaderOptions } from '@googlemaps/js-api-loader';
import type { CircleDefinition, Icon, InfoWindowDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition, RectangleDefinition } from '@symfony/ux-map';
import type { CircleDefinition, Icon, InfoWindowDefinition, MapDefinition, MarkerDefinition, PolygonDefinition, PolylineDefinition, RectangleDefinition } from '@symfony/ux-map';
import AbstractMapController from '@symfony/ux-map';
type MapOptions = Pick<google.maps.MapOptions, 'mapId' | 'gestureHandling' | 'backgroundColor' | 'disableDoubleClickZoom' | 'zoomControl' | 'zoomControlOptions' | 'mapTypeControl' | 'mapTypeControlOptions' | 'streetViewControl' | 'streetViewControlOptions' | 'fullscreenControl' | 'fullscreenControlOptions'>;
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> {
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> {
providerOptionsValue: Pick<LoaderOptions, 'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version' | 'libraries'>;
map: google.maps.Map;
parser: DOMParser;
connect(): Promise<void>;
centerValueChanged(): void;
zoomValueChanged(): void;
protected dispatchEvent(name: string, payload?: Record<string, unknown>): void;
protected doCreateMap({ center, zoom, options }: {
center: Point | null;
zoom: number | null;
options: MapOptions;
protected doCreateMap({ definition }: {
definition: MapDefinition<MapOptions, google.maps.MapOptions>;
}): google.maps.Map;
protected doCreateMarker({ definition, }: {
definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>;
Expand Down
Loading