Repository: symfony/ux
Pull request #2406 (2024-11-26 07:38:01)

[Map] Fix and improve TypeScript types, refactor same logic into dedicated methods (to reduce file size)
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| 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.
 - 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 PRs is purely internal, and aims to:
- Making TypeScript happy by fixing types definitions and usages, but also simplify them
- Refactoring methods containing the same logic (ex: `this.create*` or `this....ValueChanged`) methods) to dedicated methods:
```js
    createMarker(definition) {
        this.dispatchEvent('marker:before-create', { definition });
        const marker = this.doCreateMarker(definition);
        this.dispatchEvent('marker:after-create', { marker });
        marker['@id'] = definition['@id'];
        this.markers.set(definition['@id'], marker);
        return marker;
    }
    createPolygon(definition) {
        this.dispatchEvent('polygon:before-create', { definition });
        const polygon = this.doCreatePolygon(definition);
        this.dispatchEvent('polygon:after-create', { polygon });
        polygon['@id'] = definition['@id'];
        this.polygons.set(definition['@id'], polygon);
        return polygon;
    }
    createPolyline(definition) {
        this.dispatchEvent('polyline:before-create', { definition });
        const polyline = this.doCreatePolyline(definition);
        this.dispatchEvent('polyline:after-create', { polyline });
        polyline['@id'] = definition['@id'];
        this.polylines.set(definition['@id'], polyline);
        return polyline;
    }
```
becomes
```js
        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));
```

and
```js
    markersValueChanged() {
        if (!this.map) {
            return;
        }
        this.markers.forEach((marker) => {
            if (!this.markersValue.find((m) => m['@id'] === marker['@id'])) {
                this.removeMarker(marker);
                this.markers.delete(marker['@id']);
            }
        });
        this.markersValue.forEach((marker) => {
            if (!this.markers.has(marker['@id'])) {
                this.createMarker(marker);
            }
        });
        if (this.fitBoundsToMarkersValue) {
            this.doFitBoundsToMarkers();
        }
    }
    polygonsValueChanged() {
        if (!this.map) {
            return;
        }
        this.polygons.forEach((polygon) => {
            if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) {
                this.removePolygon(polygon);
                this.polygons.delete(polygon['@id']);
            }
        });
        this.polygonsValue.forEach((polygon) => {
            if (!this.polygons.has(polygon['@id'])) {
                this.createPolygon(polygon);
            }
        });
    }
    polylinesValueChanged() {
        if (!this.map) {
            return;
        }
        this.polylines.forEach((polyline) => {
            if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) {
                this.removePolyline(polyline);
                this.polylines.delete(polyline['@id']);
            }
        });
        this.polylinesValue.forEach((polyline) => {
            if (!this.polylines.has(polyline['@id'])) {
                this.createPolyline(polyline);
            }
        });
    }
```
becomes
```js
markersValueChanged() {
        if (!this.isConnected) {
            return;
        }
        this.onDrawChanged(this.markers, this.markersValue, this.createMarker, this.doRemoveMarker);
        if (this.fitBoundsToMarkersValue) {
            this.doFitBoundsToMarkers();
        }
    }
    polygonsValueChanged() {
        if (!this.isConnected) {
            return;
        }
        this.onDrawChanged(this.polygons, this.polygonsValue, this.createPolygon, this.doRemovePolygon);
    }
    polylinesValueChanged() {
        if (!this.isConnected) {
            return;
        }
        this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline);
    }
```


Patch:
diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js
index 8894e033..1b9f7568 100644
--- a/src/LiveComponent/assets/dist/live_controller.js
+++ b/src/LiveComponent/assets/dist/live_controller.js
@@ -131,6 +131,82 @@ function getElementAsTagText(element) {
         : element.outerHTML;
 }
 
+let componentMapByElement = new WeakMap();
+let componentMapByComponent = new Map();
+const registerComponent = (component) => {
+    componentMapByElement.set(component.element, component);
+    componentMapByComponent.set(component, component.name);
+};
+const unregisterComponent = (component) => {
+    componentMapByElement.delete(component.element);
+    componentMapByComponent.delete(component);
+};
+const getComponent = (element) => new Promise((resolve, reject) => {
+    let count = 0;
+    const maxCount = 10;
+    const interval = setInterval(() => {
+        const component = componentMapByElement.get(element);
+        if (component) {
+            clearInterval(interval);
+            resolve(component);
+        }
+        count++;
+        if (count > maxCount) {
+            clearInterval(interval);
+            reject(new Error(`Component not found for element ${getElementAsTagText(element)}`));
+        }
+    }, 5);
+});
+const findComponents = (currentComponent, onlyParents, onlyMatchName) => {
+    const components = [];
+    componentMapByComponent.forEach((componentName, component) => {
+        if (onlyParents && (currentComponent === component || !component.element.contains(currentComponent.element))) {
+            return;
+        }
+        if (onlyMatchName && componentName !== onlyMatchName) {
+            return;
+        }
+        components.push(component);
+    });
+    return components;
+};
+const findChildren = (currentComponent) => {
+    const children = [];
+    componentMapByComponent.forEach((componentName, component) => {
+        if (currentComponent === component) {
+            return;
+        }
+        if (!currentComponent.element.contains(component.element)) {
+            return;
+        }
+        let foundChildComponent = false;
+        componentMapByComponent.forEach((childComponentName, childComponent) => {
+            if (foundChildComponent) {
+                return;
+            }
+            if (childComponent === component) {
+                return;
+            }
+            if (childComponent.element.contains(component.element)) {
+                foundChildComponent = true;
+            }
+        });
+        children.push(component);
+    });
+    return children;
+};
+const findParent = (currentComponent) => {
+    let parentElement = currentComponent.element.parentElement;
+    while (parentElement) {
+        const component = componentMapByElement.get(parentElement);
+        if (component) {
+            return component;
+        }
+        parentElement = parentElement.parentElement;
+    }
+    return null;
+};
+
 function getValueFromElement(element, valueStore) {
     if (element instanceof HTMLInputElement) {
         if (element.type === 'checkbox') {
@@ -244,8 +320,16 @@ function elementBelongsToThisComponent(element, component) {
     if (!component.element.contains(element)) {
         return false;
     }
-    const closestLiveComponent = element.closest('[data-controller~="live"]');
-    return closestLiveComponent === component.element;
+    let foundChildComponent = false;
+    findChildren(component).forEach((childComponent) => {
+        if (foundChildComponent) {
+            return;
+        }
+        if (childComponent.element === element || childComponent.element.contains(element)) {
+            foundChildComponent = true;
+        }
+    });
+    return !foundChildComponent;
 }
 function cloneHTMLElement(element) {
     const newElement = element.cloneNode(true);
@@ -1791,82 +1875,6 @@ class ExternalMutationTracker {
     }
 }
 
-let componentMapByElement = new WeakMap();
-let componentMapByComponent = new Map();
-const registerComponent = (component) => {
-    componentMapByElement.set(component.element, component);
-    componentMapByComponent.set(component, component.name);
-};
-const unregisterComponent = (component) => {
-    componentMapByElement.delete(component.element);
-    componentMapByComponent.delete(component);
-};
-const getComponent = (element) => new Promise((resolve, reject) => {
-    let count = 0;
-    const maxCount = 10;
-    const interval = setInterval(() => {
-        const component = componentMapByElement.get(element);
-        if (component) {
-            clearInterval(interval);
-            resolve(component);
-        }
-        count++;
-        if (count > maxCount) {
-            clearInterval(interval);
-            reject(new Error(`Component not found for element ${getElementAsTagText(element)}`));
-        }
-    }, 5);
-});
-const findComponents = (currentComponent, onlyParents, onlyMatchName) => {
-    const components = [];
-    componentMapByComponent.forEach((componentName, component) => {
-        if (onlyParents && (currentComponent === component || !component.element.contains(currentComponent.element))) {
-            return;
-        }
-        if (onlyMatchName && componentName !== onlyMatchName) {
-            return;
-        }
-        components.push(component);
-    });
-    return components;
-};
-const findChildren = (currentComponent) => {
-    const children = [];
-    componentMapByComponent.forEach((componentName, component) => {
-        if (currentComponent === component) {
-            return;
-        }
-        if (!currentComponent.element.contains(component.element)) {
-            return;
-        }
-        let foundChildComponent = false;
-        componentMapByComponent.forEach((childComponentName, childComponent) => {
-            if (foundChildComponent) {
-                return;
-            }
-            if (childComponent === component) {
-                return;
-            }
-            if (childComponent.element.contains(component.element)) {
-                foundChildComponent = true;
-            }
-        });
-        children.push(component);
-    });
-    return children;
-};
-const findParent = (currentComponent) => {
-    let parentElement = currentComponent.element.parentElement;
-    while (parentElement) {
-        const component = componentMapByElement.get(parentElement);
-        if (component) {
-            return component;
-        }
-        parentElement = parentElement.parentElement;
-    }
-    return null;
-};
-
 class Component {
     constructor(element, name, props, listeners, id, backend, elementDriver) {
         this.fingerprint = '';
diff --git a/src/LiveComponent/assets/src/dom_utils.ts b/src/LiveComponent/assets/src/dom_utils.ts
index 28b8d233..d096f6d1 100644
--- a/src/LiveComponent/assets/src/dom_utils.ts
+++ b/src/LiveComponent/assets/src/dom_utils.ts
@@ -2,6 +2,7 @@ import type ValueStore from './Component/ValueStore';
 import { type Directive, parseDirectives } from './Directive/directives_parser';
 import { normalizeModelName } from './string_utils';
 import type Component from './Component';
+import { findChildren } from './ComponentRegistry';
 import getElementAsTagText from './Util/getElementAsTagText';
 
 /**
@@ -199,9 +200,19 @@ export function elementBelongsToThisComponent(element: Element, component: Compo
         return false;
     }
 
-    const closestLiveComponent = element.closest('[data-controller~="live"]');
+    let foundChildComponent = false;
+    findChildren(component).forEach((childComponent) => {
+        if (foundChildComponent) {
+            // return early
+            return;
+        }
+
+        if (childComponent.element === element || childComponent.element.contains(element)) {
+            foundChildComponent = true;
+        }
+    });
 
-    return closestLiveComponent === component.element;
+    return !foundChildComponent;
 }
 
 export function cloneHTMLElement(element: HTMLElement): HTMLElement {
diff --git a/src/Map/assets/dist/abstract_map_controller.d.ts b/src/Map/assets/dist/abstract_map_controller.d.ts
index 7bfd5325..308613a9 100644
--- a/src/Map/assets/dist/abstract_map_controller.d.ts
+++ b/src/Map/assets/dist/abstract_map_controller.d.ts
@@ -3,30 +3,31 @@ export type Point = {
     lat: number;
     lng: number;
 };
-export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
-    '@id': string;
+export type Identifier = string;
+export type WithIdentifier<T extends Record<string, unknown>> = T & {
+    '@id': Identifier;
+};
+export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = WithIdentifier<{
     position: Point;
     title: string | null;
-    infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
+    infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
     rawOptions?: MarkerOptions;
     extra: Record<string, unknown>;
-};
-export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
-    '@id': string;
-    infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
+}>;
+export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = WithIdentifier<{
+    infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
     points: Array<Point>;
     title: string | null;
     rawOptions?: PolygonOptions;
     extra: Record<string, unknown>;
-};
-export type PolylineDefinition<PolylineOptions, InfoWindowOptions> = {
-    '@id': string;
-    infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
+}>;
+export type PolylineDefinition<PolylineOptions, InfoWindowOptions> = WithIdentifier<{
+    infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
     points: Array<Point>;
     title: string | null;
     rawOptions?: PolylineOptions;
     extra: Record<string, unknown>;
-};
+}>;
 export type InfoWindowDefinition<InfoWindowOptions> = {
     headerContent: string | null;
     content: string | null;
@@ -36,6 +37,7 @@ export type InfoWindowDefinition<InfoWindowOptions> = {
     rawOptions?: InfoWindowOptions;
     extra: Record<string, unknown>;
 };
+export type InfoWindowWithoutPositionDefinition<InfoWindowOptions> = Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
 export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow, PolygonOptions, Polygon, PolylineOptions, Polyline> extends Controller<HTMLElement> {
     static values: {
         providerOptions: ObjectConstructor;
@@ -55,50 +57,47 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
     polylinesValue: Array<PolylineDefinition<PolylineOptions, InfoWindowOptions>>;
     optionsValue: MapOptions;
     protected map: Map;
-    protected markers: globalThis.Map<any, any>;
+    protected markers: globalThis.Map<string, Marker>;
+    protected polygons: globalThis.Map<string, Polygon>;
+    protected polylines: globalThis.Map<string, Polyline>;
     protected infoWindows: Array<InfoWindow>;
-    protected polygons: globalThis.Map<any, any>;
-    protected polylines: globalThis.Map<any, any>;
+    private isConnected;
+    private createMarker;
+    private createPolygon;
+    private createPolyline;
+    protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
     connect(): void;
+    createInfoWindow({ definition, element, }: {
+        definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
+        element: Marker | Polygon | Polyline;
+    }): InfoWindow;
+    abstract centerValueChanged(): void;
+    abstract zoomValueChanged(): void;
+    markersValueChanged(): void;
+    polygonsValueChanged(): void;
+    polylinesValueChanged(): void;
     protected abstract doCreateMap({ center, zoom, options, }: {
         center: Point | null;
         zoom: number | null;
         options: MapOptions;
     }): Map;
-    createMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
-    protected abstract removeMarker(marker: Marker): void;
-    protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
-    createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
-    protected abstract removePolygon(polygon: Polygon): void;
-    protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
-    createPolyline(definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>): Polyline;
-    protected abstract removePolyline(polyline: Polyline): void;
-    protected abstract doCreatePolyline(definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>): Polyline;
-    protected createInfoWindow({ definition, element, }: {
-        definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
-        element: Marker;
-    } | {
-        definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
-        element: Polygon;
-    } | {
-        definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>['infoWindow'];
-        element: Polyline;
-    }): InfoWindow;
+    protected abstract doFitBoundsToMarkers(): void;
+    protected abstract doCreateMarker({ definition, }: {
+        definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>;
+    }): Marker;
+    protected abstract doRemoveMarker(marker: Marker): void;
+    protected abstract doCreatePolygon({ definition, }: {
+        definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>;
+    }): Polygon;
+    protected abstract doRemovePolygon(polygon: Polygon): void;
+    protected abstract doCreatePolyline({ definition, }: {
+        definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>;
+    }): Polyline;
+    protected abstract doRemovePolyline(polyline: Polyline): void;
     protected abstract doCreateInfoWindow({ definition, element, }: {
-        definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
-        element: Marker;
-    } | {
-        definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
-        element: Polygon;
-    } | {
-        definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>['infoWindow'];
-        element: Polyline;
+        definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
+        element: Marker | Polygon | Polyline;
     }): InfoWindow;
-    protected abstract doFitBoundsToMarkers(): void;
-    protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
-    abstract centerValueChanged(): void;
-    abstract zoomValueChanged(): void;
-    markersValueChanged(): void;
-    polygonsValueChanged(): void;
-    polylinesValueChanged(): void;
+    private createDrawingFactory;
+    private onDrawChanged;
 }
diff --git a/src/Map/assets/dist/abstract_map_controller.js b/src/Map/assets/dist/abstract_map_controller.js
index 9381817e..60f5cb45 100644
--- a/src/Map/assets/dist/abstract_map_controller.js
+++ b/src/Map/assets/dist/abstract_map_controller.js
@@ -4,17 +4,21 @@ class default_1 extends Controller {
     constructor() {
         super(...arguments);
         this.markers = new Map();
-        this.infoWindows = [];
         this.polygons = new Map();
         this.polylines = new Map();
+        this.infoWindows = [];
+        this.isConnected = false;
     }
     connect() {
         const options = this.optionsValue;
         this.dispatchEvent('pre-connect', { options });
+        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.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options });
-        this.markersValue.forEach((marker) => this.createMarker(marker));
-        this.polygonsValue.forEach((polygon) => this.createPolygon(polygon));
-        this.polylinesValue.forEach((polyline) => this.createPolyline(polyline));
+        this.markersValue.forEach((definition) => this.createMarker({ definition }));
+        this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
+        this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
         if (this.fitBoundsToMarkersValue) {
             this.doFitBoundsToMarkers();
         }
@@ -25,30 +29,7 @@ class default_1 extends Controller {
             polylines: [...this.polylines.values()],
             infoWindows: this.infoWindows,
         });
-    }
-    createMarker(definition) {
-        this.dispatchEvent('marker:before-create', { definition });
-        const marker = this.doCreateMarker(definition);
-        this.dispatchEvent('marker:after-create', { marker });
-        marker['@id'] = definition['@id'];
-        this.markers.set(definition['@id'], marker);
-        return marker;
-    }
-    createPolygon(definition) {
-        this.dispatchEvent('polygon:before-create', { definition });
-        const polygon = this.doCreatePolygon(definition);
-        this.dispatchEvent('polygon:after-create', { polygon });
-        polygon['@id'] = definition['@id'];
-        this.polygons.set(definition['@id'], polygon);
-        return polygon;
-    }
-    createPolyline(definition) {
-        this.dispatchEvent('polyline:before-create', { definition });
-        const polyline = this.doCreatePolyline(definition);
-        this.dispatchEvent('polyline:after-create', { polyline });
-        polyline['@id'] = definition['@id'];
-        this.polylines.set(definition['@id'], polyline);
-        return polyline;
+        this.isConnected = true;
     }
     createInfoWindow({ definition, element, }) {
         this.dispatchEvent('info-window:before-create', { definition, element });
@@ -58,53 +39,50 @@ class default_1 extends Controller {
         return infoWindow;
     }
     markersValueChanged() {
-        if (!this.map) {
+        if (!this.isConnected) {
             return;
         }
-        this.markers.forEach((marker) => {
-            if (!this.markersValue.find((m) => m['@id'] === marker['@id'])) {
-                this.removeMarker(marker);
-                this.markers.delete(marker['@id']);
-            }
-        });
-        this.markersValue.forEach((marker) => {
-            if (!this.markers.has(marker['@id'])) {
-                this.createMarker(marker);
-            }
-        });
+        this.onDrawChanged(this.markers, this.markersValue, this.createMarker, this.doRemoveMarker);
         if (this.fitBoundsToMarkersValue) {
             this.doFitBoundsToMarkers();
         }
     }
     polygonsValueChanged() {
-        if (!this.map) {
+        if (!this.isConnected) {
             return;
         }
-        this.polygons.forEach((polygon) => {
-            if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) {
-                this.removePolygon(polygon);
-                this.polygons.delete(polygon['@id']);
-            }
-        });
-        this.polygonsValue.forEach((polygon) => {
-            if (!this.polygons.has(polygon['@id'])) {
-                this.createPolygon(polygon);
-            }
-        });
+        this.onDrawChanged(this.polygons, this.polygonsValue, this.createPolygon, this.doRemovePolygon);
     }
     polylinesValueChanged() {
-        if (!this.map) {
+        if (!this.isConnected) {
             return;
         }
-        this.polylines.forEach((polyline) => {
-            if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) {
-                this.removePolyline(polyline);
-                this.polylines.delete(polyline['@id']);
-            }
+        this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline);
+    }
+    createDrawingFactory(type, draws, factory) {
+        const eventBefore = `${type}:before-create`;
+        const eventAfter = `${type}:after-create`;
+        return ({ definition }) => {
+            this.dispatchEvent(eventBefore, { definition });
+            const drawing = factory({ definition });
+            this.dispatchEvent(eventAfter, { [type]: drawing });
+            draws.set(definition['@id'], drawing);
+            return drawing;
+        };
+    }
+    onDrawChanged(draws, newDrawDefinitions, factory, remover) {
+        const idsToRemove = new Set(draws.keys());
+        newDrawDefinitions.forEach((definition) => {
+            idsToRemove.delete(definition['@id']);
+        });
+        idsToRemove.forEach((id) => {
+            const draw = draws.get(id);
+            remover(draw);
+            draws.delete(id);
         });
-        this.polylinesValue.forEach((polyline) => {
-            if (!this.polylines.has(polyline['@id'])) {
-                this.createPolyline(polyline);
+        newDrawDefinitions.forEach((definition) => {
+            if (!draws.has(definition['@id'])) {
+                factory({ definition });
             }
         });
     }
diff --git a/src/Map/assets/src/abstract_map_controller.ts b/src/Map/assets/src/abstract_map_controller.ts
index a1f9de68..e9e7797a 100644
--- a/src/Map/assets/src/abstract_map_controller.ts
+++ b/src/Map/assets/src/abstract_map_controller.ts
@@ -2,11 +2,13 @@ import { Controller } from '@hotwired/stimulus';
 
 export type Point = { lat: number; lng: number };
 
-export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
-    '@id': string;
+export type Identifier = string;
+export type WithIdentifier<T extends Record<string, unknown>> = T & { '@id': Identifier };
+
+export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = WithIdentifier<{
     position: Point;
     title: string | null;
-    infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
+    infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
     /**
      * Raw options passed to the marker constructor, specific to the map provider (e.g.: `L.marker()` for Leaflet).
      */
@@ -18,25 +20,41 @@ export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
      *    - `ux:map:marker:after-create`
      */
     extra: Record<string, unknown>;
-};
+}>;
 
-export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
-    '@id': string;
-    infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
+export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = WithIdentifier<{
+    infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
     points: Array<Point>;
     title: string | null;
+    /**
+     * Raw options passed to the marker constructor, specific to the map provider (e.g.: `L.marker()` for Leaflet).
+     */
     rawOptions?: PolygonOptions;
+    /**
+     * 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:polygon:before-create`
+     *    - `ux:map:polygon:after-create`
+     */
     extra: Record<string, unknown>;
-};
+}>;
 
-export type PolylineDefinition<PolylineOptions, InfoWindowOptions> = {
-    '@id': string;
-    infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
+export type PolylineDefinition<PolylineOptions, InfoWindowOptions> = WithIdentifier<{
+    infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
     points: Array<Point>;
     title: string | null;
+    /**
+     * Raw options passed to the marker constructor, specific to the map provider (e.g.: `L.marker()` for Leaflet).
+     */
     rawOptions?: PolylineOptions;
+    /**
+     * 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:polyline:before-create`
+     *    - `ux:map:polyline:after-create`
+     */
     extra: Record<string, unknown>;
-};
+}>;
 
 export type InfoWindowDefinition<InfoWindowOptions> = {
     headerContent: string | null;
@@ -58,6 +76,11 @@ export type InfoWindowDefinition<InfoWindowOptions> = {
     extra: Record<string, unknown>;
 };
 
+export type InfoWindowWithoutPositionDefinition<InfoWindowOptions> = Omit<
+    InfoWindowDefinition<InfoWindowOptions>,
+    'position'
+>;
+
 export default abstract class<
     MapOptions,
     Map,
@@ -90,23 +113,37 @@ export default abstract class<
     declare optionsValue: MapOptions;
 
     protected map: Map;
-    protected markers = new Map<Marker>();
+    protected markers = new Map<Identifier, Marker>();
+    protected polygons = new Map<Identifier, Polygon>();
+    protected polylines = new Map<Identifier, Polyline>();
     protected infoWindows: Array<InfoWindow> = [];
-    protected polygons = new Map<Polygon>();
-    protected polylines = new Map<Polyline>();
+
+    private isConnected = false;
+    private createMarker: ({
+        definition,
+    }: { definition: MarkerDefinition<MarkerOptions, InfoWindowOptions> }) => Marker;
+    private createPolygon: ({
+        definition,
+    }: { definition: PolygonDefinition<PolygonOptions, InfoWindowOptions> }) => Polygon;
+    private createPolyline: ({
+        definition,
+    }: { definition: PolylineDefinition<PolylineOptions, InfoWindowOptions> }) => Polyline;
+
+    protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
 
     connect() {
         const options = this.optionsValue;
 
         this.dispatchEvent('pre-connect', { options });
 
-        this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options });
-
-        this.markersValue.forEach((marker) => this.createMarker(marker));
-
-        this.polygonsValue.forEach((polygon) => this.createPolygon(polygon));
+        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.polylinesValue.forEach((polyline) => this.createPolyline(polyline));
+        this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options });
+        this.markersValue.forEach((definition) => this.createMarker({ definition }));
+        this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
+        this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
 
         if (this.fitBoundsToMarkersValue) {
             this.doFitBoundsToMarkers();
@@ -119,76 +156,18 @@ export default abstract class<
             polylines: [...this.polylines.values()],
             infoWindows: this.infoWindows,
         });
-    }
-
-    protected abstract doCreateMap({
-        center,
-        zoom,
-        options,
-    }: {
-        center: Point | null;
-        zoom: number | null;
-        options: MapOptions;
-    }): Map;
-
-    public createMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker {
-        this.dispatchEvent('marker:before-create', { definition });
-        const marker = this.doCreateMarker(definition);
-        this.dispatchEvent('marker:after-create', { marker });
-
-        marker['@id'] = definition['@id'];
-
-        this.markers.set(definition['@id'], marker);
-
-        return marker;
-    }
-
-    protected abstract removeMarker(marker: Marker): void;
-
-    protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
-
-    public createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon {
-        this.dispatchEvent('polygon:before-create', { definition });
-        const polygon = this.doCreatePolygon(definition);
-        this.dispatchEvent('polygon:after-create', { polygon });
-
-        polygon['@id'] = definition['@id'];
-
-        this.polygons.set(definition['@id'], polygon);
-
-        return polygon;
-    }
-
-    protected abstract removePolygon(polygon: Polygon): void;
-
-    protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
-
-    public createPolyline(definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>): Polyline {
-        this.dispatchEvent('polyline:before-create', { definition });
-        const polyline = this.doCreatePolyline(definition);
-        this.dispatchEvent('polyline:after-create', { polyline });
-
-        polyline['@id'] = definition['@id'];
 
-        this.polylines.set(definition['@id'], polyline);
-
-        return polyline;
+        this.isConnected = true;
     }
 
-    protected abstract removePolyline(polyline: Polyline): void;
-
-    protected abstract doCreatePolyline(definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>): Polyline;
-
-    protected createInfoWindow({
+    //region Public API
+    public createInfoWindow({
         definition,
         element,
-    }:
-        | { definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow']; element: Marker }
-        | { definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow']; element: Polygon }
-        | {
-              definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>['infoWindow'];
-              element: Polyline;
-          }): InfoWindow {
+    }: {
+        definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
+        element: Marker | Polygon | Polyline;
+    }): InfoWindow {
         this.dispatchEvent('info-window:before-create', { definition, element });
         const infoWindow = this.doCreateInfoWindow({ definition, element });
         this.dispatchEvent('info-window:after-create', { infoWindow, element });
@@ -198,42 +177,19 @@ export default abstract class<
         return infoWindow;
     }
 
-    protected abstract doCreateInfoWindow({
-        definition,
-        element,
-    }:
-        | { definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow']; element: Marker }
-        | { definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow']; element: Polygon }
-        | {
-              definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>['infoWindow'];
-              element: Polyline;
-          }): InfoWindow;
-
-    protected abstract doFitBoundsToMarkers(): void;
-
-    protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
+    //endregion
 
+    //region Hooks called by Stimulus when the values change
     public abstract centerValueChanged(): void;
 
     public abstract zoomValueChanged(): void;
 
     public markersValueChanged(): void {
-        if (!this.map) {
+        if (!this.isConnected) {
             return;
         }
 
-        this.markers.forEach((marker) => {
-            if (!this.markersValue.find((m) => m['@id'] === marker['@id'])) {
-                this.removeMarker(marker);
-                this.markers.delete(marker['@id']);
-            }
-        });
-
-        this.markersValue.forEach((marker) => {
-            if (!this.markers.has(marker['@id'])) {
-                this.createMarker(marker);
-            }
-        });
+        this.onDrawChanged(this.markers, this.markersValue, this.createMarker, this.doRemoveMarker);
 
         if (this.fitBoundsToMarkersValue) {
             this.doFitBoundsToMarkers();
@@ -241,40 +197,149 @@ export default abstract class<
     }
 
     public polygonsValueChanged(): void {
-        if (!this.map) {
+        if (!this.isConnected) {
             return;
         }
 
-        this.polygons.forEach((polygon) => {
-            if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) {
-                this.removePolygon(polygon);
-                this.polygons.delete(polygon['@id']);
-            }
-        });
-
-        this.polygonsValue.forEach((polygon) => {
-            if (!this.polygons.has(polygon['@id'])) {
-                this.createPolygon(polygon);
-            }
-        });
+        this.onDrawChanged(this.polygons, this.polygonsValue, this.createPolygon, this.doRemovePolygon);
     }
 
     public polylinesValueChanged(): void {
-        if (!this.map) {
+        if (!this.isConnected) {
             return;
         }
 
-        this.polylines.forEach((polyline) => {
-            if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) {
-                this.removePolyline(polyline);
-                this.polylines.delete(polyline['@id']);
-            }
+        this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline);
+    }
+
+    //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;
+    }): Map;
+
+    protected abstract doFitBoundsToMarkers(): void;
+
+    protected abstract doCreateMarker({
+        definition,
+    }: { definition: MarkerDefinition<MarkerOptions, InfoWindowOptions> }): Marker;
+
+    protected abstract doRemoveMarker(marker: Marker): void;
+
+    protected abstract doCreatePolygon({
+        definition,
+    }: {
+        definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>;
+    }): Polygon;
+
+    protected abstract doRemovePolygon(polygon: Polygon): void;
+
+    protected abstract doCreatePolyline({
+        definition,
+    }: {
+        definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>;
+    }): Polyline;
+
+    protected abstract doRemovePolyline(polyline: Polyline): void;
+
+    protected abstract doCreateInfoWindow({
+        definition,
+        element,
+    }: {
+        definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
+        element: Marker | Polygon | Polyline;
+    }): InfoWindow;
+
+    //endregion
+
+    //region Private APIs
+    private createDrawingFactory(
+        type: 'marker',
+        draws: typeof this.markers,
+        factory: typeof this.doCreateMarker
+    ): typeof this.doCreateMarker;
+    private createDrawingFactory(
+        type: 'polygon',
+        draws: typeof this.polygons,
+        factory: typeof this.doCreatePolygon
+    ): typeof this.doCreatePolygon;
+    private createDrawingFactory(
+        type: 'polyline',
+        draws: typeof this.polylines,
+        factory: typeof this.doCreatePolyline
+    ): typeof this.doCreatePolyline;
+    private createDrawingFactory<
+        Factory extends typeof this.doCreateMarker | typeof this.doCreatePolygon | typeof this.doCreatePolyline,
+        Draw extends ReturnType<Factory>,
+    >(
+        type: 'marker' | 'polygon' | 'polyline',
+        draws: globalThis.Map<WithIdentifier<any>, Draw>,
+        factory: Factory
+    ): Factory {
+        const eventBefore = `${type}:before-create`;
+        const eventAfter = `${type}:after-create`;
+
+        // @ts-expect-error IDK what to do with this error
+        // 'Factory' could be instantiated with an arbitrary type which could be unrelated to '({ definition }: { definition: WithIdentifier<any>; }) => Draw'
+        return ({ definition }: { definition: WithIdentifier<any> }) => {
+            this.dispatchEvent(eventBefore, { definition });
+            const drawing = factory({ definition }) as Draw;
+            this.dispatchEvent(eventAfter, { [type]: drawing });
+
+            draws.set(definition['@id'], drawing);
+
+            return drawing;
+        };
+    }
+
+    private onDrawChanged(
+        draws: typeof this.markers,
+        newDrawDefinitions: typeof this.markersValue,
+        factory: typeof this.createMarker,
+        remover: typeof this.doRemoveMarker
+    ): void;
+    private onDrawChanged(
+        draws: typeof this.polygons,
+        newDrawDefinitions: typeof this.polygonsValue,
+        factory: typeof this.createPolygon,
+        remover: typeof this.doRemovePolygon
+    ): void;
+    private onDrawChanged(
+        draws: typeof this.polylines,
+        newDrawDefinitions: typeof this.polylinesValue,
+        factory: typeof this.createPolyline,
+        remover: typeof this.doRemovePolyline
+    ): void;
+    private onDrawChanged<Draw, DrawDefinition extends WithIdentifier<Record<string, unknown>>>(
+        draws: globalThis.Map<WithIdentifier<any>, Draw>,
+        newDrawDefinitions: Array<DrawDefinition>,
+        factory: (args: { definition: DrawDefinition }) => Draw,
+        remover: (args: Draw) => void
+    ): void {
+        const idsToRemove = new Set(draws.keys());
+        newDrawDefinitions.forEach((definition) => {
+            idsToRemove.delete(definition['@id']);
+        });
+
+        idsToRemove.forEach((id) => {
+            // biome-ignore lint/style/noNonNullAssertion: the ids are coming from the keys of the map
+            const draw = draws.get(id)!;
+            remover(draw);
+            draws.delete(id);
         });
 
-        this.polylinesValue.forEach((polyline) => {
-            if (!this.polylines.has(polyline['@id'])) {
-                this.createPolyline(polyline);
+        newDrawDefinitions.forEach((definition) => {
+            if (!draws.has(definition['@id'])) {
+                factory({ definition });
             }
         });
     }
+    //endregion
 }
diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
index 2ce6fcc0..9f99a6c3 100644
--- a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
+++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
@@ -1,36 +1,37 @@
 import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
+import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition, InfoWindowWithoutPositionDefinition } from '@symfony/ux-map';
 import type { LoaderOptions } from '@googlemaps/js-api-loader';
 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> {
     providerOptionsValue: Pick<LoaderOptions, 'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version' | 'libraries'>;
+    map: google.maps.Map;
     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;
     }): google.maps.Map;
-    protected doCreateMarker(definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>): google.maps.marker.AdvancedMarkerElement;
-    protected removeMarker(marker: google.maps.marker.AdvancedMarkerElement): void;
-    protected doCreatePolygon(definition: PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>): google.maps.Polygon;
-    protected removePolygon(polygon: google.maps.Polygon): void;
-    protected doCreatePolyline(definition: PolylineDefinition<google.maps.Polyline, google.maps.InfoWindowOptions>): google.maps.Polyline;
-    protected removePolyline(polyline: google.maps.Polyline): void;
+    protected doCreateMarker({ definition, }: {
+        definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>;
+    }): google.maps.marker.AdvancedMarkerElement;
+    protected doRemoveMarker(marker: google.maps.marker.AdvancedMarkerElement): void;
+    protected doCreatePolygon({ definition, }: {
+        definition: PolygonDefinition<google.maps.PolygonOptions, google.maps.InfoWindowOptions>;
+    }): google.maps.Polygon;
+    protected doRemovePolygon(polygon: google.maps.Polygon): void;
+    protected doCreatePolyline({ definition, }: {
+        definition: PolylineDefinition<google.maps.PolylineOptions, google.maps.InfoWindowOptions>;
+    }): google.maps.Polyline;
+    protected doRemovePolyline(polyline: google.maps.Polyline): void;
     protected doCreateInfoWindow({ definition, element, }: {
-        definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>['infoWindow'];
-        element: google.maps.marker.AdvancedMarkerElement;
-    } | {
-        definition: PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>['infoWindow'];
-        element: google.maps.Polygon;
-    } | {
-        definition: PolylineDefinition<google.maps.Polyline, google.maps.InfoWindowOptions>['infoWindow'];
-        element: google.maps.Polyline;
+        definition: InfoWindowWithoutPositionDefinition<google.maps.InfoWindowOptions>;
+        element: google.maps.marker.AdvancedMarkerElement | google.maps.Polygon | google.maps.Polyline;
     }): google.maps.InfoWindow;
+    protected doFitBoundsToMarkers(): void;
     private createTextOrElement;
     private closeInfoWindowsExcept;
-    protected doFitBoundsToMarkers(): void;
-    centerValueChanged(): void;
-    zoomValueChanged(): void;
 }
 export {};
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 4307c67a..00317a50 100644
--- a/src/Map/src/Bridge/Google/assets/dist/map_controller.js
+++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.js
@@ -5,17 +5,21 @@ class default_1 extends Controller {
     constructor() {
         super(...arguments);
         this.markers = new Map();
-        this.infoWindows = [];
         this.polygons = new Map();
         this.polylines = new Map();
+        this.infoWindows = [];
+        this.isConnected = false;
     }
     connect() {
         const options = this.optionsValue;
         this.dispatchEvent('pre-connect', { options });
+        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.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options });
-        this.markersValue.forEach((marker) => this.createMarker(marker));
-        this.polygonsValue.forEach((polygon) => this.createPolygon(polygon));
-        this.polylinesValue.forEach((polyline) => this.createPolyline(polyline));
+        this.markersValue.forEach((definition) => this.createMarker({ definition }));
+        this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
+        this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
         if (this.fitBoundsToMarkersValue) {
             this.doFitBoundsToMarkers();
         }
@@ -26,30 +30,7 @@ class default_1 extends Controller {
             polylines: [...this.polylines.values()],
             infoWindows: this.infoWindows,
         });
-    }
-    createMarker(definition) {
-        this.dispatchEvent('marker:before-create', { definition });
-        const marker = this.doCreateMarker(definition);
-        this.dispatchEvent('marker:after-create', { marker });
-        marker['@id'] = definition['@id'];
-        this.markers.set(definition['@id'], marker);
-        return marker;
-    }
-    createPolygon(definition) {
-        this.dispatchEvent('polygon:before-create', { definition });
-        const polygon = this.doCreatePolygon(definition);
-        this.dispatchEvent('polygon:after-create', { polygon });
-        polygon['@id'] = definition['@id'];
-        this.polygons.set(definition['@id'], polygon);
-        return polygon;
-    }
-    createPolyline(definition) {
-        this.dispatchEvent('polyline:before-create', { definition });
-        const polyline = this.doCreatePolyline(definition);
-        this.dispatchEvent('polyline:after-create', { polyline });
-        polyline['@id'] = definition['@id'];
-        this.polylines.set(definition['@id'], polyline);
-        return polyline;
+        this.isConnected = true;
     }
     createInfoWindow({ definition, element, }) {
         this.dispatchEvent('info-window:before-create', { definition, element });
@@ -59,53 +40,50 @@ class default_1 extends Controller {
         return infoWindow;
     }
     markersValueChanged() {
-        if (!this.map) {
+        if (!this.isConnected) {
             return;
         }
-        this.markers.forEach((marker) => {
-            if (!this.markersValue.find((m) => m['@id'] === marker['@id'])) {
-                this.removeMarker(marker);
-                this.markers.delete(marker['@id']);
-            }
-        });
-        this.markersValue.forEach((marker) => {
-            if (!this.markers.has(marker['@id'])) {
-                this.createMarker(marker);
-            }
-        });
+        this.onDrawChanged(this.markers, this.markersValue, this.createMarker, this.doRemoveMarker);
         if (this.fitBoundsToMarkersValue) {
             this.doFitBoundsToMarkers();
         }
     }
     polygonsValueChanged() {
-        if (!this.map) {
+        if (!this.isConnected) {
             return;
         }
-        this.polygons.forEach((polygon) => {
-            if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) {
-                this.removePolygon(polygon);
-                this.polygons.delete(polygon['@id']);
-            }
-        });
-        this.polygonsValue.forEach((polygon) => {
-            if (!this.polygons.has(polygon['@id'])) {
-                this.createPolygon(polygon);
-            }
-        });
+        this.onDrawChanged(this.polygons, this.polygonsValue, this.createPolygon, this.doRemovePolygon);
     }
     polylinesValueChanged() {
-        if (!this.map) {
+        if (!this.isConnected) {
             return;
         }
-        this.polylines.forEach((polyline) => {
-            if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) {
-                this.removePolyline(polyline);
-                this.polylines.delete(polyline['@id']);
-            }
+        this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline);
+    }
+    createDrawingFactory(type, draws, factory) {
+        const eventBefore = `${type}:before-create`;
+        const eventAfter = `${type}:after-create`;
+        return ({ definition }) => {
+            this.dispatchEvent(eventBefore, { definition });
+            const drawing = factory({ definition });
+            this.dispatchEvent(eventAfter, { [type]: drawing });
+            draws.set(definition['@id'], drawing);
+            return drawing;
+        };
+    }
+    onDrawChanged(draws, newDrawDefinitions, factory, remover) {
+        const idsToRemove = new Set(draws.keys());
+        newDrawDefinitions.forEach((definition) => {
+            idsToRemove.delete(definition['@id']);
+        });
+        idsToRemove.forEach((id) => {
+            const draw = draws.get(id);
+            remover(draw);
+            draws.delete(id);
         });
-        this.polylinesValue.forEach((polyline) => {
-            if (!this.polylines.has(polyline['@id'])) {
-                this.createPolyline(polyline);
+        newDrawDefinitions.forEach((definition) => {
+            if (!draws.has(definition['@id'])) {
+                factory({ definition });
             }
         });
     }
@@ -142,6 +120,16 @@ class map_controller extends default_1 {
         }
         super.connect();
     }
+    centerValueChanged() {
+        if (this.map && this.centerValue) {
+            this.map.setCenter(this.centerValue);
+        }
+    }
+    zoomValueChanged() {
+        if (this.map && this.zoomValue) {
+            this.map.setZoom(this.zoomValue);
+        }
+    }
     dispatchEvent(name, payload = {}) {
         this.dispatch(name, {
             prefix: 'ux:map',
@@ -162,7 +150,7 @@ class map_controller extends default_1 {
             zoom,
         });
     }
-    doCreateMarker(definition) {
+    doCreateMarker({ definition, }) {
         const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
         const marker = new _google.maps.marker.AdvancedMarkerElement({
             position,
@@ -176,10 +164,10 @@ class map_controller extends default_1 {
         }
         return marker;
     }
-    removeMarker(marker) {
+    doRemoveMarker(marker) {
         marker.map = null;
     }
-    doCreatePolygon(definition) {
+    doCreatePolygon({ definition, }) {
         const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
         const polygon = new _google.maps.Polygon({
             ...rawOptions,
@@ -194,10 +182,10 @@ class map_controller extends default_1 {
         }
         return polygon;
     }
-    removePolygon(polygon) {
+    doRemovePolygon(polygon) {
         polygon.setMap(null);
     }
-    doCreatePolyline(definition) {
+    doCreatePolyline({ definition, }) {
         const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
         const polyline = new _google.maps.Polyline({
             ...rawOptions,
@@ -212,7 +200,7 @@ class map_controller extends default_1 {
         }
         return polyline;
     }
-    removePolyline(polyline) {
+    doRemovePolyline(polyline) {
         polyline.setMap(null);
     }
     doCreateInfoWindow({ definition, element, }) {
@@ -253,6 +241,19 @@ class map_controller extends default_1 {
         }
         return infoWindow;
     }
+    doFitBoundsToMarkers() {
+        if (this.markers.size === 0) {
+            return;
+        }
+        const bounds = new google.maps.LatLngBounds();
+        this.markers.forEach((marker) => {
+            if (!marker.position) {
+                return;
+            }
+            bounds.extend(marker.position);
+        });
+        this.map.fitBounds(bounds);
+    }
     createTextOrElement(content) {
         if (!content) {
             return null;
@@ -271,29 +272,6 @@ class map_controller extends default_1 {
             }
         });
     }
-    doFitBoundsToMarkers() {
-        if (this.markers.length === 0) {
-            return;
-        }
-        const bounds = new google.maps.LatLngBounds();
-        this.markers.forEach((marker) => {
-            if (!marker.position) {
-                return;
-            }
-            bounds.extend(marker.position);
-        });
-        this.map.fitBounds(bounds);
-    }
-    centerValueChanged() {
-        if (this.map && this.centerValue) {
-            this.map.setCenter(this.centerValue);
-        }
-    }
-    zoomValueChanged() {
-        if (this.map && this.zoomValue) {
-            this.map.setZoom(this.zoomValue);
-        }
-    }
 }
 
 export { map_controller as default };
diff --git a/src/Map/src/Bridge/Google/assets/src/map_controller.ts b/src/Map/src/Bridge/Google/assets/src/map_controller.ts
index df5e843c..6f5b9939 100644
--- a/src/Map/src/Bridge/Google/assets/src/map_controller.ts
+++ b/src/Map/src/Bridge/Google/assets/src/map_controller.ts
@@ -8,7 +8,13 @@
  */
 
 import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
+import type {
+    Point,
+    MarkerDefinition,
+    PolygonDefinition,
+    PolylineDefinition,
+    InfoWindowWithoutPositionDefinition,
+} from '@symfony/ux-map';
 import type { LoaderOptions } from '@googlemaps/js-api-loader';
 import { Loader } from '@googlemaps/js-api-loader';
 
@@ -47,9 +53,11 @@ export default class extends AbstractMapController<
         'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version' | 'libraries'
     >;
 
+    declare map: google.maps.Map;
+
     async connect() {
         if (!_google) {
-            _google = { maps: {} };
+            _google = { maps: {} as typeof google.maps };
 
             let { libraries = [], ...loaderOptions } = this.providerOptionsValue;
 
@@ -77,6 +85,18 @@ export default class extends AbstractMapController<
         super.connect();
     }
 
+    public centerValueChanged(): void {
+        if (this.map && this.centerValue) {
+            this.map.setCenter(this.centerValue);
+        }
+    }
+
+    public zoomValueChanged(): void {
+        if (this.map && this.zoomValue) {
+            this.map.setZoom(this.zoomValue);
+        }
+    }
+
     protected dispatchEvent(name: string, payload: Record<string, unknown> = {}): void {
         this.dispatch(name, {
             prefix: 'ux:map',
@@ -109,9 +129,11 @@ export default class extends AbstractMapController<
         });
     }
 
-    protected doCreateMarker(
-        definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>
-    ): google.maps.marker.AdvancedMarkerElement {
+    protected doCreateMarker({
+        definition,
+    }: {
+        definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>;
+    }): google.maps.marker.AdvancedMarkerElement {
         const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
 
         const marker = new _google.maps.marker.AdvancedMarkerElement({
@@ -129,13 +151,15 @@ export default class extends AbstractMapController<
         return marker;
     }
 
-    protected removeMarker(marker: google.maps.marker.AdvancedMarkerElement): void {
+    protected doRemoveMarker(marker: google.maps.marker.AdvancedMarkerElement): void {
         marker.map = null;
     }
 
-    protected doCreatePolygon(
-        definition: PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>
-    ): google.maps.Polygon {
+    protected doCreatePolygon({
+        definition,
+    }: {
+        definition: PolygonDefinition<google.maps.PolygonOptions, google.maps.InfoWindowOptions>;
+    }): google.maps.Polygon {
         const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
 
         const polygon = new _google.maps.Polygon({
@@ -155,13 +179,15 @@ export default class extends AbstractMapController<
         return polygon;
     }
 
-    protected removePolygon(polygon: google.maps.Polygon) {
+    protected doRemovePolygon(polygon: google.maps.Polygon) {
         polygon.setMap(null);
     }
 
-    protected doCreatePolyline(
-        definition: PolylineDefinition<google.maps.Polyline, google.maps.InfoWindowOptions>
-    ): google.maps.Polyline {
+    protected doCreatePolyline({
+        definition,
+    }: {
+        definition: PolylineDefinition<google.maps.PolylineOptions, google.maps.InfoWindowOptions>;
+    }): google.maps.Polyline {
         const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
 
         const polyline = new _google.maps.Polyline({
@@ -181,29 +207,17 @@ export default class extends AbstractMapController<
         return polyline;
     }
 
-    protected removePolyline(polyline: google.maps.Polyline): void {
+    protected doRemovePolyline(polyline: google.maps.Polyline): void {
         polyline.setMap(null);
     }
 
     protected doCreateInfoWindow({
         definition,
         element,
-    }:
-        | {
-              definition: MarkerDefinition<
-                  google.maps.marker.AdvancedMarkerElementOptions,
-                  google.maps.InfoWindowOptions
-              >['infoWindow'];
-              element: google.maps.marker.AdvancedMarkerElement;
-          }
-        | {
-              definition: PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>['infoWindow'];
-              element: google.maps.Polygon;
-          }
-        | {
-              definition: PolylineDefinition<google.maps.Polyline, google.maps.InfoWindowOptions>['infoWindow'];
-              element: google.maps.Polyline;
-          }): google.maps.InfoWindow {
+    }: {
+        definition: InfoWindowWithoutPositionDefinition<google.maps.InfoWindowOptions>;
+        element: google.maps.marker.AdvancedMarkerElement | google.maps.Polygon | google.maps.Polyline;
+    }): google.maps.InfoWindow {
         const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;
 
         const infoWindow = new _google.maps.InfoWindow({
@@ -246,6 +260,23 @@ export default class extends AbstractMapController<
         return infoWindow;
     }
 
+    protected doFitBoundsToMarkers(): void {
+        if (this.markers.size === 0) {
+            return;
+        }
+
+        const bounds = new google.maps.LatLngBounds();
+        this.markers.forEach((marker) => {
+            if (!marker.position) {
+                return;
+            }
+
+            bounds.extend(marker.position);
+        });
+
+        this.map.fitBounds(bounds);
+    }
+
     private createTextOrElement(content: string | null): string | HTMLElement | null {
         if (!content) {
             return null;
@@ -268,33 +299,4 @@ export default class extends AbstractMapController<
             }
         });
     }
-
-    protected doFitBoundsToMarkers(): void {
-        if (this.markers.length === 0) {
-            return;
-        }
-
-        const bounds = new google.maps.LatLngBounds();
-        this.markers.forEach((marker) => {
-            if (!marker.position) {
-                return;
-            }
-
-            bounds.extend(marker.position);
-        });
-
-        this.map.fitBounds(bounds);
-    }
-
-    public centerValueChanged(): void {
-        if (this.map && this.centerValue) {
-            this.map.setCenter(this.centerValue);
-        }
-    }
-
-    public zoomValueChanged(): void {
-        if (this.map && this.zoomValue) {
-            this.map.setZoom(this.zoomValue);
-        }
-    }
 }
diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
index ccca2608..fbe96749 100644
--- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
@@ -1,5 +1,5 @@
 import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
+import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition, InfoWindowWithoutPositionDefinition } from '@symfony/ux-map';
 import 'leaflet/dist/leaflet.min.css';
 import * as L from 'leaflet';
 import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions, PolylineOptions as PolygonOptions, PolylineOptions } from 'leaflet';
@@ -10,32 +10,33 @@ type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
         options: Record<string, unknown>;
     };
 };
-export default class extends AbstractMapController<MapOptions, typeof L.Map, MarkerOptions, typeof L.Marker, PopupOptions, typeof L.Popup, PolygonOptions, typeof L.Polygon, PolylineOptions, typeof L.Polyline> {
+export default class extends AbstractMapController<MapOptions, L.Map, MarkerOptions, L.Marker, PopupOptions, L.Popup, PolygonOptions, L.Polygon, PolylineOptions, L.Polyline> {
+    map: L.Map;
     connect(): 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;
     }): L.Map;
-    protected doCreateMarker(definition: MarkerDefinition<typeof L.Marker, typeof L.Popup>): L.Marker;
-    protected removeMarker(marker: L.Marker): void;
-    protected doCreatePolygon(definition: PolygonDefinition<typeof L.Polygon, typeof L.Popup>): L.Polygon;
-    protected removePolygon(polygon: L.Polygon): void;
-    protected doCreatePolyline(definition: PolylineDefinition): L.Polyline;
-    protected removePolyline(polyline: L.Polyline): void;
+    protected doCreateMarker({ definition }: {
+        definition: MarkerDefinition<MarkerOptions, PopupOptions>;
+    }): L.Marker;
+    protected doRemoveMarker(marker: L.Marker): void;
+    protected doCreatePolygon({ definition, }: {
+        definition: PolygonDefinition<PolygonOptions, PopupOptions>;
+    }): L.Polygon;
+    protected doRemovePolygon(polygon: L.Polygon): void;
+    protected doCreatePolyline({ definition, }: {
+        definition: PolylineDefinition<PolylineOptions, PopupOptions>;
+    }): L.Polyline;
+    protected doRemovePolyline(polyline: L.Polyline): void;
     protected doCreateInfoWindow({ definition, element, }: {
-        definition: MarkerDefinition<typeof L.Marker, typeof L.Popup>['infoWindow'];
-        element: L.Marker;
-    } | {
-        definition: PolygonDefinition<typeof L.Polygon, typeof L.Popup>['infoWindow'];
-        element: L.Polygon;
-    } | {
-        definition: PolylineDefinition<typeof L.Polyline, typeof L.Popup>['infoWindow'];
-        element: L.Polyline;
+        definition: InfoWindowWithoutPositionDefinition<PopupOptions>;
+        element: L.Marker | L.Polygon | L.Polyline;
     }): L.Popup;
     protected doFitBoundsToMarkers(): void;
-    centerValueChanged(): void;
-    zoomValueChanged(): void;
 }
 export {};
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 088c2706..f5bf72b5 100644
--- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
+++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
@@ -6,17 +6,21 @@ class default_1 extends Controller {
     constructor() {
         super(...arguments);
         this.markers = new Map();
-        this.infoWindows = [];
         this.polygons = new Map();
         this.polylines = new Map();
+        this.infoWindows = [];
+        this.isConnected = false;
     }
     connect() {
         const options = this.optionsValue;
         this.dispatchEvent('pre-connect', { options });
+        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.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options });
-        this.markersValue.forEach((marker) => this.createMarker(marker));
-        this.polygonsValue.forEach((polygon) => this.createPolygon(polygon));
-        this.polylinesValue.forEach((polyline) => this.createPolyline(polyline));
+        this.markersValue.forEach((definition) => this.createMarker({ definition }));
+        this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
+        this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
         if (this.fitBoundsToMarkersValue) {
             this.doFitBoundsToMarkers();
         }
@@ -27,30 +31,7 @@ class default_1 extends Controller {
             polylines: [...this.polylines.values()],
             infoWindows: this.infoWindows,
         });
-    }
-    createMarker(definition) {
-        this.dispatchEvent('marker:before-create', { definition });
-        const marker = this.doCreateMarker(definition);
-        this.dispatchEvent('marker:after-create', { marker });
-        marker['@id'] = definition['@id'];
-        this.markers.set(definition['@id'], marker);
-        return marker;
-    }
-    createPolygon(definition) {
-        this.dispatchEvent('polygon:before-create', { definition });
-        const polygon = this.doCreatePolygon(definition);
-        this.dispatchEvent('polygon:after-create', { polygon });
-        polygon['@id'] = definition['@id'];
-        this.polygons.set(definition['@id'], polygon);
-        return polygon;
-    }
-    createPolyline(definition) {
-        this.dispatchEvent('polyline:before-create', { definition });
-        const polyline = this.doCreatePolyline(definition);
-        this.dispatchEvent('polyline:after-create', { polyline });
-        polyline['@id'] = definition['@id'];
-        this.polylines.set(definition['@id'], polyline);
-        return polyline;
+        this.isConnected = true;
     }
     createInfoWindow({ definition, element, }) {
         this.dispatchEvent('info-window:before-create', { definition, element });
@@ -60,53 +41,50 @@ class default_1 extends Controller {
         return infoWindow;
     }
     markersValueChanged() {
-        if (!this.map) {
+        if (!this.isConnected) {
             return;
         }
-        this.markers.forEach((marker) => {
-            if (!this.markersValue.find((m) => m['@id'] === marker['@id'])) {
-                this.removeMarker(marker);
-                this.markers.delete(marker['@id']);
-            }
-        });
-        this.markersValue.forEach((marker) => {
-            if (!this.markers.has(marker['@id'])) {
-                this.createMarker(marker);
-            }
-        });
+        this.onDrawChanged(this.markers, this.markersValue, this.createMarker, this.doRemoveMarker);
         if (this.fitBoundsToMarkersValue) {
             this.doFitBoundsToMarkers();
         }
     }
     polygonsValueChanged() {
-        if (!this.map) {
+        if (!this.isConnected) {
             return;
         }
-        this.polygons.forEach((polygon) => {
-            if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) {
-                this.removePolygon(polygon);
-                this.polygons.delete(polygon['@id']);
-            }
-        });
-        this.polygonsValue.forEach((polygon) => {
-            if (!this.polygons.has(polygon['@id'])) {
-                this.createPolygon(polygon);
-            }
-        });
+        this.onDrawChanged(this.polygons, this.polygonsValue, this.createPolygon, this.doRemovePolygon);
     }
     polylinesValueChanged() {
-        if (!this.map) {
+        if (!this.isConnected) {
             return;
         }
-        this.polylines.forEach((polyline) => {
-            if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) {
-                this.removePolyline(polyline);
-                this.polylines.delete(polyline['@id']);
-            }
+        this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline);
+    }
+    createDrawingFactory(type, draws, factory) {
+        const eventBefore = `${type}:before-create`;
+        const eventAfter = `${type}:after-create`;
+        return ({ definition }) => {
+            this.dispatchEvent(eventBefore, { definition });
+            const drawing = factory({ definition });
+            this.dispatchEvent(eventAfter, { [type]: drawing });
+            draws.set(definition['@id'], drawing);
+            return drawing;
+        };
+    }
+    onDrawChanged(draws, newDrawDefinitions, factory, remover) {
+        const idsToRemove = new Set(draws.keys());
+        newDrawDefinitions.forEach((definition) => {
+            idsToRemove.delete(definition['@id']);
+        });
+        idsToRemove.forEach((id) => {
+            const draw = draws.get(id);
+            remover(draw);
+            draws.delete(id);
         });
-        this.polylinesValue.forEach((polyline) => {
-            if (!this.polylines.has(polyline['@id'])) {
-                this.createPolyline(polyline);
+        newDrawDefinitions.forEach((definition) => {
+            if (!draws.has(definition['@id'])) {
+                factory({ definition });
             }
         });
     }
@@ -133,6 +111,16 @@ class map_controller extends default_1 {
         });
         super.connect();
     }
+    centerValueChanged() {
+        if (this.map && this.centerValue && this.zoomValue) {
+            this.map.setView(this.centerValue, this.zoomValue);
+        }
+    }
+    zoomValueChanged() {
+        if (this.map && this.zoomValue) {
+            this.map.setZoom(this.zoomValue);
+        }
+    }
     dispatchEvent(name, payload = {}) {
         this.dispatch(name, {
             prefix: 'ux:map',
@@ -154,18 +142,18 @@ class map_controller extends default_1 {
         }).addTo(map);
         return map;
     }
-    doCreateMarker(definition) {
+    doCreateMarker({ definition }) {
         const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
-        const marker = L.marker(position, { title, ...otherOptions, ...rawOptions }).addTo(this.map);
+        const marker = L.marker(position, { title: title || undefined, ...otherOptions, ...rawOptions }).addTo(this.map);
         if (infoWindow) {
             this.createInfoWindow({ definition: infoWindow, element: marker });
         }
         return marker;
     }
-    removeMarker(marker) {
+    doRemoveMarker(marker) {
         marker.remove();
     }
-    doCreatePolygon(definition) {
+    doCreatePolygon({ definition, }) {
         const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
         const polygon = L.polygon(points, { ...rawOptions }).addTo(this.map);
         if (title) {
@@ -176,10 +164,10 @@ class map_controller extends default_1 {
         }
         return polygon;
     }
-    removePolygon(polygon) {
+    doRemovePolygon(polygon) {
         polygon.remove();
     }
-    doCreatePolyline(definition) {
+    doCreatePolyline({ definition, }) {
         const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
         const polyline = L.polyline(points, { ...rawOptions }).addTo(this.map);
         if (title) {
@@ -190,7 +178,7 @@ class map_controller extends default_1 {
         }
         return polyline;
     }
-    removePolyline(polyline) {
+    doRemovePolyline(polyline) {
         polyline.remove();
     }
     doCreateInfoWindow({ definition, element, }) {
@@ -206,23 +194,15 @@ class map_controller extends default_1 {
         return popup;
     }
     doFitBoundsToMarkers() {
-        if (this.markers.length === 0) {
+        if (this.markers.size === 0) {
             return;
         }
-        this.map.fitBounds(this.markers.map((marker) => {
+        const bounds = [];
+        this.markers.forEach((marker) => {
             const position = marker.getLatLng();
-            return [position.lat, position.lng];
-        }));
-    }
-    centerValueChanged() {
-        if (this.map) {
-            this.map.setView(this.centerValue, this.zoomValue);
-        }
-    }
-    zoomValueChanged() {
-        if (this.map) {
-            this.map.setZoom(this.zoomValue);
-        }
+            bounds.push([position.lat, position.lng]);
+        });
+        this.map.fitBounds(bounds);
     }
 }
 
diff --git a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
index 86cc334c..f0c79637 100644
--- a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
@@ -1,5 +1,11 @@
 import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
+import type {
+    Point,
+    MarkerDefinition,
+    PolygonDefinition,
+    PolylineDefinition,
+    InfoWindowWithoutPositionDefinition,
+} from '@symfony/ux-map';
 import 'leaflet/dist/leaflet.min.css';
 import * as L from 'leaflet';
 import type {
@@ -8,6 +14,7 @@ import type {
     PopupOptions,
     PolylineOptions as PolygonOptions,
     PolylineOptions,
+    LatLngBoundsExpression,
 } from 'leaflet';
 
 type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
@@ -16,16 +23,18 @@ type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
 
 export default class extends AbstractMapController<
     MapOptions,
-    typeof L.Map,
+    L.Map,
     MarkerOptions,
-    typeof L.Marker,
+    L.Marker,
     PopupOptions,
-    typeof L.Popup,
+    L.Popup,
     PolygonOptions,
-    typeof L.Polygon,
+    L.Polygon,
     PolylineOptions,
-    typeof L.Polyline
+    L.Polyline
 > {
+    declare map: L.Map;
+
     connect(): void {
         L.Marker.prototype.options.icon = L.divIcon({
             html: '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linecap="round" clip-rule="evenodd" viewBox="0 0 500 820"><defs><linearGradient id="__sf_ux_map_gradient_marker_fill" x1="0" x2="1" y1="0" y2="0" gradientTransform="matrix(0 -37.57 37.57 0 416.45 541)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#126FC6"/><stop offset="1" stop-color="#4C9CD1"/></linearGradient><linearGradient id="__sf_ux_map_gradient_marker_border" x1="0" x2="1" y1="0" y2="0" gradientTransform="matrix(0 -19.05 19.05 0 414.48 522.49)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2E6C97"/><stop offset="1" stop-color="#3883B7"/></linearGradient></defs><circle cx="252.31" cy="266.24" r="83.99" fill="#fff"/><path fill="url(#__sf_ux_map_gradient_marker_fill)" stroke="url(#__sf_ux_map_gradient_marker_border)" stroke-width="1.1" d="M416.54 503.61c-6.57 0-12.04 5.7-12.04 11.87 0 2.78 1.56 6.3 2.7 8.74l9.3 17.88 9.26-17.88c1.13-2.43 2.74-5.79 2.74-8.74 0-6.18-5.38-11.87-11.96-11.87Zm0 7.16a4.69 4.69 0 1 1-.02 9.4 4.69 4.69 0 0 1 .02-9.4Z" transform="translate(-7889.1 -9807.44) scale(19.54)"/></svg>',
@@ -38,6 +47,18 @@ export default class extends AbstractMapController<
         super.connect();
     }
 
+    public centerValueChanged(): void {
+        if (this.map && this.centerValue && this.zoomValue) {
+            this.map.setView(this.centerValue, this.zoomValue);
+        }
+    }
+
+    public zoomValueChanged(): void {
+        if (this.map && this.zoomValue) {
+            this.map.setZoom(this.zoomValue);
+        }
+    }
+
     protected dispatchEvent(name: string, payload: Record<string, unknown> = {}): void {
         this.dispatch(name, {
             prefix: 'ux:map',
@@ -67,10 +88,12 @@ export default class extends AbstractMapController<
         return map;
     }
 
-    protected doCreateMarker(definition: MarkerDefinition<typeof L.Marker, typeof L.Popup>): L.Marker {
+    protected doCreateMarker({ definition }: { definition: MarkerDefinition<MarkerOptions, PopupOptions> }): L.Marker {
         const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
 
-        const marker = L.marker(position, { title, ...otherOptions, ...rawOptions }).addTo(this.map);
+        const marker = L.marker(position, { title: title || undefined, ...otherOptions, ...rawOptions }).addTo(
+            this.map
+        );
 
         if (infoWindow) {
             this.createInfoWindow({ definition: infoWindow, element: marker });
@@ -79,11 +102,13 @@ export default class extends AbstractMapController<
         return marker;
     }
 
-    protected removeMarker(marker: L.Marker): void {
+    protected doRemoveMarker(marker: L.Marker): void {
         marker.remove();
     }
 
-    protected doCreatePolygon(definition: PolygonDefinition<typeof L.Polygon, typeof L.Popup>): L.Polygon {
+    protected doCreatePolygon({
+        definition,
+    }: { definition: PolygonDefinition<PolygonOptions, PopupOptions> }): L.Polygon {
         const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
 
         const polygon = L.polygon(points, { ...rawOptions }).addTo(this.map);
@@ -99,11 +124,13 @@ export default class extends AbstractMapController<
         return polygon;
     }
 
-    protected removePolygon(polygon: L.Polygon) {
+    protected doRemovePolygon(polygon: L.Polygon) {
         polygon.remove();
     }
 
-    protected doCreatePolyline(definition: PolylineDefinition): L.Polyline {
+    protected doCreatePolyline({
+        definition,
+    }: { definition: PolylineDefinition<PolylineOptions, PopupOptions> }): L.Polyline {
         const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
 
         const polyline = L.polyline(points, { ...rawOptions }).addTo(this.map);
@@ -119,23 +146,17 @@ export default class extends AbstractMapController<
         return polyline;
     }
 
-    protected removePolyline(polyline: L.Polyline): void {
+    protected doRemovePolyline(polyline: L.Polyline): void {
         polyline.remove();
     }
 
     protected doCreateInfoWindow({
         definition,
         element,
-    }:
-        | {
-              definition: MarkerDefinition<typeof L.Marker, typeof L.Popup>['infoWindow'];
-              element: L.Marker;
-          }
-        | { definition: PolygonDefinition<typeof L.Polygon, typeof L.Popup>['infoWindow']; element: L.Polygon }
-        | {
-              definition: PolylineDefinition<typeof L.Polyline, typeof L.Popup>['infoWindow'];
-              element: L.Polyline;
-          }): L.Popup {
+    }: {
+        definition: InfoWindowWithoutPositionDefinition<PopupOptions>;
+        element: L.Marker | L.Polygon | L.Polyline;
+    }): L.Popup {
         const { headerContent, content, rawOptions = {}, ...otherOptions } = definition;
 
         element.bindPopup([headerContent, content].filter((x) => x).join('<br>'), { ...otherOptions, ...rawOptions });
@@ -153,28 +174,15 @@ export default class extends AbstractMapController<
     }
 
     protected doFitBoundsToMarkers(): void {
-        if (this.markers.length === 0) {
+        if (this.markers.size === 0) {
             return;
         }
 
-        this.map.fitBounds(
-            this.markers.map((marker: L.Marker) => {
-                const position = marker.getLatLng();
-
-                return [position.lat, position.lng];
-            })
-        );
-    }
-
-    public centerValueChanged(): void {
-        if (this.map) {
-            this.map.setView(this.centerValue, this.zoomValue);
-        }
-    }
-
-    public zoomValueChanged(): void {
-        if (this.map) {
-            this.map.setZoom(this.zoomValue);
-        }
+        const bounds: LatLngBoundsExpression = [];
+        this.markers.forEach((marker) => {
+            const position = marker.getLatLng();
+            bounds.push([position.lat, position.lng]);
+        });
+        this.map.fitBounds(bounds);
     }
 }


Test patch:
diff --git a/src/LiveComponent/assets/test/dom_utils.test.ts b/src/LiveComponent/assets/test/dom_utils.test.ts
index 40a4bfb6..2afdbafd 100644
--- a/src/LiveComponent/assets/test/dom_utils.test.ts
+++ b/src/LiveComponent/assets/test/dom_utils.test.ts
@@ -271,19 +271,11 @@ describe('elementBelongsToThisComponent', () => {
         expect(elementBelongsToThisComponent(targetElement, component)).toBeFalsy();
     });
 
-    it('returns true if element lives inside of a div', () => {
-        const targetElement = htmlToElement('<input name="user[firstName]"/>');
+    it('returns true if element lives inside of controller', () => {
+        const targetElement = htmlToElement('<input name="user[firstName]">');
         const component = createComponent('<div></div>');
         component.element.appendChild(targetElement);
 
-        expect(elementBelongsToThisComponent(targetElement, component)).toBeFalsy();
-    });
-
-    it('returns true if element lives inside of live controller', () => {
-        const targetElement = htmlToElement('<input name="user[firstName]"/>');
-        const component = createComponent('<div data-controller="live" data-live-name-value="parentLabel"></div>');
-        component.element.appendChild(targetElement);
-
         expect(elementBelongsToThisComponent(targetElement, component)).toBeTruthy();
     });
 
@@ -295,6 +287,7 @@ describe('elementBelongsToThisComponent', () => {
         const component = createComponent('<div class="parent"></div>');
         component.element.appendChild(childComponent.element);
 
+        //expect(elementBelongsToThisComponent(targetElement, childComponent)).toBeTruthy();
         expect(elementBelongsToThisComponent(targetElement, component)).toBeFalsy();
     });
 
diff --git a/src/Map/assets/test/abstract_map_controller.test.ts b/src/Map/assets/test/abstract_map_controller.test.ts
index b3d2623a..691f2c5d 100644
--- a/src/Map/assets/test/abstract_map_controller.test.ts
+++ b/src/Map/assets/test/abstract_map_controller.test.ts
@@ -16,7 +16,7 @@ class MyMapController extends AbstractMapController {
         return { map: 'map', center, zoom, options };
     }
 
-    doCreateMarker(definition) {
+    doCreateMarker({ definition }) {
         const marker = { marker: 'marker', title: definition.title };
 
         if (definition.infoWindow) {
@@ -26,7 +26,7 @@ class MyMapController extends AbstractMapController {
         return marker;
     }
 
-    doCreatePolygon(definition) {
+    doCreatePolygon({ definition }) {
         const polygon = { polygon: 'polygon', title: definition.title };
 
         if (definition.infoWindow) {
@@ -35,7 +35,7 @@ class MyMapController extends AbstractMapController {
         return polygon;
     }
 
-    doCreatePolyline(definition) {
+    doCreatePolyline({ definition }) {
         const polyline = { polyline: 'polyline', title: definition.title };
 
         if (definition.infoWindow) {
@@ -103,20 +103,18 @@ describe('AbstractMapController', () => {
         expect(controller.map).toEqual({ map: 'map', center: { lat: 48.8566, lng: 2.3522 }, zoom: 4, options: {} });
         expect(controller.markers).toEqual(
             new Map([
-                ['a69f13edd2e571f3', { '@id': 'a69f13edd2e571f3', marker: 'marker', title: 'Paris' }],
-                ['cb9c1a30d562694b', { '@id': 'cb9c1a30d562694b', marker: 'marker', title: 'Lyon' }],
-                ['e6b3acef1325fb52', { '@id': 'e6b3acef1325fb52', marker: 'marker', title: 'Toulouse' }],
+                ['a69f13edd2e571f3', { marker: 'marker', title: 'Paris' }],
+                ['cb9c1a30d562694b', { marker: 'marker', title: 'Lyon' }],
+                ['e6b3acef1325fb52', { marker: 'marker', title: 'Toulouse' }],
             ])
         );
         expect(controller.polygons).toEqual(
             new Map([
-                ['228ae6f5c1b17cfd', { '@id': '228ae6f5c1b17cfd', polygon: 'polygon', title: null }],
-                ['9874334e4e8caa16', { '@id': '9874334e4e8caa16', polygon: 'polygon', title: null }],
+                ['228ae6f5c1b17cfd', { polygon: 'polygon', title: null }],
+                ['9874334e4e8caa16', { polygon: 'polygon', title: null }],
             ])
         );
-        expect(controller.polylines).toEqual(
-            new Map([['0fa955da866c7720', { '@id': '0fa955da866c7720', polyline: 'polyline', title: null }]])
-        );
+        expect(controller.polylines).toEqual(new Map([['0fa955da866c7720', { polyline: 'polyline', title: null }]]));
         expect(controller.infoWindows).toEqual([
             {
                 headerContent: 'Paris',