@mapvx/web-components-ssr 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +1065 -0
- package/dist/browser/main.js +1532 -0
- package/dist/browser/styles.css +1 -0
- package/dist/types/public-api.d.ts +1132 -0
- package/package.json +84 -0
package/README.md
ADDED
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
# MapVX Website Component
|
|
2
|
+
|
|
3
|
+
A modern, framework-agnostic web component built with Angular 20 that provides an interactive map and location discovery interface.
|
|
4
|
+
|
|
5
|
+
## 🚀 Features
|
|
6
|
+
|
|
7
|
+
- ✅ Compatible with any framework (React, Vue, Vanilla JS, etc.)
|
|
8
|
+
- ✅ Self-contained and isolated (Shadow DOM)
|
|
9
|
+
- ✅ Optimized bundle without hashing for production
|
|
10
|
+
- ✅ Angular 20 with modern architecture (standalone components)
|
|
11
|
+
- 🔑 **Requires API Key**: The component needs a valid API key to function
|
|
12
|
+
|
|
13
|
+
## 📦 Installation
|
|
14
|
+
|
|
15
|
+
### Via NPM
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @mapvx/web-components-ssr
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
After installation, you can reference the files in your application:
|
|
22
|
+
|
|
23
|
+
**JavaScript/TypeScript:**
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
// Import the main component file
|
|
27
|
+
import '@mapvx/web-components-ssr/dist/browser/main.js'
|
|
28
|
+
// Import the styles
|
|
29
|
+
import '@mapvx/web-components-ssr/dist/browser/styles.css'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**HTML:**
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<!-- Include the component files -->
|
|
36
|
+
<script src="node_modules/@mapvx/web-components-ssr/dist/browser/main.js"></script>
|
|
37
|
+
<link rel="stylesheet" href="node_modules/@mapvx/web-components-ssr/dist/browser/styles.css" />
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Webpack/Vite/Bundler:**
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
// In your main entry file
|
|
44
|
+
import '@mapvx/web-components-ssr/dist/browser/main.js'
|
|
45
|
+
import '@mapvx/web-components-ssr/dist/browser/styles.css'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Framework-specific examples:**
|
|
49
|
+
|
|
50
|
+
**React:**
|
|
51
|
+
|
|
52
|
+
```jsx
|
|
53
|
+
// In your main App.jsx or index.jsx
|
|
54
|
+
import '@mapvx/web-components-ssr/dist/browser/main.js'
|
|
55
|
+
|
|
56
|
+
function App() {
|
|
57
|
+
return <mapvx-website api-key="your-api-key-here" />
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```css
|
|
62
|
+
/* In your main CSS file (e.g., index.css, App.css) */
|
|
63
|
+
@import '@mapvx/web-components-ssr/dist/browser/styles.css';
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Vue:**
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
// In your main.js
|
|
70
|
+
import '@mapvx/web-components-ssr/dist/browser/main.js'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```css
|
|
74
|
+
/* In your main CSS file (e.g., style.css, main.css) */
|
|
75
|
+
@import '@mapvx/web-components-ssr/dist/browser/styles.css';
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Angular:**
|
|
79
|
+
|
|
80
|
+
#### Method 1: HTML Script Tags
|
|
81
|
+
|
|
82
|
+
Configure assets in `angular.json` and use script tags in `index.html`:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"assets": [
|
|
87
|
+
{
|
|
88
|
+
"glob": "**/*",
|
|
89
|
+
"input": "public"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"glob": "**/*",
|
|
93
|
+
"input": "src/assets",
|
|
94
|
+
"output": "assets"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"glob": "**/*",
|
|
98
|
+
"input": "node_modules/@mapvx/web-components-ssr/dist/browser",
|
|
99
|
+
"output": "website-component"
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then reference the files in your `index.html`:
|
|
106
|
+
|
|
107
|
+
```html
|
|
108
|
+
<!-- Web Component Scripts -->
|
|
109
|
+
<script src="website-component/main.js" defer></script>
|
|
110
|
+
|
|
111
|
+
<!-- Web Component Styles -->
|
|
112
|
+
<link rel="stylesheet" href="website-component/styles.css" />
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Why this configuration is needed:**
|
|
116
|
+
|
|
117
|
+
- Angular doesn't serve files from `node_modules` by default in development
|
|
118
|
+
- Without proper asset configuration, you'll get `SyntaxError: Unexpected token '<'` errors
|
|
119
|
+
- This ensures files are served as static assets with proper caching headers
|
|
120
|
+
|
|
121
|
+
#### Method 2: TypeScript Imports
|
|
122
|
+
|
|
123
|
+
Alternatively, you can import the files in your application:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// In your main.ts - Import the JavaScript
|
|
127
|
+
import '@mapvx/web-components-ssr/dist/browser/main.js'
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```scss
|
|
131
|
+
/* In your styles.scss - Import the styles (Modern Sass) */
|
|
132
|
+
@use '@mapvx/web-components-ssr/dist/browser/styles.css';
|
|
133
|
+
|
|
134
|
+
/* Alternative for older Sass versions or CSS files */
|
|
135
|
+
@import '@mapvx/web-components-ssr/dist/browser/styles.css';
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Important**: Choose ONE method only:
|
|
139
|
+
|
|
140
|
+
- ✅ **Method 1**: HTML script tags + assets configuration
|
|
141
|
+
- ✅ **Method 2**: TypeScript imports + SCSS imports
|
|
142
|
+
- ❌ **Don't use both**: This can cause duplicate loading and conflicts
|
|
143
|
+
|
|
144
|
+
### Helper Package (SSR Support)
|
|
145
|
+
|
|
146
|
+
For Server-Side Rendering (SSR) applications that need to preload initial data on the server:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm install @mapvx/web-components-ssr-helpers
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Via CDN (unpkg)
|
|
153
|
+
|
|
154
|
+
```html
|
|
155
|
+
<!-- Include the component files -->
|
|
156
|
+
<script src="https://unpkg.com/@mapvx/web-components-ssr@latest/dist/browser/main.js"></script>
|
|
157
|
+
<link
|
|
158
|
+
rel="stylesheet"
|
|
159
|
+
href="https://unpkg.com/@mapvx/web-components-ssr@latest/dist/browser/styles.css"
|
|
160
|
+
/>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## 🎯 Usage
|
|
164
|
+
|
|
165
|
+
### Basic Usage
|
|
166
|
+
|
|
167
|
+
```html
|
|
168
|
+
<!-- Basic implementation with required API key -->
|
|
169
|
+
<mapvx-website api-key="your-api-key-here"></mapvx-website>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Advanced Usage with Configuration
|
|
173
|
+
|
|
174
|
+
```html
|
|
175
|
+
<!-- With all available options -->
|
|
176
|
+
<mapvx-website
|
|
177
|
+
api-key="your-api-key-here"
|
|
178
|
+
institution-id="optional-institution-id"
|
|
179
|
+
initial-data='{"custom": "data"}'
|
|
180
|
+
default-to-map="true"
|
|
181
|
+
show-category-filters="false"
|
|
182
|
+
show-city-filter="true"
|
|
183
|
+
inherit-font-family="false"
|
|
184
|
+
>
|
|
185
|
+
</mapvx-website>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Framework Integration
|
|
189
|
+
|
|
190
|
+
#### React
|
|
191
|
+
|
|
192
|
+
```jsx
|
|
193
|
+
function App() {
|
|
194
|
+
return (
|
|
195
|
+
<mapvx-website
|
|
196
|
+
api-key="your-api-key-here"
|
|
197
|
+
default-to-map="true"
|
|
198
|
+
show-category-filters="false"
|
|
199
|
+
/>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### Vue
|
|
205
|
+
|
|
206
|
+
```vue
|
|
207
|
+
<template>
|
|
208
|
+
<mapvx-website
|
|
209
|
+
api-key="your-api-key-here"
|
|
210
|
+
:default-to-map="true"
|
|
211
|
+
:show-category-filters="false"
|
|
212
|
+
/>
|
|
213
|
+
</template>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### Angular
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// In your component
|
|
220
|
+
@Component({
|
|
221
|
+
template: `
|
|
222
|
+
<mapvx-website
|
|
223
|
+
[attr.api-key]="apiKey"
|
|
224
|
+
[attr.default-to-map]="defaultToMap"
|
|
225
|
+
[attr.show-category-filters]="showFilters"
|
|
226
|
+
/>
|
|
227
|
+
`,
|
|
228
|
+
})
|
|
229
|
+
export class MyComponent {
|
|
230
|
+
apiKey = 'your-api-key-here'
|
|
231
|
+
defaultToMap = true
|
|
232
|
+
showFilters = false
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## 📋 Input Properties
|
|
237
|
+
|
|
238
|
+
The web component accepts the following input properties to customize its behavior:
|
|
239
|
+
|
|
240
|
+
| Property | Type | Required | Default | Description |
|
|
241
|
+
| ----------------------- | --------- | ---------- | ------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
242
|
+
| `api-key` | `string` | ✅ **Yes** | - | Valid API key to initialize the SDK |
|
|
243
|
+
| `institution-id` | `string` | ✅ **Yes** | - | Institution ID for specific data filtering and search |
|
|
244
|
+
| `initial-data` | `string` | ❌ No | - | JSON string with initial data (use `prepareInitialData` from `@mapvx/web-components-ssr-helpers`) |
|
|
245
|
+
| `default-to-map` | `boolean` | ❌ No | `false` | If `true`, shows map view by default |
|
|
246
|
+
| `show-category-filters` | `boolean` | ❌ No | `true` | If `true`, shows category filters |
|
|
247
|
+
| `show-city-filter` | `boolean` | ❌ No | `true` | If `true`, shows city filter |
|
|
248
|
+
| `inherit-font-family` | `boolean` | ❌ No | `false` | If `true`, inherits font family from parent |
|
|
249
|
+
| `hide-deals` | `boolean` | ❌ No | `false` | If `true`, hides deals and promotional content |
|
|
250
|
+
| `show-deal-labels` | `boolean` | ❌ No | `false` | If `true`, shows deal labels over their images on the home screen |
|
|
251
|
+
| `card-width` | `number` | ❌ No | `237` (desktop), `170` (mobile) | Custom card width in pixels for grid view. Affects card size and number of cards per row |
|
|
252
|
+
| `rows-per-page` | `number` | ❌ No | `2` | Maximum number of rows per page (1-10). Shows fewer rows if not enough cards to fill all rows |
|
|
253
|
+
|
|
254
|
+
### Input Usage Examples
|
|
255
|
+
|
|
256
|
+
#### Required Properties Only
|
|
257
|
+
|
|
258
|
+
```html
|
|
259
|
+
<mapvx-website api-key="your-api-key-here" institution-id="institution-123"></mapvx-website>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### With Optional Configuration
|
|
263
|
+
|
|
264
|
+
```html
|
|
265
|
+
<mapvx-website
|
|
266
|
+
api-key="your-api-key-here"
|
|
267
|
+
institution-id="institution-123"
|
|
268
|
+
default-to-map="true"
|
|
269
|
+
show-category-filters="false"
|
|
270
|
+
show-city-filter="true"
|
|
271
|
+
hide-deals="true"
|
|
272
|
+
show-deal-labels="true"
|
|
273
|
+
card-width="300"
|
|
274
|
+
>
|
|
275
|
+
</mapvx-website>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### Custom Card Width
|
|
279
|
+
|
|
280
|
+
```html
|
|
281
|
+
<!-- Larger cards (300px) -->
|
|
282
|
+
<mapvx-website api-key="your-api-key-here" institution-id="institution-123" card-width="300">
|
|
283
|
+
</mapvx-website>
|
|
284
|
+
|
|
285
|
+
<!-- Smaller cards (200px) -->
|
|
286
|
+
<mapvx-website api-key="your-api-key-here" institution-id="institution-123" card-width="200">
|
|
287
|
+
</mapvx-website>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
#### Custom Rows Per Page
|
|
291
|
+
|
|
292
|
+
```html
|
|
293
|
+
<!-- Show 3 rows per page -->
|
|
294
|
+
<mapvx-website api-key="your-api-key-here" institution-id="institution-123" rows-per-page="3">
|
|
295
|
+
</mapvx-website>
|
|
296
|
+
|
|
297
|
+
<!-- Show 1 row per page (minimal) -->
|
|
298
|
+
<mapvx-website api-key="your-api-key-here" institution-id="institution-123" rows-per-page="1">
|
|
299
|
+
</mapvx-website>
|
|
300
|
+
|
|
301
|
+
<!-- Combine with custom card width -->
|
|
302
|
+
<mapvx-website
|
|
303
|
+
api-key="your-api-key-here"
|
|
304
|
+
institution-id="institution-123"
|
|
305
|
+
card-width="250"
|
|
306
|
+
rows-per-page="4"
|
|
307
|
+
>
|
|
308
|
+
</mapvx-website>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## 📤 Output Events
|
|
312
|
+
|
|
313
|
+
The web component emits custom events that you can listen to for user interactions:
|
|
314
|
+
|
|
315
|
+
| Event Name | Type | Description |
|
|
316
|
+
| -------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
317
|
+
| `cardSelected` | `string` | ⚠️ **Deprecated**: Use `userAction` with type `show-place` instead. Emitted when a location card is selected. Contains the card ID. |
|
|
318
|
+
| `userAction` | `UserAction` | Emitted when a user performs an action (filter selection, search, place navigation, etc.). Contains action type and associated data. |
|
|
319
|
+
|
|
320
|
+
### Output Usage Examples
|
|
321
|
+
|
|
322
|
+
#### Vanilla JavaScript
|
|
323
|
+
|
|
324
|
+
```javascript
|
|
325
|
+
// Wait for the web component to be registered
|
|
326
|
+
function setupUserActionListener() {
|
|
327
|
+
const element = document.querySelector('mapvx-website')
|
|
328
|
+
if (element) {
|
|
329
|
+
element.addEventListener('userAction', (event) => {
|
|
330
|
+
if (event instanceof CustomEvent && event.detail.type === 'show-place') {
|
|
331
|
+
const placeId = event.detail.data.placeId
|
|
332
|
+
console.log('Place shown:', placeId)
|
|
333
|
+
// Handle place display
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
} else {
|
|
337
|
+
// Retry if element is not yet available
|
|
338
|
+
setTimeout(setupUserActionListener, 100)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check if web component is already registered
|
|
343
|
+
if (customElements.get('mapvx-website')) {
|
|
344
|
+
setupUserActionListener()
|
|
345
|
+
} else {
|
|
346
|
+
// Wait for registration
|
|
347
|
+
const checkInterval = setInterval(() => {
|
|
348
|
+
if (customElements.get('mapvx-website')) {
|
|
349
|
+
setupUserActionListener()
|
|
350
|
+
clearInterval(checkInterval)
|
|
351
|
+
}
|
|
352
|
+
}, 100)
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
#### Update Browser URL with `history.pushState`
|
|
357
|
+
|
|
358
|
+
You can react to user actions to change the browser URL without a full page reload, which is especially useful in SSR apps looking to keep client-side navigation in sync with the selected place.
|
|
359
|
+
|
|
360
|
+
```html
|
|
361
|
+
<script>
|
|
362
|
+
// Wait for the web component to be registered
|
|
363
|
+
function setupUrlUpdater() {
|
|
364
|
+
const element = document.querySelector('mapvx-website')
|
|
365
|
+
if (!element) {
|
|
366
|
+
setTimeout(setupUrlUpdater, 100)
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const handleUserAction = (event) => {
|
|
371
|
+
if (event instanceof CustomEvent) {
|
|
372
|
+
const action = event.detail
|
|
373
|
+
switch (action.type) {
|
|
374
|
+
case 'return-to-home':
|
|
375
|
+
case 'select-filter': {
|
|
376
|
+
const { filter } = action.data
|
|
377
|
+
history.pushState(
|
|
378
|
+
{ page: 'home', filter },
|
|
379
|
+
'',
|
|
380
|
+
`${location.pathname}?tab=${encodeURIComponent(filter)}`,
|
|
381
|
+
)
|
|
382
|
+
break
|
|
383
|
+
}
|
|
384
|
+
case 'show-place': {
|
|
385
|
+
const { placeId, alias } = action.data
|
|
386
|
+
if (alias) {
|
|
387
|
+
history.pushState(
|
|
388
|
+
{ page: 'profile', id: alias },
|
|
389
|
+
'',
|
|
390
|
+
`${location.pathname}?tenant=${alias}`,
|
|
391
|
+
)
|
|
392
|
+
} else {
|
|
393
|
+
history.pushState(
|
|
394
|
+
{ page: 'profile', id: placeId },
|
|
395
|
+
'',
|
|
396
|
+
`${location.pathname}?tenant=${placeId}`,
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
break
|
|
400
|
+
}
|
|
401
|
+
case 'search': {
|
|
402
|
+
const { searchTerm } = action.data
|
|
403
|
+
history.pushState(
|
|
404
|
+
{ page: 'search', term: searchTerm },
|
|
405
|
+
'',
|
|
406
|
+
`${location.pathname}?search=${encodeURIComponent(searchTerm)}`,
|
|
407
|
+
)
|
|
408
|
+
break
|
|
409
|
+
}
|
|
410
|
+
case 'select-destination': {
|
|
411
|
+
const { destinationId } = action.data
|
|
412
|
+
history.pushState(
|
|
413
|
+
{ page: 'route', id: destinationId },
|
|
414
|
+
'',
|
|
415
|
+
`${location.pathname}?destination=${destinationId}`,
|
|
416
|
+
)
|
|
417
|
+
break
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
element.addEventListener('userAction', handleUserAction)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Check if web component is already registered
|
|
427
|
+
if (customElements.get('mapvx-website')) {
|
|
428
|
+
setupUrlUpdater()
|
|
429
|
+
} else {
|
|
430
|
+
// Wait for registration
|
|
431
|
+
const waitForRegistration = setInterval(() => {
|
|
432
|
+
if (customElements.get('mapvx-website')) {
|
|
433
|
+
clearInterval(waitForRegistration)
|
|
434
|
+
setupUrlUpdater()
|
|
435
|
+
}
|
|
436
|
+
}, 100)
|
|
437
|
+
}
|
|
438
|
+
</script>
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
#### React
|
|
442
|
+
|
|
443
|
+
```jsx
|
|
444
|
+
import { useEffect, useRef } from 'react'
|
|
445
|
+
|
|
446
|
+
function App() {
|
|
447
|
+
const webComponentRef = useRef(null)
|
|
448
|
+
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
const element = webComponentRef.current
|
|
451
|
+
if (!element) return
|
|
452
|
+
|
|
453
|
+
const handleUserAction = (event) => {
|
|
454
|
+
if (event instanceof CustomEvent && event.detail.type === 'show-place') {
|
|
455
|
+
const placeId = event.detail.data.placeId
|
|
456
|
+
console.log('Place shown:', placeId)
|
|
457
|
+
// Handle place display
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
element.addEventListener('userAction', handleUserAction)
|
|
462
|
+
|
|
463
|
+
return () => {
|
|
464
|
+
element.removeEventListener('userAction', handleUserAction)
|
|
465
|
+
}
|
|
466
|
+
}, [])
|
|
467
|
+
|
|
468
|
+
return <mapvx-website ref={webComponentRef} api-key="your-api-key-here" />
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
#### Vue
|
|
473
|
+
|
|
474
|
+
```vue
|
|
475
|
+
<template>
|
|
476
|
+
<mapvx-website ref="webComponent" api-key="your-api-key-here" />
|
|
477
|
+
</template>
|
|
478
|
+
|
|
479
|
+
<script setup>
|
|
480
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
481
|
+
|
|
482
|
+
const webComponent = ref(null)
|
|
483
|
+
|
|
484
|
+
const handleUserAction = (event) => {
|
|
485
|
+
if (event instanceof CustomEvent && event.detail.type === 'show-place') {
|
|
486
|
+
const placeId = event.detail.data.placeId
|
|
487
|
+
console.log('Place shown:', placeId)
|
|
488
|
+
// Handle place display
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
onMounted(() => {
|
|
493
|
+
if (webComponent.value) {
|
|
494
|
+
webComponent.value.addEventListener('userAction', handleUserAction)
|
|
495
|
+
}
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
onUnmounted(() => {
|
|
499
|
+
if (webComponent.value) {
|
|
500
|
+
webComponent.value.removeEventListener('userAction', handleUserAction)
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
</script>
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
#### Angular
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
import { Component, ElementRef, OnDestroy, ViewChild, AfterViewInit } from '@angular/core'
|
|
510
|
+
|
|
511
|
+
@Component({
|
|
512
|
+
selector: 'app-mapvx',
|
|
513
|
+
template: ` <mapvx-website #webComponent [attr.api-key]="apiKey" /> `,
|
|
514
|
+
})
|
|
515
|
+
export class MapvxComponent implements AfterViewInit, OnDestroy {
|
|
516
|
+
@ViewChild('webComponent', { static: false }) webComponentRef!: ElementRef<HTMLElement>
|
|
517
|
+
|
|
518
|
+
apiKey = 'your-api-key-here'
|
|
519
|
+
private userActionListener?: (event: Event) => void
|
|
520
|
+
|
|
521
|
+
ngAfterViewInit() {
|
|
522
|
+
this.setupUserActionListener()
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private setupUserActionListener() {
|
|
526
|
+
const element = this.webComponentRef?.nativeElement
|
|
527
|
+
if (!element) {
|
|
528
|
+
// Retry if element is not yet available
|
|
529
|
+
setTimeout(() => this.setupUserActionListener(), 100)
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
this.userActionListener = (event: Event) => {
|
|
534
|
+
if (event instanceof CustomEvent) {
|
|
535
|
+
const action = event.detail as { type: string; data: any }
|
|
536
|
+
if (action.type === 'show-place') {
|
|
537
|
+
const placeId = action.data.placeId
|
|
538
|
+
console.log('Place shown:', placeId)
|
|
539
|
+
// Handle place display
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
element.addEventListener('userAction', this.userActionListener)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
ngOnDestroy() {
|
|
548
|
+
const element = this.webComponentRef?.nativeElement
|
|
549
|
+
if (element && this.userActionListener) {
|
|
550
|
+
element.removeEventListener('userAction', this.userActionListener)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### User Action Event
|
|
557
|
+
|
|
558
|
+
The `userAction` event provides detailed information about user interactions within the component. The event detail contains an object with `type` and `data` properties.
|
|
559
|
+
|
|
560
|
+
#### UserAction Types
|
|
561
|
+
|
|
562
|
+
| Type | Description | Data Structure |
|
|
563
|
+
| -------------------- | ------------------------------------------ | --------------------------------------- | ------------ |
|
|
564
|
+
| `select-filter` | Emitted when a filter is selected | `{ filter: string }` |
|
|
565
|
+
| `show-place` | Emitted when a place is displayed | `{ placeId: string, alias: string | undefined }` |
|
|
566
|
+
| `select-destination` | Emitted when a destination is selected | `{ destinationId: string, alias: string | undefined }` |
|
|
567
|
+
| `search` | Emitted when a search is performed | `{ searchTerm: string }` |
|
|
568
|
+
| `return-to-home` | Emitted when user returns to the home view | `{ filter: string }` |
|
|
569
|
+
|
|
570
|
+
#### UserAction Usage Examples
|
|
571
|
+
|
|
572
|
+
#### Vanilla JavaScript
|
|
573
|
+
|
|
574
|
+
```javascript
|
|
575
|
+
// Wait for the web component to be registered
|
|
576
|
+
function setupUserActionListener() {
|
|
577
|
+
const element = document.querySelector('mapvx-website')
|
|
578
|
+
if (element) {
|
|
579
|
+
element.addEventListener('userAction', (event) => {
|
|
580
|
+
if (event instanceof CustomEvent) {
|
|
581
|
+
const action = event.detail
|
|
582
|
+
console.log('User action:', action.type, action.data)
|
|
583
|
+
|
|
584
|
+
// Handle different action types
|
|
585
|
+
switch (action.type) {
|
|
586
|
+
case 'select-filter':
|
|
587
|
+
console.log('Filter selected:', action.data.filter)
|
|
588
|
+
break
|
|
589
|
+
case 'show-place':
|
|
590
|
+
console.log('Place shown:', action.data.placeId)
|
|
591
|
+
break
|
|
592
|
+
case 'select-destination':
|
|
593
|
+
console.log('Destination selected:', action.data.destinationId)
|
|
594
|
+
break
|
|
595
|
+
case 'search':
|
|
596
|
+
console.log('Search performed:', action.data.searchTerm)
|
|
597
|
+
break
|
|
598
|
+
case 'return-to-home':
|
|
599
|
+
console.log('Returned to home with filter:', action.data.filter)
|
|
600
|
+
break
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
} else {
|
|
605
|
+
// Retry if element is not yet available
|
|
606
|
+
setTimeout(setupUserActionListener, 100)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Check if web component is already registered
|
|
611
|
+
if (customElements.get('mapvx-website')) {
|
|
612
|
+
setupUserActionListener()
|
|
613
|
+
} else {
|
|
614
|
+
// Wait for registration
|
|
615
|
+
const checkInterval = setInterval(() => {
|
|
616
|
+
if (customElements.get('mapvx-website')) {
|
|
617
|
+
setupUserActionListener()
|
|
618
|
+
clearInterval(checkInterval)
|
|
619
|
+
}
|
|
620
|
+
}, 100)
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
#### React
|
|
625
|
+
|
|
626
|
+
```jsx
|
|
627
|
+
import { useEffect, useRef } from 'react'
|
|
628
|
+
|
|
629
|
+
function App() {
|
|
630
|
+
const webComponentRef = useRef(null)
|
|
631
|
+
|
|
632
|
+
useEffect(() => {
|
|
633
|
+
const element = webComponentRef.current
|
|
634
|
+
if (!element) return
|
|
635
|
+
|
|
636
|
+
const handleUserAction = (event) => {
|
|
637
|
+
if (event instanceof CustomEvent) {
|
|
638
|
+
const action = event.detail
|
|
639
|
+
console.log('User action:', action.type, action.data)
|
|
640
|
+
|
|
641
|
+
// Handle different action types
|
|
642
|
+
switch (action.type) {
|
|
643
|
+
case 'select-filter':
|
|
644
|
+
// Handle filter selection
|
|
645
|
+
break
|
|
646
|
+
case 'show-place':
|
|
647
|
+
// Handle place display
|
|
648
|
+
break
|
|
649
|
+
case 'search':
|
|
650
|
+
// Handle search
|
|
651
|
+
break
|
|
652
|
+
// ... other cases
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
element.addEventListener('userAction', handleUserAction)
|
|
658
|
+
|
|
659
|
+
return () => {
|
|
660
|
+
element.removeEventListener('userAction', handleUserAction)
|
|
661
|
+
}
|
|
662
|
+
}, [])
|
|
663
|
+
|
|
664
|
+
return <mapvx-website ref={webComponentRef} api-key="your-api-key-here" />
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
#### Vue
|
|
669
|
+
|
|
670
|
+
```vue
|
|
671
|
+
<template>
|
|
672
|
+
<mapvx-website ref="webComponent" api-key="your-api-key-here" />
|
|
673
|
+
</template>
|
|
674
|
+
|
|
675
|
+
<script setup>
|
|
676
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
677
|
+
|
|
678
|
+
const webComponent = ref(null)
|
|
679
|
+
|
|
680
|
+
const handleUserAction = (event) => {
|
|
681
|
+
if (event instanceof CustomEvent) {
|
|
682
|
+
const action = event.detail
|
|
683
|
+
console.log('User action:', action.type, action.data)
|
|
684
|
+
|
|
685
|
+
// Handle different action types
|
|
686
|
+
switch (action.type) {
|
|
687
|
+
case 'select-filter':
|
|
688
|
+
// Handle filter selection
|
|
689
|
+
break
|
|
690
|
+
case 'show-place':
|
|
691
|
+
// Handle place display
|
|
692
|
+
break
|
|
693
|
+
case 'search':
|
|
694
|
+
// Handle search
|
|
695
|
+
break
|
|
696
|
+
// ... other cases
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
onMounted(() => {
|
|
702
|
+
if (webComponent.value) {
|
|
703
|
+
webComponent.value.addEventListener('userAction', handleUserAction)
|
|
704
|
+
}
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
onUnmounted(() => {
|
|
708
|
+
if (webComponent.value) {
|
|
709
|
+
webComponent.value.removeEventListener('userAction', handleUserAction)
|
|
710
|
+
}
|
|
711
|
+
})
|
|
712
|
+
</script>
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
#### Angular
|
|
716
|
+
|
|
717
|
+
```typescript
|
|
718
|
+
import { Component, ElementRef, OnDestroy, ViewChild, AfterViewInit } from '@angular/core'
|
|
719
|
+
|
|
720
|
+
@Component({
|
|
721
|
+
selector: 'app-mapvx',
|
|
722
|
+
template: ` <mapvx-website #webComponent [attr.api-key]="apiKey" /> `,
|
|
723
|
+
})
|
|
724
|
+
export class MapvxComponent implements AfterViewInit, OnDestroy {
|
|
725
|
+
@ViewChild('webComponent', { static: false }) webComponentRef!: ElementRef<HTMLElement>
|
|
726
|
+
|
|
727
|
+
apiKey = 'your-api-key-here'
|
|
728
|
+
private userActionListener?: (event: Event) => void
|
|
729
|
+
|
|
730
|
+
ngAfterViewInit() {
|
|
731
|
+
this.setupUserActionListener()
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private setupUserActionListener() {
|
|
735
|
+
const element = this.webComponentRef?.nativeElement
|
|
736
|
+
if (!element) {
|
|
737
|
+
// Retry if element is not yet available
|
|
738
|
+
setTimeout(() => this.setupUserActionListener(), 100)
|
|
739
|
+
return
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
this.userActionListener = (event: Event) => {
|
|
743
|
+
if (event instanceof CustomEvent) {
|
|
744
|
+
const action = event.detail as { type: string; data: any }
|
|
745
|
+
console.log('User action:', action.type, action.data)
|
|
746
|
+
|
|
747
|
+
// Handle different action types
|
|
748
|
+
switch (action.type) {
|
|
749
|
+
case 'select-filter':
|
|
750
|
+
// Handle filter selection
|
|
751
|
+
break
|
|
752
|
+
case 'show-place':
|
|
753
|
+
// Handle place display
|
|
754
|
+
break
|
|
755
|
+
case 'search':
|
|
756
|
+
// Handle search
|
|
757
|
+
break
|
|
758
|
+
// ... other cases
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
element.addEventListener('userAction', this.userActionListener)
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
ngOnDestroy() {
|
|
767
|
+
const element = this.webComponentRef?.nativeElement
|
|
768
|
+
if (element && this.userActionListener) {
|
|
769
|
+
element.removeEventListener('userAction', this.userActionListener)
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
## 🔧 Server-Side Rendering (SSR) Support
|
|
776
|
+
|
|
777
|
+
For applications using Server-Side Rendering (SSR) where you need to preload initial data on the server before sending it to the browser:
|
|
778
|
+
|
|
779
|
+
### Installation
|
|
780
|
+
|
|
781
|
+
```bash
|
|
782
|
+
npm install @mapvx/web-components-ssr-helpers
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### Server-Side Usage
|
|
786
|
+
|
|
787
|
+
#### Next.js (App Router)
|
|
788
|
+
|
|
789
|
+
**1. Install dependencies:**
|
|
790
|
+
|
|
791
|
+
```bash
|
|
792
|
+
npm install @mapvx/web-components-ssr @mapvx/web-components-ssr-helpers
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
**2. Copy the bundle script to your `package.json`:**
|
|
796
|
+
|
|
797
|
+
```json
|
|
798
|
+
{
|
|
799
|
+
"scripts": {
|
|
800
|
+
"copy-bundle": "node copy-bundle.mjs"
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
**3. Create `copy-bundle.mjs` in your project root:**
|
|
806
|
+
|
|
807
|
+
```javascript
|
|
808
|
+
#!/usr/bin/env node
|
|
809
|
+
|
|
810
|
+
import { existsSync, mkdirSync, copyFileSync } from 'fs'
|
|
811
|
+
import { join } from 'path'
|
|
812
|
+
|
|
813
|
+
const sourcePath = 'node_modules/@mapvx/web-components-ssr/dist/browser/main.js'
|
|
814
|
+
const targetDir = 'public/mapvx-website'
|
|
815
|
+
const targetPath = join(targetDir, 'bundle.js')
|
|
816
|
+
|
|
817
|
+
if (!existsSync(sourcePath)) {
|
|
818
|
+
console.error(`❌ Source file not found: ${sourcePath}`)
|
|
819
|
+
process.exit(1)
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (!existsSync(targetDir)) {
|
|
823
|
+
mkdirSync(targetDir, { recursive: true })
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
copyFileSync(sourcePath, targetPath)
|
|
827
|
+
console.log(`✅ Bundle copied successfully!`)
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
**4. Create your page component:**
|
|
831
|
+
|
|
832
|
+
```typescript
|
|
833
|
+
// app/page.tsx
|
|
834
|
+
import { prepareInitialData } from '@mapvx/web-components-ssr-helpers';
|
|
835
|
+
import '@mapvx/web-components-ssr/dist/browser/styles.css';
|
|
836
|
+
import Script from 'next/script';
|
|
837
|
+
|
|
838
|
+
export default async function HomePage() {
|
|
839
|
+
const initialData = await prepareInitialData(process.env.NEXT_PUBLIC_MAPVX_API_KEY || '');
|
|
840
|
+
|
|
841
|
+
return (
|
|
842
|
+
<div>
|
|
843
|
+
<Script src='/mapvx-website/bundle.js' />
|
|
844
|
+
<mapvx-website
|
|
845
|
+
api-key={process.env.NEXT_PUBLIC_MAPVX_API_KEY}
|
|
846
|
+
institution-id={process.env.NEXT_PUBLIC_MAPVX_INSTITUTION_ID}
|
|
847
|
+
initial-data={initialData}
|
|
848
|
+
default-to-map="true"
|
|
849
|
+
/>
|
|
850
|
+
</div>
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
**5. Set environment variables in `.env.local`:**
|
|
856
|
+
|
|
857
|
+
```bash
|
|
858
|
+
NEXT_PUBLIC_MAPVX_API_KEY=your-api-key-here
|
|
859
|
+
NEXT_PUBLIC_MAPVX_INSTITUTION_ID=your-institution-id
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
**6. Run the copy script before building:**
|
|
863
|
+
|
|
864
|
+
```bash
|
|
865
|
+
npm run copy-bundle
|
|
866
|
+
npm run build
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
#### Nuxt.js
|
|
870
|
+
|
|
871
|
+
```vue
|
|
872
|
+
<!-- pages/index.vue -->
|
|
873
|
+
<template>
|
|
874
|
+
<div>
|
|
875
|
+
<h1>My App</h1>
|
|
876
|
+
<mapvx-website :api-key="apiKey" :initial-data="initialData" default-to-map="true" />
|
|
877
|
+
</div>
|
|
878
|
+
</template>
|
|
879
|
+
|
|
880
|
+
<script setup>
|
|
881
|
+
import { prepareInitialData } from '@mapvx/web-components-ssr-helpers'
|
|
882
|
+
|
|
883
|
+
// Server-side data fetching
|
|
884
|
+
const { data: initialData } = await useAsyncData('mapvx-data', async () => {
|
|
885
|
+
return await prepareInitialData(process.env.MAPVX_API_KEY)
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
const apiKey = process.env.MAPVX_API_KEY
|
|
889
|
+
</script>
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
#### SvelteKit
|
|
893
|
+
|
|
894
|
+
```typescript
|
|
895
|
+
// src/routes/+page.server.ts
|
|
896
|
+
import { prepareInitialData } from '@mapvx/web-components-ssr-helpers'
|
|
897
|
+
import type { PageServerLoad } from './$types'
|
|
898
|
+
|
|
899
|
+
export const load: PageServerLoad = async () => {
|
|
900
|
+
// Execute on the server
|
|
901
|
+
const initialData = await prepareInitialData(process.env.MAPVX_API_KEY!)
|
|
902
|
+
|
|
903
|
+
return {
|
|
904
|
+
initialData,
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
```svelte
|
|
910
|
+
<!-- src/routes/+page.svelte -->
|
|
911
|
+
<script lang="ts">
|
|
912
|
+
import type { PageData } from './$types';
|
|
913
|
+
|
|
914
|
+
export let data: PageData;
|
|
915
|
+
</script>
|
|
916
|
+
|
|
917
|
+
<div>
|
|
918
|
+
<h1>My App</h1>
|
|
919
|
+
<mapvx-website
|
|
920
|
+
api-key={process.env.MAPVX_API_KEY}
|
|
921
|
+
initial-data={data.initialData}
|
|
922
|
+
default-to-map="true"
|
|
923
|
+
/>
|
|
924
|
+
</div>
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
### What `prepareInitialData` does
|
|
928
|
+
|
|
929
|
+
The `prepareInitialData` function is specifically designed for SSR applications:
|
|
930
|
+
|
|
931
|
+
- **Server-side execution**: Must be called on the server, not in the browser
|
|
932
|
+
- **Data fetching**: Fetches available places and institutions from the API
|
|
933
|
+
- **Performance optimization**: Preloads data on the server to avoid client-side API calls
|
|
934
|
+
- **SEO benefits**: Ensures data is available during server-side rendering
|
|
935
|
+
- **Returns**: JSON string with combined places and institutions data
|
|
936
|
+
|
|
937
|
+
### Important Notes
|
|
938
|
+
|
|
939
|
+
⚠️ **SSR Only**: This function should only be used in server-side rendering contexts. For client-side applications, the web component will fetch data automatically when needed.
|
|
940
|
+
|
|
941
|
+
## 🎨 Customization
|
|
942
|
+
|
|
943
|
+
### Custom Colors
|
|
944
|
+
|
|
945
|
+
The web component supports custom color theming through CSS custom properties. You can override the default colors by defining CSS variables in your application's root:
|
|
946
|
+
|
|
947
|
+
```css
|
|
948
|
+
:root {
|
|
949
|
+
--mapvx-secondary-orange: #b41500;
|
|
950
|
+
--mapvx-secondary-blue: #053a96;
|
|
951
|
+
}
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
#### Example Implementation
|
|
955
|
+
|
|
956
|
+
**HTML with inline styles:**
|
|
957
|
+
|
|
958
|
+
```html
|
|
959
|
+
<style>
|
|
960
|
+
:root {
|
|
961
|
+
--mapvx-secondary-orange: #ff6b35;
|
|
962
|
+
--mapvx-secondary-blue: #1e3a8a;
|
|
963
|
+
}
|
|
964
|
+
</style>
|
|
965
|
+
|
|
966
|
+
<mapvx-website api-key="your-api-key-here"></mapvx-website>
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
**CSS file:**
|
|
970
|
+
|
|
971
|
+
```css
|
|
972
|
+
/* styles.css */
|
|
973
|
+
:root {
|
|
974
|
+
--mapvx-secondary-orange: #e74c3c;
|
|
975
|
+
--mapvx-secondary-blue: #3498db;
|
|
976
|
+
}
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
**Framework-specific examples:**
|
|
980
|
+
|
|
981
|
+
**React:**
|
|
982
|
+
|
|
983
|
+
```jsx
|
|
984
|
+
// In your CSS file or styled-components
|
|
985
|
+
:root {
|
|
986
|
+
--mapvx-secondary-orange: #ff6b35;
|
|
987
|
+
--mapvx-secondary-blue: #1e3a8a;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function App() {
|
|
991
|
+
return <mapvx-website api-key="your-api-key-here" />;
|
|
992
|
+
}
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
**Vue:**
|
|
996
|
+
|
|
997
|
+
```vue
|
|
998
|
+
<template>
|
|
999
|
+
<mapvx-website api-key="your-api-key-here" />
|
|
1000
|
+
</template>
|
|
1001
|
+
|
|
1002
|
+
<style>
|
|
1003
|
+
:root {
|
|
1004
|
+
--mapvx-secondary-orange: #ff6b35;
|
|
1005
|
+
--mapvx-secondary-blue: #1e3a8a;
|
|
1006
|
+
}
|
|
1007
|
+
</style>
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
**Angular:**
|
|
1011
|
+
|
|
1012
|
+
```scss
|
|
1013
|
+
// styles.scss
|
|
1014
|
+
:root {
|
|
1015
|
+
--mapvx-secondary-orange: #ff6b35;
|
|
1016
|
+
--mapvx-secondary-blue: #1e3a8a;
|
|
1017
|
+
}
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
### Available Color Variables
|
|
1021
|
+
|
|
1022
|
+
| Variable | Default Value | Description |
|
|
1023
|
+
| -------------------------- | ------------- | ------------------------------------------ |
|
|
1024
|
+
| `--mapvx-secondary-orange` | `#EE5845` | Secondary orange color used in UI elements |
|
|
1025
|
+
| `--mapvx-secondary-blue` | `#2C57A0` | Secondary blue color used in UI elements |
|
|
1026
|
+
|
|
1027
|
+
### Notes
|
|
1028
|
+
|
|
1029
|
+
- Colors are applied globally when defined in `:root`
|
|
1030
|
+
- The web component will automatically pick up these custom colors
|
|
1031
|
+
- Use valid CSS color values (hex, rgb, hsl, etc.)
|
|
1032
|
+
|
|
1033
|
+
## 🔧 Troubleshooting
|
|
1034
|
+
|
|
1035
|
+
### Common Issues
|
|
1036
|
+
|
|
1037
|
+
#### `SyntaxError: Unexpected token '<'` in Angular
|
|
1038
|
+
|
|
1039
|
+
**Problem**: Angular returns HTML instead of JavaScript files from `node_modules`.
|
|
1040
|
+
|
|
1041
|
+
**Solution**: Configure assets in `angular.json` as shown in the Angular Configuration section above.
|
|
1042
|
+
|
|
1043
|
+
#### Slow Loading (>10 seconds)
|
|
1044
|
+
|
|
1045
|
+
**Problem**: Large bundle size or incorrect asset serving.
|
|
1046
|
+
|
|
1047
|
+
**Solutions**:
|
|
1048
|
+
|
|
1049
|
+
- Use the recommended Angular assets configuration
|
|
1050
|
+
- Ensure files are served as static assets, not through routing
|
|
1051
|
+
- Check network tab in DevTools for 404 errors
|
|
1052
|
+
|
|
1053
|
+
#### Web Component Not Loading
|
|
1054
|
+
|
|
1055
|
+
**Problem**: Scripts not loading or component not registering.
|
|
1056
|
+
|
|
1057
|
+
**Solutions**:
|
|
1058
|
+
|
|
1059
|
+
- Verify file paths are correct
|
|
1060
|
+
- Check browser console for errors
|
|
1061
|
+
- Ensure `defer` attribute is used for script tags
|
|
1062
|
+
|
|
1063
|
+
## 🏠 Homepage
|
|
1064
|
+
|
|
1065
|
+
Visit [MapVX](https://mapvx.com) for more information.
|