@mapvx/website-component
v0.11.1
Published
A modern, framework-agnostic web component built with Angular 20 that provides an interactive map and location discovery interface.
Readme
MapVX Website Component
A modern, framework-agnostic web component built with Angular 20 that provides an interactive map and location discovery interface.
🚀 Features
- ✅ 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
📦 Installation
Via NPM
npm install @mapvx/website-componentAfter installation, you can reference the files in your application:
JavaScript/TypeScript:
// 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:
<!-- Include the component files -->
<script src="node_modules/@mapvx/website-component/dist/browser/main.js"></script>
<link rel="stylesheet" href="node_modules/@mapvx/website-component/dist/browser/styles.css" />Webpack/Vite/Bundler:
// 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:
// In your main App.jsx or index.jsx
import '@mapvx/website-component/dist/browser/main.js'
function App() {
return <mapvx-website api-key="your-api-key-here" />
}/* In your main CSS file (e.g., index.css, App.css) */
@import '@mapvx/website-component/dist/browser/styles.css';Vue:
// In your main.js
import '@mapvx/website-component/dist/browser/main.js'/* 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:
{
"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:
<!-- Web Component Scripts -->
<script src="website-component/main.js" defer></script>
<!-- Web Component Styles -->
<link rel="stylesheet" href="website-component/styles.css" />Why this configuration is needed:
- Angular doesn't serve files from
node_modulesby default in development - Without proper asset configuration, you'll get
SyntaxError: Unexpected token '<'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:
// In your main.ts - Import the JavaScript
import '@mapvx/website-component/dist/browser/main.js'/* 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
Helper Package (SSR Support)
For Server-Side Rendering (SSR) applications that need to preload initial data on the server:
npm install @mapvx/website-component-helpersVia CDN (unpkg)
<!-- Include the component files -->
<script src="https://unpkg.com/@mapvx/website-component@latest/dist/browser/main.js"></script>
<link
rel="stylesheet"
href="https://unpkg.com/@mapvx/website-component@latest/dist/browser/styles.css"
/>🎯 Usage
Basic Usage
<!-- Basic implementation with required API key -->
<mapvx-website api-key="your-api-key-here"></mapvx-website>Advanced Usage with Configuration
<!-- With all available options -->
<mapvx-website
api-key="your-api-key-here"
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"
>
</mapvx-website>Framework Integration
React
function App() {
return (
<mapvx-website
api-key="your-api-key-here"
default-to-map="true"
show-category-filters="false"
/>
)
}Vue
<template>
<mapvx-website
api-key="your-api-key-here"
:default-to-map="true"
:show-category-filters="false"
/>
</template>Angular
// In your component
@Component({
template: `
<mapvx-website
[attr.api-key]="apiKey"
[attr.default-to-map]="defaultToMap"
[attr.show-category-filters]="showFilters"
/>
`,
})
export class MyComponent {
apiKey = 'your-api-key-here'
defaultToMap = true
showFilters = false
}📋 Input Properties
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 |
Input Usage Examples
Required Properties Only
<mapvx-website api-key="your-api-key-here" institution-id="institution-123"></mapvx-website>With Optional Configuration
<mapvx-website
api-key="your-api-key-here"
institution-id="institution-123"
default-to-map="true"
show-category-filters="false"
show-city-filter="true"
hide-deals="true"
>
</mapvx-website>📤 Output Events
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. |
Output Usage Examples
Vanilla 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.
<script>
// Wait for the web component to be registered
function setupUrlUpdater() {
const element = document.querySelector('mapvx-website')
if (!element) {
setTimeout(setupUrlUpdater, 100)
return
}
const handleUserAction = (event) => {
if (event instanceof CustomEvent) {
const action = event.detail
switch (action.type) {
case 'return-to-home':
case 'select-filter': {
const { filter } = action.data
history.pushState(
{ page: 'home', filter },
'',
`${location.pathname}?tab=${encodeURIComponent(filter)}`,
)
break
}
case 'show-place': {
const { placeId, alias } = action.data
if (alias) {
history.pushState(
{ page: 'profile', id: alias },
'',
`${location.pathname}?tenant=${alias}`,
)
} else {
history.pushState(
{ page: 'profile', id: placeId },
'',
`${location.pathname}?tenant=${placeId}`,
)
}
break
}
case 'search': {
const { searchTerm } = action.data
history.pushState(
{ page: 'search', term: searchTerm },
'',
`${location.pathname}?search=${encodeURIComponent(searchTerm)}`,
)
break
}
case 'select-destination': {
const { destinationId } = action.data
history.pushState(
{ page: 'route', id: destinationId },
'',
`${location.pathname}?destination=${destinationId}`,
)
break
}
}
}
}
element.addEventListener('userAction', handleUserAction)
}
// Check if web component is already registered
if (customElements.get('mapvx-website')) {
setupUrlUpdater()
} else {
// Wait for registration
const waitForRegistration = setInterval(() => {
if (customElements.get('mapvx-website')) {
clearInterval(waitForRegistration)
setupUrlUpdater()
}
}, 100)
}
</script>React
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 <mapvx-website ref={webComponentRef} api-key="your-api-key-here" />
}Vue
<template>
<mapvx-website ref="webComponent" api-key="your-api-key-here" />
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const webComponent = ref(null)
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
}
}
onMounted(() => {
if (webComponent.value) {
webComponent.value.addEventListener('userAction', handleUserAction)
}
})
onUnmounted(() => {
if (webComponent.value) {
webComponent.value.removeEventListener('userAction', handleUserAction)
}
})
</script>Angular
import { Component, ElementRef, OnDestroy, ViewChild, AfterViewInit } from '@angular/core'
@Component({
selector: 'app-mapvx',
template: ` <mapvx-website #webComponent [attr.api-key]="apiKey" /> `,
})
export class MapvxComponent implements AfterViewInit, OnDestroy {
@ViewChild('webComponent', { static: false }) webComponentRef!: ElementRef<HTMLElement>
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)
}
}
}User Action Event
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
// 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
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 <mapvx-website ref={webComponentRef} api-key="your-api-key-here" />
}Vue
<template>
<mapvx-website ref="webComponent" api-key="your-api-key-here" />
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const webComponent = ref(null)
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
}
}
}
onMounted(() => {
if (webComponent.value) {
webComponent.value.addEventListener('userAction', handleUserAction)
}
})
onUnmounted(() => {
if (webComponent.value) {
webComponent.value.removeEventListener('userAction', handleUserAction)
}
})
</script>Angular
import { Component, ElementRef, OnDestroy, ViewChild, AfterViewInit } from '@angular/core'
@Component({
selector: 'app-mapvx',
template: ` <mapvx-website #webComponent [attr.api-key]="apiKey" /> `,
})
export class MapvxComponent implements AfterViewInit, OnDestroy {
@ViewChild('webComponent', { static: false }) webComponentRef!: ElementRef<HTMLElement>
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)
}
}
}🔧 Server-Side Rendering (SSR) Support
For applications using Server-Side Rendering (SSR) where you need to preload initial data on the server before sending it to the browser:
Installation
npm install @mapvx/website-component-helpersServer-Side Usage
Next.js (App Router)
1. Install dependencies:
npm install @mapvx/website-component @mapvx/website-component-helpers2. Copy the bundle script to your package.json:
{
"scripts": {
"copy-bundle": "node copy-bundle.mjs"
}
}3. Create copy-bundle.mjs in your project root:
#!/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:
// 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 (
<div>
<Script src='/mapvx-website/bundle.js' />
<mapvx-website
api-key={process.env.NEXT_PUBLIC_MAPVX_API_KEY}
institution-id={process.env.NEXT_PUBLIC_MAPVX_INSTITUTION_ID}
initial-data={initialData}
default-to-map="true"
/>
</div>
);
}5. Set environment variables in .env.local:
NEXT_PUBLIC_MAPVX_API_KEY=your-api-key-here
NEXT_PUBLIC_MAPVX_INSTITUTION_ID=your-institution-id6. Run the copy script before building:
npm run copy-bundle
npm run buildNuxt.js
<!-- pages/index.vue -->
<template>
<div>
<h1>My App</h1>
<mapvx-website :api-key="apiKey" :initial-data="initialData" default-to-map="true" />
</div>
</template>
<script setup>
import { prepareInitialData } from '@mapvx/website-component-helpers'
// Server-side data fetching
const { data: initialData } = await useAsyncData('mapvx-data', async () => {
return await prepareInitialData(process.env.MAPVX_API_KEY)
})
const apiKey = process.env.MAPVX_API_KEY
</script>SvelteKit
// 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,
}
}<!-- src/routes/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<div>
<h1>My App</h1>
<mapvx-website
api-key={process.env.MAPVX_API_KEY}
initial-data={data.initialData}
default-to-map="true"
/>
</div>What prepareInitialData does
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
Important Notes
⚠️ 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
Custom Colors
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:
:root {
--mapvx-secondary-orange: #b41500;
--mapvx-secondary-blue: #053a96;
}Example Implementation
HTML with inline styles:
<style>
:root {
--mapvx-secondary-orange: #ff6b35;
--mapvx-secondary-blue: #1e3a8a;
}
</style>
<mapvx-website api-key="your-api-key-here"></mapvx-website>CSS file:
/* styles.css */
:root {
--mapvx-secondary-orange: #e74c3c;
--mapvx-secondary-blue: #3498db;
}Framework-specific examples:
React:
// In your CSS file or styled-components
:root {
--mapvx-secondary-orange: #ff6b35;
--mapvx-secondary-blue: #1e3a8a;
}
function App() {
return <mapvx-website api-key="your-api-key-here" />;
}Vue:
<template>
<mapvx-website api-key="your-api-key-here" />
</template>
<style>
:root {
--mapvx-secondary-orange: #ff6b35;
--mapvx-secondary-blue: #1e3a8a;
}
</style>Angular:
// styles.scss
:root {
--mapvx-secondary-orange: #ff6b35;
--mapvx-secondary-blue: #1e3a8a;
}Available Color Variables
| 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 |
Notes
- 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
Common Issues
SyntaxError: Unexpected token '<' in Angular
Problem: 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
deferattribute is used for script tags
🏠 Homepage
Visit MapVX for more information.
