npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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-component

After 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_modules by 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-helpers

Via 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-helpers

Server-Side Usage

Next.js (App Router)

1. Install dependencies:

npm install @mapvx/website-component @mapvx/website-component-helpers

2. 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-id

6. Run the copy script before building:

npm run copy-bundle
npm run build

Nuxt.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 defer attribute is used for script tags

🏠 Homepage

Visit MapVX for more information.