A modern, framework-agnostic web component built with Angular 20 that provides an interactive map and location discovery interface.
A modern, framework-agnostic web component built with Angular 20 that provides an interactive map and location discovery interface.
- ā
Compatible with any framework (React, Vue, Vanilla JS, etc.)
- ā
Self-contained and isolated (Shadow DOM)
- ā
Optimized bundle without hashing for production
- ā
Angular 20 with modern architecture (standalone components)
- š Requires API Key: The component needs a valid API key to function
``bash`
npm install @mapvx/website-component
After installation, you can reference the files in your application:
JavaScript/TypeScript:
`javascript`
// Import the main component file
import '@mapvx/website-component/dist/browser/main.js'
// Import the styles
import '@mapvx/website-component/dist/browser/styles.css'
HTML:
`html`
Webpack/Vite/Bundler:
`javascript`
// In your main entry file
import '@mapvx/website-component/dist/browser/main.js'
import '@mapvx/website-component/dist/browser/styles.css'
Framework-specific examples:
React:
`jsx
// In your main App.jsx or index.jsx
import '@mapvx/website-component/dist/browser/main.js'
function App() {
return
}
`
`css`
/ In your main CSS file (e.g., index.css, App.css) /
@import '@mapvx/website-component/dist/browser/styles.css';
Vue:
`javascript`
// In your main.js
import '@mapvx/website-component/dist/browser/main.js'
`css`
/ In your main CSS file (e.g., style.css, main.css) /
@import '@mapvx/website-component/dist/browser/styles.css';
Angular:
#### Method 1: HTML Script Tags
Configure assets in angular.json and use script tags in index.html:
`json`
{
"assets": [
{
"glob": "*/",
"input": "public"
},
{
"glob": "*/",
"input": "src/assets",
"output": "assets"
},
{
"glob": "*/",
"input": "node_modules/@mapvx/website-component/dist/browser",
"output": "website-component"
}
]
}
Then reference the files in your index.html:
`html
`
Why this configuration is needed:
- Angular doesn't serve files from node_modules by default in developmentSyntaxError: Unexpected token '<'
- Without proper asset configuration, you'll get errors
- This ensures files are served as static assets with proper caching headers
#### Method 2: TypeScript Imports
Alternatively, you can import the files in your application:
`typescript`
// In your main.ts - Import the JavaScript
import '@mapvx/website-component/dist/browser/main.js'
`scss
/ In your styles.scss - Import the styles (Modern Sass) /
@use '@mapvx/website-component/dist/browser/styles.css';
/ Alternative for older Sass versions or CSS files /
@import '@mapvx/website-component/dist/browser/styles.css';
`
Important: Choose ONE method only:
- ā
Method 1: HTML script tags + assets configuration
- ā
Method 2: TypeScript imports + SCSS imports
- ā Don't use both: This can cause duplicate loading and conflicts
For Server-Side Rendering (SSR) applications that need to preload initial data on the server:
`bash`
npm install @mapvx/website-component-helpers
`html`
rel="stylesheet"
href="https://unpkg.com/@mapvx/website-component@latest/dist/browser/styles.css"
/>
`html`
`html`
institution-id="optional-institution-id"
initial-data='{"custom": "data"}'
default-to-map="true"
show-category-filters="false"
show-city-filter="true"
inherit-font-family="false"
>
#### React
`jsx`
function App() {
return (
default-to-map="true"
show-category-filters="false"
/>
)
}
#### Vue
`vue`
:default-to-map="true"
:show-category-filters="false"
/>
#### Angular
`typescript
// In your component
@Component({
template:
[attr.default-to-map]="defaultToMap"
[attr.show-category-filters]="showFilters"
/>
,`
})
export class MyComponent {
apiKey = 'your-api-key-here'
defaultToMap = true
showFilters = false
}
The web component accepts the following input properties to customize its behavior:
| Property | Type | Required | Default | Description |
| ----------------------- | --------- | ---------- | ------------------------------- | ------------------------------------------------------------------------------------------------ |
| api-key | string | ā
Yes | - | Valid API key to initialize the SDK |institution-id
| | string | ā
Yes | - | Institution ID for specific data filtering and search |initial-data
| | string | ā No | - | JSON string with initial data (use prepareInitialData from @mapvx/website-component-helpers) |default-to-map
| | boolean | ā No | false | If true, shows map view by default |show-category-filters
| | boolean | ā No | true | If true, shows category filters |show-city-filter
| | boolean | ā No | true | If true, shows city filter |inherit-font-family
| | boolean | ā No | false | If true, inherits font family from parent |hide-deals
| | boolean | ā No | false | If true, hides deals and promotional content |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 |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 |
#### Required Properties Only
`html`
#### With Optional Configuration
`html`
institution-id="institution-123"
default-to-map="true"
show-category-filters="false"
show-city-filter="true"
hide-deals="true"
card-width="300"
>
#### Custom Card Width
`html
`
#### Custom Rows Per Page
`html
institution-id="institution-123"
card-width="250"
rows-per-page="4"
>
`
The web component emits custom events that you can listen to for user interactions:
| Event Name | Type | Description |
| -------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| cardSelected | string | ā ļø Deprecated: Use userAction with type show-place instead. Emitted when a location card is selected. Contains the card ID. |userAction
| | UserAction | Emitted when a user performs an action (filter selection, search, place navigation, etc.). Contains action type and associated data. |
#### Vanilla JavaScript
`javascript
// Wait for the web component to be registered
function setupUserActionListener() {
const element = document.querySelector('mapvx-website')
if (element) {
element.addEventListener('userAction', (event) => {
if (event instanceof CustomEvent && event.detail.type === 'show-place') {
const placeId = event.detail.data.placeId
console.log('Place shown:', placeId)
// Handle place display
}
})
} else {
// Retry if element is not yet available
setTimeout(setupUserActionListener, 100)
}
}
// Check if web component is already registered
if (customElements.get('mapvx-website')) {
setupUserActionListener()
} else {
// Wait for registration
const checkInterval = setInterval(() => {
if (customElements.get('mapvx-website')) {
setupUserActionListener()
clearInterval(checkInterval)
}
}, 100)
}
`
#### Update Browser URL with history.pushState
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.
`html`
#### React
`jsx
import { useEffect, useRef } from 'react'
function App() {
const webComponentRef = useRef(null)
useEffect(() => {
const element = webComponentRef.current
if (!element) return
const handleUserAction = (event) => {
if (event instanceof CustomEvent && event.detail.type === 'show-place') {
const placeId = event.detail.data.placeId
console.log('Place shown:', placeId)
// Handle place display
}
}
element.addEventListener('userAction', handleUserAction)
return () => {
element.removeEventListener('userAction', handleUserAction)
}
}, [])
return
}
`
#### Vue
`vue
`
#### Angular
`typescript
import { Component, ElementRef, OnDestroy, ViewChild, AfterViewInit } from '@angular/core'
@Component({
selector: 'app-mapvx',
template: ,
})
export class MapvxComponent implements AfterViewInit, OnDestroy {
@ViewChild('webComponent', { static: false }) webComponentRef!: ElementRef
apiKey = 'your-api-key-here'
private userActionListener?: (event: Event) => void
ngAfterViewInit() {
this.setupUserActionListener()
}
private setupUserActionListener() {
const element = this.webComponentRef?.nativeElement
if (!element) {
// Retry if element is not yet available
setTimeout(() => this.setupUserActionListener(), 100)
return
}
this.userActionListener = (event: Event) => {
if (event instanceof CustomEvent) {
const action = event.detail as { type: string; data: any }
if (action.type === 'show-place') {
const placeId = action.data.placeId
console.log('Place shown:', placeId)
// Handle place display
}
}
}
element.addEventListener('userAction', this.userActionListener)
}
ngOnDestroy() {
const element = this.webComponentRef?.nativeElement
if (element && this.userActionListener) {
element.removeEventListener('userAction', this.userActionListener)
}
}
}
`
The userAction event provides detailed information about user interactions within the component. The event detail contains an object with type and data properties.
#### UserAction Types
| Type | Description | Data Structure |
| -------------------- | ------------------------------------------ | --------------------------------------- | ------------ |
| select-filter | Emitted when a filter is selected | { filter: string } |show-place
| | Emitted when a place is displayed | { placeId: string, alias: string | undefined } |select-destination
| | Emitted when a destination is selected | { destinationId: string, alias: string | undefined } |search
| | Emitted when a search is performed | { searchTerm: string } |return-to-home
| | Emitted when user returns to the home view | { filter: string } |
#### UserAction Usage Examples
#### Vanilla JavaScript
`javascript
// Wait for the web component to be registered
function setupUserActionListener() {
const element = document.querySelector('mapvx-website')
if (element) {
element.addEventListener('userAction', (event) => {
if (event instanceof CustomEvent) {
const action = event.detail
console.log('User action:', action.type, action.data)
// Handle different action types
switch (action.type) {
case 'select-filter':
console.log('Filter selected:', action.data.filter)
break
case 'show-place':
console.log('Place shown:', action.data.placeId)
break
case 'select-destination':
console.log('Destination selected:', action.data.destinationId)
break
case 'search':
console.log('Search performed:', action.data.searchTerm)
break
case 'return-to-home':
console.log('Returned to home with filter:', action.data.filter)
break
}
}
})
} else {
// Retry if element is not yet available
setTimeout(setupUserActionListener, 100)
}
}
// Check if web component is already registered
if (customElements.get('mapvx-website')) {
setupUserActionListener()
} else {
// Wait for registration
const checkInterval = setInterval(() => {
if (customElements.get('mapvx-website')) {
setupUserActionListener()
clearInterval(checkInterval)
}
}, 100)
}
`
#### React
`jsx
import { useEffect, useRef } from 'react'
function App() {
const webComponentRef = useRef(null)
useEffect(() => {
const element = webComponentRef.current
if (!element) return
const handleUserAction = (event) => {
if (event instanceof CustomEvent) {
const action = event.detail
console.log('User action:', action.type, action.data)
// Handle different action types
switch (action.type) {
case 'select-filter':
// Handle filter selection
break
case 'show-place':
// Handle place display
break
case 'search':
// Handle search
break
// ... other cases
}
}
}
element.addEventListener('userAction', handleUserAction)
return () => {
element.removeEventListener('userAction', handleUserAction)
}
}, [])
return
}
`
#### Vue
`vue
`
#### Angular
`typescript
import { Component, ElementRef, OnDestroy, ViewChild, AfterViewInit } from '@angular/core'
@Component({
selector: 'app-mapvx',
template: ,
})
export class MapvxComponent implements AfterViewInit, OnDestroy {
@ViewChild('webComponent', { static: false }) webComponentRef!: ElementRef
apiKey = 'your-api-key-here'
private userActionListener?: (event: Event) => void
ngAfterViewInit() {
this.setupUserActionListener()
}
private setupUserActionListener() {
const element = this.webComponentRef?.nativeElement
if (!element) {
// Retry if element is not yet available
setTimeout(() => this.setupUserActionListener(), 100)
return
}
this.userActionListener = (event: Event) => {
if (event instanceof CustomEvent) {
const action = event.detail as { type: string; data: any }
console.log('User action:', action.type, action.data)
// Handle different action types
switch (action.type) {
case 'select-filter':
// Handle filter selection
break
case 'show-place':
// Handle place display
break
case 'search':
// Handle search
break
// ... other cases
}
}
}
element.addEventListener('userAction', this.userActionListener)
}
ngOnDestroy() {
const element = this.webComponentRef?.nativeElement
if (element && this.userActionListener) {
element.removeEventListener('userAction', this.userActionListener)
}
}
}
`
For applications using Server-Side Rendering (SSR) where you need to preload initial data on the server before sending it to the browser:
`bash`
npm install @mapvx/website-component-helpers
#### Next.js (App Router)
1. Install dependencies:
`bash`
npm install @mapvx/website-component @mapvx/website-component-helpers
2. Copy the bundle script to your package.json:
`json`
{
"scripts": {
"copy-bundle": "node copy-bundle.mjs"
}
}
3. Create copy-bundle.mjs in your project root:
`javascript
#!/usr/bin/env node
import { existsSync, mkdirSync, copyFileSync } from 'fs'
import { join } from 'path'
const sourcePath = 'node_modules/@mapvx/website-component/dist/browser/main.js'
const targetDir = 'public/mapvx-website'
const targetPath = join(targetDir, 'bundle.js')
if (!existsSync(sourcePath)) {
console.error(ā Source file not found: ${sourcePath})
process.exit(1)
}
if (!existsSync(targetDir)) {
mkdirSync(targetDir, { recursive: true })
}
copyFileSync(sourcePath, targetPath)
console.log(ā
Bundle copied successfully!)`
4. Create your page component:
`typescript
// app/page.tsx
import { prepareInitialData } from '@mapvx/website-component-helpers';
import '@mapvx/website-component/dist/browser/styles.css';
import Script from 'next/script';
export default async function HomePage() {
const initialData = await prepareInitialData(process.env.NEXT_PUBLIC_MAPVX_API_KEY || '');
return (
5. Set environment variables in
.env.local:`bash
NEXT_PUBLIC_MAPVX_API_KEY=your-api-key-here
NEXT_PUBLIC_MAPVX_INSTITUTION_ID=your-institution-id
`6. Run the copy script before building:
`bash
npm run copy-bundle
npm run build
`#### Nuxt.js
`vue
My App
`#### SvelteKit
`typescript
// src/routes/+page.server.ts
import { prepareInitialData } from '@mapvx/website-component-helpers'
import type { PageServerLoad } from './$types'export const load: PageServerLoad = async () => {
// Execute on the server
const initialData = await prepareInitialData(process.env.MAPVX_API_KEY!)
return {
initialData,
}
}
``svelte
My App
api-key={process.env.MAPVX_API_KEY}
initial-data={data.initialData}
default-to-map="true"
/>
`$3
The
prepareInitialData function is specifically designed for SSR applications:- Server-side execution: Must be called on the server, not in the browser
- Data fetching: Fetches available places and institutions from the API
- Performance optimization: Preloads data on the server to avoid client-side API calls
- SEO benefits: Ensures data is available during server-side rendering
- Returns: JSON string with combined places and institutions data
$3
ā ļø 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.
šØ Customization
$3
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:
`css
:root {
--mapvx-secondary-orange: #b41500;
--mapvx-secondary-blue: #053a96;
}
`#### Example Implementation
HTML with inline styles:
`html
`CSS file:
`css
/ styles.css /
:root {
--mapvx-secondary-orange: #e74c3c;
--mapvx-secondary-blue: #3498db;
}
`Framework-specific examples:
React:
`jsx
// In your CSS file or styled-components
:root {
--mapvx-secondary-orange: #ff6b35;
--mapvx-secondary-blue: #1e3a8a;
}function App() {
return ;
}
`Vue:
`vue
`Angular:
`scss
// styles.scss
:root {
--mapvx-secondary-orange: #ff6b35;
--mapvx-secondary-blue: #1e3a8a;
}
`$3
| Variable | Default Value | Description |
| -------------------------- | ------------- | ------------------------------------------ |
|
--mapvx-secondary-orange | #EE5845 | Secondary orange color used in UI elements |
| --mapvx-secondary-blue | #2C57A0 | Secondary blue color used in UI elements |$3
- Colors are applied globally when defined in
:root
- The web component will automatically pick up these custom colors
- Use valid CSS color values (hex, rgb, hsl, etc.)š§ Troubleshooting
$3
####
SyntaxError: Unexpected token '<' in AngularProblem: Angular returns HTML instead of JavaScript files from
node_modules.Solution: Configure assets in
angular.json as shown in the Angular Configuration section above.#### Slow Loading (>10 seconds)
Problem: Large bundle size or incorrect asset serving.
Solutions:
- Use the recommended Angular assets configuration
- Ensure files are served as static assets, not through routing
- Check network tab in DevTools for 404 errors
#### Web Component Not Loading
Problem: Scripts not loading or component not registering.
Solutions:
- Verify file paths are correct
- Check browser console for errors
- Ensure
defer` attribute is used for script tagsVisit MapVX for more information.