Repository: symfony/ux
Pull request #2863 (2025-06-23 12:20:23)

[Map] Add `extra` data to `Map`
| 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 #2810 

Like for `Marker` and cie, `extra` data can now be defined on the `Map` in order to pass extra data from your PHP code to your custom Stimulus Controller, through `ux:map:pre-connect` and `ux:map:connect` events.

Combo-ed with `bridgeOptions` from #2861, it means that you can fully customize the Map creation given extra data given your PHP code (e.g.: injecting some API keys):
```php
$map = new Map(extra: ['vector_layer_api_key' => 'bar']);
// or
$map->extra(['foo' => 'bar']);
```
```js
this.element.addEventListener('ux:map:pre-connect', (event) => {
    const { detail } = event;

    console.log(detail.extra); // {'foo': 'bar'} 
    
    if (detail.extra.foo === 'bar') {
        detail.extra.bridgeOptions = { a_bridge_specific_option: 'foobar' };
    }
});
```


Patch:
diff --git a/src/Map/CHANGELOG.md b/src/Map/CHANGELOG.md
index e68d950a..1e1c7b38 100644
--- a/src/Map/CHANGELOG.md
+++ b/src/Map/CHANGELOG.md
@@ -39,6 +39,7 @@ this.element.addEventListener('ux:map:pre-connect', (event) => {
     };
 });
 ```
+-  Add `extra` data support to `Map`, which can be accessed in `ux:map:pre-connect` and `ux:map:connect` events
 
 ## 2.26
 
diff --git a/src/Map/assets/dist/abstract_map_controller.d.ts b/src/Map/assets/dist/abstract_map_controller.d.ts
index 93adcb1d..f4b24236 100644
--- a/src/Map/assets/dist/abstract_map_controller.d.ts
+++ b/src/Map/assets/dist/abstract_map_controller.d.ts
@@ -7,6 +7,7 @@ export type Identifier = string;
 export type WithIdentifier<T extends Record<string, unknown>> = T & {
     '@id': Identifier;
 };
+type ExtraData = Record<string, unknown>;
 export declare const IconTypes: {
     readonly Url: "url";
     readonly Svg: "svg";
@@ -31,6 +32,7 @@ export type MapDefinition<MapOptions, BridgeMapOptions> = {
     zoom: number | null;
     options: MapOptions;
     bridgeOptions?: BridgeMapOptions;
+    extra: ExtraData;
 };
 export type MarkerDefinition<BridgeMarkerOptions, BridgeInfoWindowOptions> = WithIdentifier<{
     position: Point;
@@ -39,7 +41,7 @@ export type MarkerDefinition<BridgeMarkerOptions, BridgeInfoWindowOptions> = Wit
     icon?: Icon;
     rawOptions?: BridgeMarkerOptions;
     bridgeOptions?: BridgeMarkerOptions;
-    extra: Record<string, unknown>;
+    extra: ExtraData;
 }>;
 export type PolygonDefinition<BridgePolygonOptions, BridgeInfoWindowOptions> = WithIdentifier<{
     infoWindow?: Omit<InfoWindowDefinition<BridgeInfoWindowOptions>, 'position'>;
@@ -47,7 +49,7 @@ export type PolygonDefinition<BridgePolygonOptions, BridgeInfoWindowOptions> = W
     title: string | null;
     rawOptions?: BridgePolygonOptions;
     bridgeOptions?: BridgePolygonOptions;
-    extra: Record<string, unknown>;
+    extra: ExtraData;
 }>;
 export type PolylineDefinition<BridgePolylineOptions, BridgeInfoWindowOptions> = WithIdentifier<{
     infoWindow?: Omit<InfoWindowDefinition<BridgeInfoWindowOptions>, 'position'>;
@@ -55,7 +57,7 @@ export type PolylineDefinition<BridgePolylineOptions, BridgeInfoWindowOptions> =
     title: string | null;
     rawOptions?: BridgePolylineOptions;
     bridgeOptions?: BridgePolylineOptions;
-    extra: Record<string, unknown>;
+    extra: ExtraData;
 }>;
 export type CircleDefinition<BridgeCircleOptions, BridgeInfoWindowOptions> = WithIdentifier<{
     infoWindow?: Omit<InfoWindowDefinition<BridgeInfoWindowOptions>, 'position'>;
@@ -64,7 +66,7 @@ export type CircleDefinition<BridgeCircleOptions, BridgeInfoWindowOptions> = Wit
     title: string | null;
     rawOptions?: BridgeCircleOptions;
     bridgeOptions?: BridgeCircleOptions;
-    extra: Record<string, unknown>;
+    extra: ExtraData;
 }>;
 export type RectangleDefinition<BridgeRectangleOptions, BridgeInfoWindowOptions> = WithIdentifier<{
     infoWindow?: Omit<InfoWindowDefinition<BridgeInfoWindowOptions>, 'position'>;
@@ -73,7 +75,7 @@ export type RectangleDefinition<BridgeRectangleOptions, BridgeInfoWindowOptions>
     title: string | null;
     rawOptions?: BridgeRectangleOptions;
     bridgeOptions?: BridgeRectangleOptions;
-    extra: Record<string, unknown>;
+    extra: ExtraData;
 }>;
 export type InfoWindowDefinition<BridgeInfoWindowOptions> = {
     headerContent: string | null;
@@ -83,7 +85,7 @@ export type InfoWindowDefinition<BridgeInfoWindowOptions> = {
     autoClose: boolean;
     rawOptions?: BridgeInfoWindowOptions;
     bridgeOptions?: BridgeInfoWindowOptions;
-    extra: Record<string, unknown>;
+    extra: ExtraData;
 };
 export default abstract class<MapOptions, BridgeMapOptions, BridgeMap, BridgeMarkerOptions, BridgeMarker, BridgeInfoWindowOptions, BridgeInfoWindow, BridgePolygonOptions, BridgePolygon, BridgePolylineOptions, BridgePolyline, BridgeCircleOptions, BridgeCircle, BridgeRectangleOptions, BridgeRectangle> extends Controller<HTMLElement> {
     static values: {
@@ -97,6 +99,7 @@ export default abstract class<MapOptions, BridgeMapOptions, BridgeMap, BridgeMar
         circles: ArrayConstructor;
         rectangles: ArrayConstructor;
         options: ObjectConstructor;
+        extra: ObjectConstructor;
     };
     centerValue: Point | null;
     zoomValue: number | null;
@@ -107,6 +110,7 @@ export default abstract class<MapOptions, BridgeMapOptions, BridgeMap, BridgeMar
     circlesValue: Array<CircleDefinition<BridgeCircleOptions, BridgeInfoWindowOptions>>;
     rectanglesValue: Array<RectangleDefinition<BridgeRectangleOptions, BridgeInfoWindowOptions>>;
     optionsValue: MapOptions;
+    extraValue: Record<string, unknown>;
     hasCenterValue: boolean;
     hasZoomValue: boolean;
     hasFitBoundsToMarkersValue: boolean;
@@ -116,6 +120,7 @@ export default abstract class<MapOptions, BridgeMapOptions, BridgeMap, BridgeMar
     hasCirclesValue: boolean;
     hasRectanglesValue: boolean;
     hasOptionsValue: boolean;
+    hasExtraValue: boolean;
     protected map: BridgeMap;
     protected markers: Map<string, BridgeMarker>;
     protected polygons: Map<string, BridgePolygon>;
@@ -177,3 +182,4 @@ export default abstract class<MapOptions, BridgeMapOptions, BridgeMap, BridgeMar
     private createDrawingFactory;
     private onDrawChanged;
 }
+export {};
diff --git a/src/Map/assets/dist/abstract_map_controller.js b/src/Map/assets/dist/abstract_map_controller.js
index 58a6001b..7b237c8a 100644
--- a/src/Map/assets/dist/abstract_map_controller.js
+++ b/src/Map/assets/dist/abstract_map_controller.js
@@ -17,10 +17,12 @@ class default_1 extends Controller {
         this.isConnected = false;
     }
     connect() {
+        const extra = this.hasExtraValue ? this.extraValue : {};
         const mapDefinition = {
             center: this.hasCenterValue ? this.centerValue : null,
             zoom: this.hasZoomValue ? this.zoomValue : null,
             options: this.optionsValue,
+            extra,
         };
         this.dispatchEvent('pre-connect', mapDefinition);
         this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this));
@@ -45,6 +47,7 @@ class default_1 extends Controller {
             circles: [...this.circles.values()],
             rectangles: [...this.rectangles.values()],
             infoWindows: this.infoWindows,
+            extra,
         });
         this.isConnected = true;
     }
@@ -130,6 +133,7 @@ default_1.values = {
     circles: Array,
     rectangles: Array,
     options: Object,
+    extra: Object,
 };
 
 export { IconTypes, default_1 as default };
diff --git a/src/Map/assets/src/abstract_map_controller.ts b/src/Map/assets/src/abstract_map_controller.ts
index 9086061b..75f950c1 100644
--- a/src/Map/assets/src/abstract_map_controller.ts
+++ b/src/Map/assets/src/abstract_map_controller.ts
@@ -13,6 +13,8 @@ export type Point = { lat: number; lng: number };
 export type Identifier = string;
 export type WithIdentifier<T extends Record<string, unknown>> = T & { '@id': Identifier };
 
+type ExtraData = Record<string, unknown>;
+
 export const IconTypes = {
     Url: 'url',
     Svg: 'svg',
@@ -46,6 +48,13 @@ export type MapDefinition<MapOptions, BridgeMapOptions> = {
      * These options are specific to the Map Bridge, and can be defined through `ux:map:pre-connect` event.
      */
     bridgeOptions?: BridgeMapOptions;
+    /**
+     * Extra data defined by the developer.
+     * They are not directly used by the Stimulus controller, but they can be used by the developer with event listeners:
+     *    - `ux:map:pre-connect`
+     *    - `ux:map:connect`
+     */
+    extra: ExtraData;
 };
 
 export type MarkerDefinition<BridgeMarkerOptions, BridgeInfoWindowOptions> = WithIdentifier<{
@@ -69,7 +78,7 @@ export type MarkerDefinition<BridgeMarkerOptions, BridgeInfoWindowOptions> = Wit
      *    - `ux:map:marker:before-create`
      *    - `ux:map:marker:after-create`
      */
-    extra: Record<string, unknown>;
+    extra: ExtraData;
 }>;
 
 export type PolygonDefinition<BridgePolygonOptions, BridgeInfoWindowOptions> = WithIdentifier<{
@@ -92,7 +101,7 @@ export type PolygonDefinition<BridgePolygonOptions, BridgeInfoWindowOptions> = W
      *    - `ux:map:polygon:before-create`
      *    - `ux:map:polygon:after-create`
      */
-    extra: Record<string, unknown>;
+    extra: ExtraData;
 }>;
 
 export type PolylineDefinition<BridgePolylineOptions, BridgeInfoWindowOptions> = WithIdentifier<{
@@ -115,7 +124,7 @@ export type PolylineDefinition<BridgePolylineOptions, BridgeInfoWindowOptions> =
      *    - `ux:map:polyline:before-create`
      *    - `ux:map:polyline:after-create`
      */
-    extra: Record<string, unknown>;
+    extra: ExtraData;
 }>;
 
 export type CircleDefinition<BridgeCircleOptions, BridgeInfoWindowOptions> = WithIdentifier<{
@@ -139,7 +148,7 @@ export type CircleDefinition<BridgeCircleOptions, BridgeInfoWindowOptions> = Wit
      *    - `ux:map:circle:before-create`
      *    - `ux:map:circle:after-create`
      */
-    extra: Record<string, unknown>;
+    extra: ExtraData;
 }>;
 
 export type RectangleDefinition<BridgeRectangleOptions, BridgeInfoWindowOptions> = WithIdentifier<{
@@ -163,7 +172,7 @@ export type RectangleDefinition<BridgeRectangleOptions, BridgeInfoWindowOptions>
      *    - `ux:map:rectangle:before-create`
      *    - `ux:map:rectangle:after-create`
      */
-    extra: Record<string, unknown>;
+    extra: ExtraData;
 }>;
 
 export type InfoWindowDefinition<BridgeInfoWindowOptions> = {
@@ -188,7 +197,7 @@ export type InfoWindowDefinition<BridgeInfoWindowOptions> = {
      *    - `ux:map:info-window:before-create`
      *    - `ux:map:info-window:after-create`
      */
-    extra: Record<string, unknown>;
+    extra: ExtraData;
 };
 
 export default abstract class<
@@ -219,6 +228,7 @@ export default abstract class<
         circles: Array,
         rectangles: Array,
         options: Object,
+        extra: Object,
     };
 
     declare centerValue: Point | null;
@@ -230,6 +240,7 @@ export default abstract class<
     declare circlesValue: Array<CircleDefinition<BridgeCircleOptions, BridgeInfoWindowOptions>>;
     declare rectanglesValue: Array<RectangleDefinition<BridgeRectangleOptions, BridgeInfoWindowOptions>>;
     declare optionsValue: MapOptions;
+    declare extraValue: Record<string, unknown>;
 
     declare hasCenterValue: boolean;
     declare hasZoomValue: boolean;
@@ -240,6 +251,7 @@ export default abstract class<
     declare hasCirclesValue: boolean;
     declare hasRectanglesValue: boolean;
     declare hasOptionsValue: boolean;
+    declare hasExtraValue: boolean;
 
     protected map: BridgeMap;
     protected markers = new Map<Identifier, BridgeMarker>();
@@ -259,10 +271,12 @@ export default abstract class<
     protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
 
     connect() {
+        const extra = this.hasExtraValue ? this.extraValue : {};
         const mapDefinition: MapDefinition<MapOptions, BridgeMapOptions> = {
             center: this.hasCenterValue ? this.centerValue : null,
             zoom: this.hasZoomValue ? this.zoomValue : null,
             options: this.optionsValue,
+            extra,
         };
         this.dispatchEvent('pre-connect', mapDefinition);
 
@@ -291,6 +305,7 @@ export default abstract class<
             circles: [...this.circles.values()],
             rectangles: [...this.rectangles.values()],
             infoWindows: this.infoWindows,
+            extra,
         });
 
         this.isConnected = true;
diff --git a/src/Map/doc/index.rst b/src/Map/doc/index.rst
index a7fc6795..e01cfbd7 100644
--- a/src/Map/doc/index.rst
+++ b/src/Map/doc/index.rst
@@ -713,6 +713,39 @@ On the JavaScript side, you can access your extra data via the
         // ...
     }
 
+.. versionadded:: 2.27
+
+    The ``Map`` class now has an ``extra`` property, which can be accessed in the ``ux:map:pre-connect`` and ``ux:map:connect`` events::
+
+        $map = new Map(/* ... */, extra: [
+            'foo' => 'bar',
+        ]);
+        // or
+        $map->extra([
+            'foo' => 'bar',
+        ]);
+
+    .. code-block:: javascript
+
+        // assets/controllers/mymap_controller.js
+
+        import { Controller } from '@hotwired/stimulus';
+
+        export default class extends Controller {
+
+            // ...
+
+            _onPreConnect(event) {
+                console.log(event.detail.extra);
+                // { foo: 'bar', ... }
+            }
+
+            _onConnect(event) {
+                console.log(event.detail.extra);
+                // { foo: 'bar', ... }
+            }
+        }
+
 .. _map-live-component:
 
 Usage with Live Components
diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.js b/src/Map/src/Bridge/Google/assets/dist/map_controller.js
index 909154a9..7c4813a9 100644
--- a/src/Map/src/Bridge/Google/assets/dist/map_controller.js
+++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.js
@@ -18,10 +18,12 @@ class default_1 extends Controller {
         this.isConnected = false;
     }
     connect() {
+        const extra = this.hasExtraValue ? this.extraValue : {};
         const mapDefinition = {
             center: this.hasCenterValue ? this.centerValue : null,
             zoom: this.hasZoomValue ? this.zoomValue : null,
             options: this.optionsValue,
+            extra,
         };
         this.dispatchEvent('pre-connect', mapDefinition);
         this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this));
@@ -46,6 +48,7 @@ class default_1 extends Controller {
             circles: [...this.circles.values()],
             rectangles: [...this.rectangles.values()],
             infoWindows: this.infoWindows,
+            extra,
         });
         this.isConnected = true;
     }
@@ -131,6 +134,7 @@ default_1.values = {
     circles: Array,
     rectangles: Array,
     options: Object,
+    extra: Object,
 };
 
 let _google;
diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
index da3cbc82..3e61359e 100644
--- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
+++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
@@ -19,10 +19,12 @@ class default_1 extends Controller {
         this.isConnected = false;
     }
     connect() {
+        const extra = this.hasExtraValue ? this.extraValue : {};
         const mapDefinition = {
             center: this.hasCenterValue ? this.centerValue : null,
             zoom: this.hasZoomValue ? this.zoomValue : null,
             options: this.optionsValue,
+            extra,
         };
         this.dispatchEvent('pre-connect', mapDefinition);
         this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this));
@@ -47,6 +49,7 @@ class default_1 extends Controller {
             circles: [...this.circles.values()],
             rectangles: [...this.rectangles.values()],
             infoWindows: this.infoWindows,
+            extra,
         });
         this.isConnected = true;
     }
@@ -132,6 +135,7 @@ default_1.values = {
     circles: Array,
     rectangles: Array,
     options: Object,
+    extra: Object,
 };
 
 class map_controller extends default_1 {
diff --git a/src/Map/src/Circle.php b/src/Map/src/Circle.php
index 74786eb0..711f4a24 100644
--- a/src/Map/src/Circle.php
+++ b/src/Map/src/Circle.php
@@ -21,7 +21,9 @@ use Symfony\UX\Map\Exception\InvalidArgumentException;
 final class Circle implements Element
 {
     /**
-     * @param array<string, mixed> $extra  Extra data, can be used by the developer to store additional information and use them later JavaScript side
+     * @param array<string, mixed> $extra  Extra data forwarded to the JavaScript side. It can be used in your custom
+     *                                     Stimulus controller to benefit from greater flexibility and customization.
+     *                                     These data must be serializable to JSON. These data are not used by UX Map.
      * @param float                $radius The radius of the circle in meters
      */
     public function __construct(
diff --git a/src/Map/src/Map.php b/src/Map/src/Map.php
index 340399e8..672369e2 100644
--- a/src/Map/src/Map.php
+++ b/src/Map/src/Map.php
@@ -27,11 +27,14 @@ final class Map
     private Rectangles $rectangles;
 
     /**
-     * @param Marker[]     $markers
-     * @param Polygon[]    $polygons
-     * @param Polyline[]   $polylines
-     * @param Circle[]     $circles
-     * @param Rectangles[] $rectangles
+     * @param Marker[]             $markers
+     * @param Polygon[]            $polygons
+     * @param Polyline[]           $polylines
+     * @param Circle[]             $circles
+     * @param Rectangles[]         $rectangles
+     * @param array<string, mixed> $extra      Extra data forwarded to the JavaScript side. It can be used in your custom
+     *                                         Stimulus controller to benefit from greater flexibility and customization.
+     *                                         These data must be serializable to JSON. These data are not used by UX Map.
      */
     public function __construct(
         private readonly ?string $rendererName = null,
@@ -44,6 +47,7 @@ final class Map
         array $polylines = [],
         array $circles = [],
         array $rectangles = [],
+        private array $extra = [],
     ) {
         $this->markers = new Markers($markers);
         $this->polygons = new Polygons($polygons);
@@ -165,6 +169,18 @@ final class Map
         return $this;
     }
 
+    /**
+     * @param array<string, mixed> $extra Extra data forwarded to the JavaScript side. It can be used in your custom
+     *                                    Stimulus controller to benefit from greater flexibility and customization.
+     *                                    These data must be serializable to JSON. These data are not used by UX Map.
+     */
+    public function extra(array $extra): self
+    {
+        $this->extra = $extra;
+
+        return $this;
+    }
+
     public function toArray(): array
     {
         if (!$this->fitBoundsToMarkers) {
@@ -187,6 +203,9 @@ final class Map
             'polylines' => $this->polylines->toArray(),
             'circles' => $this->circles->toArray(),
             'rectangles' => $this->rectangles->toArray(),
+            // Send `null` if empty instead of `[]`, because Stimulus Controller values validation expect an Object,
+            // and sending `(object) $this->extra` mess with LiveComponent hydration checksum validation
+            'extra' => [] === $this->extra ? null : $this->extra,
         ];
     }
 
@@ -201,6 +220,7 @@ final class Map
      *     rectangles?: list<array>,
      *     fitBoundsToMarkers?: bool,
      *     options?: array<string, mixed>,
+     *     extra?: array<string, mixed>,
      * } $map
      *
      * @internal
@@ -245,6 +265,8 @@ final class Map
         }
         $map['rectangles'] = array_map(Rectangle::fromArray(...), $map['rectangles']);
 
+        $map['extra'] ??= [];
+
         return new self(...$map);
     }
 }
diff --git a/src/Map/src/Marker.php b/src/Map/src/Marker.php
index ed58c484..62e0395e 100644
--- a/src/Map/src/Marker.php
+++ b/src/Map/src/Marker.php
@@ -13,7 +13,6 @@ namespace Symfony\UX\Map;
 
 use Symfony\UX\Map\Exception\InvalidArgumentException;
 use Symfony\UX\Map\Icon\Icon;
-use Symfony\UX\Map\Icon\IconType;
 
 /**
  * Represents a marker on a map.
@@ -23,8 +22,9 @@ use Symfony\UX\Map\Icon\IconType;
 final class Marker implements Element
 {
     /**
-     * @param array<string, mixed> $extra Extra data, can be used by the developer to store additional information and
-     *                                    use them later JavaScript side
+     * @param array<string, mixed> $extra Extra data forwarded to the JavaScript side. It can be used in your custom
+     *                                    Stimulus controller to benefit from greater flexibility and customization.
+     *                                    These data must be serializable to JSON. These data are not used by UX Map.
      */
     public function __construct(
         public readonly Point $position,
diff --git a/src/Map/src/Polygon.php b/src/Map/src/Polygon.php
index 2622634c..180c7e13 100644
--- a/src/Map/src/Polygon.php
+++ b/src/Map/src/Polygon.php
@@ -22,7 +22,9 @@ final class Polygon implements Element
 {
     /**
      * @param array<Point>|array<array<Point>> $points a list of point representing the polygon, or a list of paths (each path is an array of points) representing a polygon with holes
-     * @param array<string, mixed>             $extra  Extra data, can be used by the developer to store additional information and use them later JavaScript side
+     * @param array<string, mixed>             $extra  Extra data forwarded to the JavaScript side. It can be used in your custom
+     *                                                 Stimulus controller to benefit from greater flexibility and customization.
+     *                                                 These data must be serializable to JSON. These data are not used by UX Map.
      */
     public function __construct(
         private readonly array $points,
diff --git a/src/Map/src/Polyline.php b/src/Map/src/Polyline.php
index 9a51e62b..4279456e 100644
--- a/src/Map/src/Polyline.php
+++ b/src/Map/src/Polyline.php
@@ -21,7 +21,9 @@ use Symfony\UX\Map\Exception\InvalidArgumentException;
 final class Polyline implements Element
 {
     /**
-     * @param array<string, mixed> $extra Extra data, can be used by the developer to store additional information and use them later JavaScript side
+     * @param array<string, mixed> $extra Extra data forwarded to the JavaScript side. It can be used in your custom
+     *                                    Stimulus controller to benefit from greater flexibility and customization.
+     *                                    These data must be serializable to JSON. These data are not used by UX Map.
      */
     public function __construct(
         private readonly array $points,
diff --git a/src/Map/src/Rectangle.php b/src/Map/src/Rectangle.php
index 98146097..71479495 100644
--- a/src/Map/src/Rectangle.php
+++ b/src/Map/src/Rectangle.php
@@ -20,6 +20,11 @@ use Symfony\UX\Map\Exception\InvalidArgumentException;
  */
 final class Rectangle implements Element
 {
+    /**
+     * @param array<string, mixed> $extra Extra data forwarded to the JavaScript side. It can be used in your custom
+     *                                    Stimulus controller to benefit from greater flexibility and customization.
+     *                                    These data must be serializable to JSON. These data are not used by UX Map.
+     */
     public function __construct(
         public readonly Point $southWest,
         public readonly Point $northEast,


Test patch:
diff --git a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php
index d40a1e71..0106e1bc 100644
--- a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php
+++ b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php
@@ -188,5 +188,16 @@ class GoogleRendererTest extends RendererTestCase
                 ->addMarker(new Marker(position: new Point(45.7640, 4.8357), title: 'Lyon', icon: Icon::ux('fa:map-marker')->width(32)->height(32)))
                 ->addMarker(new Marker(position: new Point(45.8566, 2.3522), title: 'Dijon', icon: Icon::svg('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">...</svg>'))),
         ];
+
+        yield 'with map extra data' => [
+            'renderer' => new GoogleRenderer(new StimulusHelper(null), new UxIconRenderer(null), apiKey: 'api_key'),
+            'map' => (new Map())
+                ->center(new Point(48.8566, 2.3522))
+                ->zoom(12)
+                ->extra([
+                    'foo' => 'bar',
+                    'baz' => 42,
+                ]),
+        ];
     }
 }
diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with map extra data__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with map extra data__1.txt
new file mode 100644
index 00000000..41c3440c
--- /dev/null
+++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with map extra data__1.txt
@@ -0,0 +1,16 @@
+<!-- This HTML has been prettified for testing purposes, and may not represent the actual HTML output.
+     Run "php vendor/bin/phpunit -d --update-snapshots" to update the snapshot. -->
+<div
+  data-controller="symfony--ux-google-map--map"
+  data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}"
+  data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}"
+  data-symfony--ux-google-map--map-zoom-value="12"
+  data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false"
+  data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}"
+  data-symfony--ux-google-map--map-markers-value="[]"
+  data-symfony--ux-google-map--map-polygons-value="[]"
+  data-symfony--ux-google-map--map-polylines-value="[]"
+  data-symfony--ux-google-map--map-circles-value="[]"
+  data-symfony--ux-google-map--map-rectangles-value="[]"
+  data-symfony--ux-google-map--map-extra-value="{&quot;foo&quot;:&quot;bar&quot;,&quot;baz&quot;:42}"
+></div>
\ No newline at end of file
diff --git a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
index 07490459..5af90585 100644
--- a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
+++ b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
@@ -142,5 +142,13 @@ class LeafletRendererTest extends RendererTestCase
                 ->addMarker(new Marker(position: new Point(45.7640, 4.8357), title: 'Lyon', icon: Icon::ux('fa:map-marker')->width(32)->height(32)))
                 ->addMarker(new Marker(position: new Point(45.8566, 2.3522), title: 'Dijon', icon: Icon::svg('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">...</svg>'))),
         ];
+
+        yield 'with map extra data' => [
+            'renderer' => new LeafletRenderer(new StimulusHelper(null), new UxIconRenderer(null)),
+            'map' => (new Map())
+                ->center(new Point(48.8566, 2.3522))
+                ->zoom(12)
+                ->extra(['key1' => 'value1', 'key2' => 'value2']),
+        ];
     }
 }
diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with map extra data__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with map extra data__1.txt
new file mode 100644
index 00000000..ead1c000
--- /dev/null
+++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with map extra data__1.txt
@@ -0,0 +1,16 @@
+<!-- This HTML has been prettified for testing purposes, and may not represent the actual HTML output.
+     Run "php vendor/bin/phpunit -d --update-snapshots" to update the snapshot. -->
+<div
+  data-controller="symfony--ux-leaflet-map--map"
+  data-symfony--ux-leaflet-map--map-provider-options-value="{}"
+  data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}"
+  data-symfony--ux-leaflet-map--map-zoom-value="12"
+  data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false"
+  data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;attributionControlOptions&quot;:{&quot;position&quot;:&quot;bottomright&quot;,&quot;prefix&quot;:&quot;Leaflet&quot;},&quot;zoomControlOptions&quot;:{&quot;position&quot;:&quot;topleft&quot;,&quot;zoomInText&quot;:&quot;&lt;span aria-hidden=\&quot;true\&quot;&gt;+&lt;\/span&gt;&quot;,&quot;zoomInTitle&quot;:&quot;Zoom in&quot;,&quot;zoomOutText&quot;:&quot;&lt;span aria-hidden=\&quot;true\&quot;&gt;&amp;#x2212;&lt;\/span&gt;&quot;,&quot;zoomOutTitle&quot;:&quot;Zoom out&quot;},&quot;@provider&quot;:&quot;leaflet&quot;}"
+  data-symfony--ux-leaflet-map--map-markers-value="[]"
+  data-symfony--ux-leaflet-map--map-polygons-value="[]"
+  data-symfony--ux-leaflet-map--map-polylines-value="[]"
+  data-symfony--ux-leaflet-map--map-circles-value="[]"
+  data-symfony--ux-leaflet-map--map-rectangles-value="[]"
+  data-symfony--ux-leaflet-map--map-extra-value="{&quot;key1&quot;:&quot;value1&quot;,&quot;key2&quot;:&quot;value2&quot;}"
+></div>
\ No newline at end of file
diff --git a/src/Map/tests/MapTest.php b/src/Map/tests/MapTest.php
index 06d09a24..a09e5866 100644
--- a/src/Map/tests/MapTest.php
+++ b/src/Map/tests/MapTest.php
@@ -72,6 +72,7 @@ class MapTest extends TestCase
             'polylines' => [],
             'circles' => [],
             'rectangles' => [],
+            'extra' => null,
         ], $array);
     }
 
@@ -94,6 +95,7 @@ class MapTest extends TestCase
             'polylines' => [],
             'circles' => [],
             'rectangles' => [],
+            'extra' => null,
         ], $array);
     }
 
@@ -217,6 +219,11 @@ class MapTest extends TestCase
                     autoClose: true,
                 ),
             ))
+            ->extra([
+                'foo' => 'bar',
+                'bar' => true,
+                'baz' => ['qux' => 'quux'],
+            ])
         ;
 
         self::assertEquals([
@@ -401,6 +408,11 @@ class MapTest extends TestCase
                     'id' => null,
                 ],
             ],
+            'extra' => [
+                'foo' => 'bar',
+                'bar' => true,
+                'baz' => ['qux' => 'quux'],
+            ],
         ], $map->toArray());
     }
 }