Skip to content

Commit 7bef0bf

Browse files
committed
feature #2385 [Map] Make UX Map compatible with Live Components (and some internal things) (Kocal)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Map] Make UX Map compatible with Live Components (and some internal things) Q | A | ------------- | --- | Bug fix? | no | New feature? | yes <!-- 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 Here’s a corrected and polished version of your text: --- Hi! 😊 This PR enhances the UX Map to be compatible with Live Components, allowing you to interact with the `Map` directly from your PHP code. You can perform actions and see your map update in real-time from the front-end! To achieve this, I had to refactor and improve several areas: 1. Due to the hydration and de-hydration processes of Live Components, I ensured that the `toArray` and new `fromArray` methods for `Map`, `Marker`, and all other value objects could rebuild an equivalent object accurately (e.g., `$marker == Marker::fromArray($marker->toArray())`). 2. Since Stimulus monitors value changes, it wasn’t efficient to store everything in a single large `view` object containing `zoom`, `center`, `markers`, etc. These properties were split into individual values to improve performance. 3. Before rendering the map, an ``@id`` is now automatically computed for each `Marker` and `Polygon` definition. This optimization makes rendering significantly more efficient, avoiding the need to remove and re-render all markers or polygons (and avoiding a visual glitch aswell). https://github.com/user-attachments/assets/c151e64c-7321-46d3-a5c6-cfeaf57beb47 https://github.com/user-attachments/assets/ffb12035-a0c3-48b3-afb7-ddf2c24c318a Commits ------- 74d937a Apply `@smnandre`'s suggestions from code review 7d30e20 [Map] Listen for zoom, center, markers and polygons changes, and update JS tests aae9ddd [Map] Complete and simplify the normalization/denormalization process of Map's value objects, add MapOptionsNormalizer 4744d3d [Map] Add "fromArray" methods to options and other DTO, make "*Array" methods internal 8ab164b [Map] Split "view" attribute into multiple attributes
2 parents 08be7d5 + 74d937a commit 7bef0bf

40 files changed

+1127
-259
lines changed

src/Map/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Add method `Symfony\UX\Map\Renderer\AbstractRenderer::tapOptions()`, to allow Renderer to modify options before rendering a Map.
66
- Add `ux_map.google_maps.default_map_id` configuration to set the Google ``Map ID``
7+
- Add `ComponentWithMapTrait` to ease maps integration in [Live Components](https://symfony.com/bundles/ux-live-component/current/index.html)
78

89
## 2.20
910

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

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,16 @@ export type Point = {
33
lat: number;
44
lng: number;
55
};
6-
export type MapView<Options, MarkerOptions, InfoWindowOptions, PolygonOptions> = {
7-
center: Point | null;
8-
zoom: number | null;
9-
fitBoundsToMarkers: boolean;
10-
markers: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
11-
polygons: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
12-
options: Options;
13-
};
146
export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
7+
'@id': string;
158
position: Point;
169
title: string | null;
1710
infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
1811
rawOptions?: MarkerOptions;
1912
extra: Record<string, unknown>;
2013
};
2114
export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
15+
'@id': string;
2216
infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
2317
points: Array<Point>;
2418
title: string | null;
@@ -37,22 +31,33 @@ export type InfoWindowDefinition<InfoWindowOptions> = {
3731
export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow, PolygonOptions, Polygon> extends Controller<HTMLElement> {
3832
static values: {
3933
providerOptions: ObjectConstructor;
40-
view: ObjectConstructor;
34+
center: ObjectConstructor;
35+
zoom: NumberConstructor;
36+
fitBoundsToMarkers: BooleanConstructor;
37+
markers: ArrayConstructor;
38+
polygons: ArrayConstructor;
39+
options: ObjectConstructor;
4140
};
42-
viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions, PolygonOptions>;
41+
centerValue: Point | null;
42+
zoomValue: number | null;
43+
fitBoundsToMarkersValue: boolean;
44+
markersValue: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
45+
polygonsValue: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
46+
optionsValue: MapOptions;
4347
protected map: Map;
44-
protected markers: Array<Marker>;
48+
protected markers: globalThis.Map<any, any>;
4549
protected infoWindows: Array<InfoWindow>;
46-
protected polygons: Array<Polygon>;
50+
protected polygons: globalThis.Map<any, any>;
4751
connect(): void;
4852
protected abstract doCreateMap({ center, zoom, options, }: {
4953
center: Point | null;
5054
zoom: number | null;
5155
options: MapOptions;
5256
}): Map;
5357
createMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
54-
createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
58+
protected abstract removeMarker(marker: Marker): void;
5559
protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
60+
createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
5661
protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
5762
protected createInfoWindow({ definition, element, }: {
5863
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'] | PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
@@ -67,4 +72,8 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
6772
}): InfoWindow;
6873
protected abstract doFitBoundsToMarkers(): void;
6974
protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
75+
abstract centerValueChanged(): void;
76+
abstract zoomValueChanged(): void;
77+
markersValueChanged(): void;
78+
polygonsValueChanged(): void;
7079
}

src/Map/assets/dist/abstract_map_controller.js

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,40 @@ import { Controller } from '@hotwired/stimulus';
33
class default_1 extends Controller {
44
constructor() {
55
super(...arguments);
6-
this.markers = [];
6+
this.markers = new Map();
77
this.infoWindows = [];
8-
this.polygons = [];
8+
this.polygons = new Map();
99
}
1010
connect() {
11-
const { center, zoom, options, markers, polygons, fitBoundsToMarkers } = this.viewValue;
11+
const options = this.optionsValue;
1212
this.dispatchEvent('pre-connect', { options });
13-
this.map = this.doCreateMap({ center, zoom, options });
14-
markers.forEach((marker) => this.createMarker(marker));
15-
polygons.forEach((polygon) => this.createPolygon(polygon));
16-
if (fitBoundsToMarkers) {
13+
this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options });
14+
this.markersValue.forEach((marker) => this.createMarker(marker));
15+
this.polygonsValue.forEach((polygon) => this.createPolygon(polygon));
16+
if (this.fitBoundsToMarkersValue) {
1717
this.doFitBoundsToMarkers();
1818
}
1919
this.dispatchEvent('connect', {
2020
map: this.map,
21-
markers: this.markers,
22-
polygons: this.polygons,
21+
markers: [...this.markers.values()],
22+
polygons: [...this.polygons.values()],
2323
infoWindows: this.infoWindows,
2424
});
2525
}
2626
createMarker(definition) {
2727
this.dispatchEvent('marker:before-create', { definition });
2828
const marker = this.doCreateMarker(definition);
2929
this.dispatchEvent('marker:after-create', { marker });
30-
this.markers.push(marker);
30+
marker['@id'] = definition['@id'];
31+
this.markers.set(definition['@id'], marker);
3132
return marker;
3233
}
3334
createPolygon(definition) {
3435
this.dispatchEvent('polygon:before-create', { definition });
3536
const polygon = this.doCreatePolygon(definition);
3637
this.dispatchEvent('polygon:after-create', { polygon });
37-
this.polygons.push(polygon);
38+
polygon['@id'] = definition['@id'];
39+
this.polygons.set(definition['@id'], polygon);
3840
return polygon;
3941
}
4042
createInfoWindow({ definition, element, }) {
@@ -44,10 +46,50 @@ class default_1 extends Controller {
4446
this.infoWindows.push(infoWindow);
4547
return infoWindow;
4648
}
49+
markersValueChanged() {
50+
if (!this.map) {
51+
return;
52+
}
53+
this.markers.forEach((marker) => {
54+
if (!this.markersValue.find((m) => m['@id'] === marker['@id'])) {
55+
this.removeMarker(marker);
56+
this.markers.delete(marker['@id']);
57+
}
58+
});
59+
this.markersValue.forEach((marker) => {
60+
if (!this.markers.has(marker['@id'])) {
61+
this.createMarker(marker);
62+
}
63+
});
64+
if (this.fitBoundsToMarkersValue) {
65+
this.doFitBoundsToMarkers();
66+
}
67+
}
68+
polygonsValueChanged() {
69+
if (!this.map) {
70+
return;
71+
}
72+
this.polygons.forEach((polygon) => {
73+
if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) {
74+
polygon.remove();
75+
this.polygons.delete(polygon['@id']);
76+
}
77+
});
78+
this.polygonsValue.forEach((polygon) => {
79+
if (!this.polygons.has(polygon['@id'])) {
80+
this.createPolygon(polygon);
81+
}
82+
});
83+
}
4784
}
4885
default_1.values = {
4986
providerOptions: Object,
50-
view: Object,
87+
center: Object,
88+
zoom: Number,
89+
fitBoundsToMarkers: Boolean,
90+
markers: Array,
91+
polygons: Array,
92+
options: Object,
5193
};
5294

5395
export { default_1 as default };

src/Map/assets/src/abstract_map_controller.ts

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,8 @@ import { Controller } from '@hotwired/stimulus';
22

33
export type Point = { lat: number; lng: number };
44

5-
export type MapView<Options, MarkerOptions, InfoWindowOptions, PolygonOptions> = {
6-
center: Point | null;
7-
zoom: number | null;
8-
fitBoundsToMarkers: boolean;
9-
markers: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
10-
polygons: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
11-
options: Options;
12-
};
13-
145
export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
6+
'@id': string;
157
position: Point;
168
title: string | null;
179
infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
@@ -29,6 +21,7 @@ export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
2921
};
3022

3123
export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
24+
'@id': string;
3225
infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
3326
points: Array<Point>;
3427
title: string | null;
@@ -68,35 +61,45 @@ export default abstract class<
6861
> extends Controller<HTMLElement> {
6962
static values = {
7063
providerOptions: Object,
71-
view: Object,
64+
center: Object,
65+
zoom: Number,
66+
fitBoundsToMarkers: Boolean,
67+
markers: Array,
68+
polygons: Array,
69+
options: Object,
7270
};
7371

74-
declare viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions, PolygonOptions>;
72+
declare centerValue: Point | null;
73+
declare zoomValue: number | null;
74+
declare fitBoundsToMarkersValue: boolean;
75+
declare markersValue: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
76+
declare polygonsValue: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
77+
declare optionsValue: MapOptions;
7578

7679
protected map: Map;
77-
protected markers: Array<Marker> = [];
80+
protected markers = new Map<Marker>();
7881
protected infoWindows: Array<InfoWindow> = [];
79-
protected polygons: Array<Polygon> = [];
82+
protected polygons = new Map<Polygon>();
8083

8184
connect() {
82-
const { center, zoom, options, markers, polygons, fitBoundsToMarkers } = this.viewValue;
85+
const options = this.optionsValue;
8386

8487
this.dispatchEvent('pre-connect', { options });
8588

86-
this.map = this.doCreateMap({ center, zoom, options });
89+
this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options });
8790

88-
markers.forEach((marker) => this.createMarker(marker));
91+
this.markersValue.forEach((marker) => this.createMarker(marker));
8992

90-
polygons.forEach((polygon) => this.createPolygon(polygon));
93+
this.polygonsValue.forEach((polygon) => this.createPolygon(polygon));
9194

92-
if (fitBoundsToMarkers) {
95+
if (this.fitBoundsToMarkersValue) {
9396
this.doFitBoundsToMarkers();
9497
}
9598

9699
this.dispatchEvent('connect', {
97100
map: this.map,
98-
markers: this.markers,
99-
polygons: this.polygons,
101+
markers: [...this.markers.values()],
102+
polygons: [...this.polygons.values()],
100103
infoWindows: this.infoWindows,
101104
});
102105
}
@@ -116,20 +119,29 @@ export default abstract class<
116119
const marker = this.doCreateMarker(definition);
117120
this.dispatchEvent('marker:after-create', { marker });
118121

119-
this.markers.push(marker);
122+
marker['@id'] = definition['@id'];
123+
124+
this.markers.set(definition['@id'], marker);
120125

121126
return marker;
122127
}
123128

124-
createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon {
129+
protected abstract removeMarker(marker: Marker): void;
130+
131+
protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
132+
133+
public createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon {
125134
this.dispatchEvent('polygon:before-create', { definition });
126135
const polygon = this.doCreatePolygon(definition);
127136
this.dispatchEvent('polygon:after-create', { polygon });
128-
this.polygons.push(polygon);
137+
138+
polygon['@id'] = definition['@id'];
139+
140+
this.polygons.set(definition['@id'], polygon);
141+
129142
return polygon;
130143
}
131144

132-
protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
133145
protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
134146

135147
protected createInfoWindow({
@@ -166,4 +178,50 @@ export default abstract class<
166178
protected abstract doFitBoundsToMarkers(): void;
167179

168180
protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
181+
182+
public abstract centerValueChanged(): void;
183+
184+
public abstract zoomValueChanged(): void;
185+
186+
public markersValueChanged(): void {
187+
if (!this.map) {
188+
return;
189+
}
190+
191+
this.markers.forEach((marker) => {
192+
if (!this.markersValue.find((m) => m['@id'] === marker['@id'])) {
193+
this.removeMarker(marker);
194+
this.markers.delete(marker['@id']);
195+
}
196+
});
197+
198+
this.markersValue.forEach((marker) => {
199+
if (!this.markers.has(marker['@id'])) {
200+
this.createMarker(marker);
201+
}
202+
});
203+
204+
if (this.fitBoundsToMarkersValue) {
205+
this.doFitBoundsToMarkers();
206+
}
207+
}
208+
209+
public polygonsValueChanged(): void {
210+
if (!this.map) {
211+
return;
212+
}
213+
214+
this.polygons.forEach((polygon) => {
215+
if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) {
216+
polygon.remove();
217+
this.polygons.delete(polygon['@id']);
218+
}
219+
});
220+
221+
this.polygonsValue.forEach((polygon) => {
222+
if (!this.polygons.has(polygon['@id'])) {
223+
this.createPolygon(polygon);
224+
}
225+
});
226+
}
169227
}

0 commit comments

Comments
 (0)