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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@minute-spa/vanilla-ui-router

v0.0.3

Published

A simple vanilla client side SPA router.

Readme

Vanilla UI Router

⚠️ DEPRECATED: This package is deprecated. Please use the public MinSPA repository for the newest code: https://github.com/devosm1030/minspa

A lightweight, client-side router for single-page applications (SPAs) that handles navigation, route registration, authentication, and lifecycle management. This is a standalone component of the minute-spa browser SPA application framework (work in progress).

This module is a component of the Minute SPA framework (https://github.com/devosm1030/minute-spa), but is an standalone component that can be used independently of the framework.

Zero dependencies - use in your project via NPM, or simply clone the code right into your project for a truly vanilla experience that requires no packagers or bundlers!

Table of Contents

Overview

The VanillaUiRouter class provides a simple yet powerful routing solution for vanilla JavaScript applications. It manages:

  • Route Registration: Define paths and associated HTML content
  • Navigation: Handle programmatic and browser-based navigation
  • Authentication: Optionally implements custom authorization logic
  • Lifecycle Hooks: Execute callbacks when pages are rendered or unmounted
  • History Management: Integrates with the browser's History API
  • Navigation Listeners: Subscribe to route changes

Installation and Usage

For NodeJS projects

Installation

In your project directory, install the dependency on the command line:

npm install --save @minute-spa/vanilla-ui-router

Usage

Import the package in your code:

import { VanillaUiRouter } from '@minute-spa/vanilla-ui-router'

For Vanila projects

Installation

Clone https://github.com/devosm1030/minute-spa/blob/main/packages/VanillaUiRouter/index.js into your project and rename as appropriate.

Usage

From an ES module, import the package in your code:

import { VanillaUiRouter } from '<path/to/your/file>'

Constructor

new VanillaUiRouter(rootElem, pages = [])

Creates a new VanillaUiRouter instance.

Parameters

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | rootElem | HTMLElement or string | Yes | The DOM element or selector where pages will be mounted | | pages | Array<{path: string, page: Object}> | No | Optional array of page configurations to register during initialization |

Returns

A new VanillaUiRouter instance.

Throws

  • Error - If rootElem is not provided

Example

const rootElem = document.getElementById('app')
const homeElem = document.createElement('div')
homeElem.innerHTML = '<h1>Home</h1>'
const aboutElem = document.createElement('div')
aboutElem.innerHTML = '<h1>About</h1>'

const pages = [
  { path: '/home', page: { domElem: homeElem } },
  { path: '/about', page: { domElem: aboutElem } }
]
const router = new VanillaUiRouter(rootElem, pages)

Public Methods

registerRoute(path, pageConfig)

Registers a new route with the router.

Parameters

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | path | string | Yes | The URL path (e.g., '/home', '/about') | | pageConfig | Object | Yes | Configuration object for the page | | pageConfig.domElem | HTMLElement or function | Yes | DOM element to render, or a function that returns a DOM element | | pageConfig.onRendered | function(element) | No | Callback executed after page is rendered | | pageConfig.onUnmount | function(element) | No | Callback executed before page is removed |

Returns

The VanillaUiRouter instance (for method chaining).

Throws

  • Error - If path or domElem is missing

Example

const profileElem = document.createElement('div')
profileElem.id = 'profile'
profileElem.innerHTML = '<h1>User Profile</h1>'

router.registerRoute('/profile', {
  domElem: profileElem,
  onRendered: (element) => {
    console.log('Profile page rendered:', element)
    // Initialize event listeners, load data, etc.
  },
  onUnmount: (element) => {
    console.log('Profile page unmounting:', element)
    // Cleanup event listeners, cancel requests, etc.
  }
})

// Or use a function that returns an element if you want to 
// be able to change pages dynamically between navigations.
router.registerRoute('/dashboard', {
  domElem: () => {
    const elem = document.createElement('div')
    elem.innerHTML = '<h1>Dashboard</h1>'
    return elem
  }
})

initialNav()

Performs the initial navigation based on the current browser URL. Should be called after all routes are registered to render the appropriate page.

Returns

The VanillaUiRouter instance (for method chaining).

Example

const homeElem = document.createElement('h1')
homeElem.textContent = 'Home'
const aboutElem = document.createElement('h1')
aboutElem.textContent = 'About'

const router = new VanillaUiRouter(document.getElementById('app'))
  .registerRoute('/home', { domElem: homeElem })
  .registerRoute('/about', { domElem: aboutElem })
  .initialNav() // Renders the page matching current URL

navigateTo(path)

Programmatically navigates to a specified path. Updates the browser URL and renders the corresponding page.

Parameters

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | path | string | Yes | The path to navigate to |

Returns

undefined

Behavior

  • If the path is invalid and no auth callback is registered, navigation is ignored
  • Updates window.history using pushState
  • Triggers authentication check if configured
  • Renders the new page
  • Executes all registered navigation listeners

Example

// Navigate to a different page
router.navigateTo('/about')

// Navigate from within an event handler
document.getElementById('homeBtn').addEventListener('click', () => {
  router.navigateTo('/home')
})

onPathAuth(callback)

Optionally registers an authentication/authorization callback that is invoked before rendering any page. If your app does not require authentication/authorization, skip this registration, and all registered pages will be allowed to be navigated to/rendered.

Parameters

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | callback | function(path, helpers) | Yes | Authentication callback function |

The callback receives:

  • path (string): The path being navigated to
  • helpers (object): Helper functions for handling auth results
    • helpers.redirectTo(path): Redirect to a different path
    • helpers.renderDomElem(element): Render a custom DOM element instead

Callback Return Values

  • true - Allow navigation to the requested path
  • false - Block navigation (should call redirectTo or renderDomElem)

Returns

The VanillaUiRouter instance (for method chaining).

Throws

  • Error - If redirectTo is called with an invalid path

Example

router.onPathAuth((path, { redirectTo, renderDomElem }) => {
  // Check if user is authenticated
  if (!isUserLoggedIn() && path !== '/login') {
    // Redirect to login page
    return redirectTo('/login')
  }

  // Check if user has permission for this path
  if (path === '/admin' && !isUserAdmin()) {
    // Render unauthorized message in /admin path without changing path in browser
    const unauthorizedElem = document.createElement('h1')
    unauthorizedElem.textContent = 'Unauthorized'
    return renderDomElem(unauthorizedElem)
  }

  // Allow navigation
  return true
})

registerNavListener(callback)

Registers a callback to be executed whenever navigation occurs.

Parameters

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | callback | function(path) | Yes | Callback invoked on navigation |

The callback receives:

  • path (string): The path that was navigated to

Returns

The VanillaUiRouter instance (for method chaining).

Behavior

  • If a current path exists when registering, the callback is immediately invoked with that path
  • The callback is invoked after every successful navigation

Example

// Update active nav item
router.registerNavListener((path) => {
  navbar.activelink(path)
})

reauthenticate()

Re-runs the authentication check for the current path. Useful when authentication state changes (e.g., user logs in or out). N/A if you choose not to register an auth callback for your app.

Returns

undefined

Example

// After user logs in
async function login(username, password) {
  const success = await authService.login(username, password)
  if (success) {
    // Re-check authentication for current page
    router.reauthenticate()
  }
}

// After user logs out
function logout() {
  authService.logout()
  router.reauthenticate() // Will redirect to login if needed
}

isValidPath(path)

Checks if a given path has been registered with the router.

Parameters

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | path | string | Yes | The path to check |

Returns

boolean - true if the path is registered, false otherwise

Example

if (router.isValidPath('/profile')) {
  router.navigateTo('/profile')
} else {
  console.log('Profile page not found')
}

Route Configuration

Page Configuration Object

When registering routes, you provide a page configuration object:

{
  domElem: HTMLElement | function,  // Required: DOM element or function returning DOM element
  onRendered: function(element),    // Optional: Called after render
  onUnmount: function(element)      // Optional: Called before unmount
}

DOM Element

The domElem property can be:

  1. HTML Element:

    const pageElement = document.createElement('div')
    pageElement.innerHTML = '<h1>Welcome</h1>'
    { domElem: pageElement }
  2. Function returning HTML Element:

    { 
      domElem: () => {
        const elem = document.createElement('div')
        elem.innerHTML = '<h1>Welcome</h1>'
        return elem
      }
    }

Be sure to sanitize any untrusted content that could be used to build your DOM elements! For a NodeJS project, DomPurify (https://www.npmjs.com/package/dompurify) is a convenient tool for doing this.

Authentication & Authorization

Authentication and Authorization only occurs if you have registered an auth callback via onPathAuth.

Authentication Flow

  1. User attempts to navigate to a path
  2. Router calls the onPathAuth callback (if registered)
  3. Callback decides to:
    • Allow navigation (return true)
    • Redirect to another path (call redirectTo() and return false)
    • Render custom DOM element (call renderDomElem() and return false)

Example: Protected Routes

const publicRoutes = ['/home', '/about', '/login']

router.onPathAuth((path, { redirectTo }) => {
  // Allow public routes
  if (publicRoutes.includes(path)) {
    return true
  }

  // Check authentication for protected routes
  const token = localStorage.getItem('authToken')
  if (!token) {
    // Store the intended destination
    sessionStorage.setItem('redirectAfterLogin', path)
    return redirectTo('/login')
  }

  return true
})

Example: Role-Based Access

const adminRoutes = ['/admin', '/users', '/settings']

router.onPathAuth((path, { redirectTo, renderDomElem }) => {
  const user = getCurrentUser()

  if (!user && path !== '/login') {
    return redirectTo('/login')
  }

  if (adminRoutes.includes(path) && !user.isAdmin) {
    const deniedElem = document.createElement('div')
    deniedElem.innerHTML = '<h1>Access Denied</h1><p>Admin access required.</p>'
    return renderDomElem(deniedElem)
  }

  return true
})

Navigation Listeners

Navigation listeners are invoked after every successful navigation, making them perfect for:

  • Updating UI elements (navigation bars, breadcrumbs)
  • Analytics tracking
  • Logging
  • Side effects based on current route

Multiple Listeners

You can register multiple navigation listeners:

// Update page title
router.registerNavListener((path) => {
  const titles = {
    '/home': 'Home',
    '/about': 'About Us',
    '/contact': 'Contact'
  }
  document.title = titles[path] || 'My App'
})

// Update navigation state
router.registerNavListener((path) => {
  updateActiveNavItem(path)
})

// Track page views
router.registerNavListener((path) => {
  gtag('config', 'GA_MEASUREMENT_ID', {
    page_path: path
  })
})

Lifecycle Hooks

onRendered Hook

Called immediately after a page is rendered and added to the DOM.

Use cases:

  • Initialize event listeners
  • Load data
  • Start animations
  • Focus elements
  • Initialize third-party libraries
const dashboardElem = document.createElement('div')
dashboardElem.id = 'dashboard'
dashboardElem.innerHTML = '...' // dashboard HTML

router.registerRoute('/dashboard', {
  domElem: dashboardElem,
  onRendered: (element) => {
    // Initialize chart library
    const chartCanvas = element.querySelector('#chart')
    initializeChart(chartCanvas)

    // Load data
    fetchDashboardData().then(data => {
      updateDashboard(element, data)
    })

    // Add event listeners
    element.querySelector('#refresh-btn').addEventListener('click', refreshData)
  }
})

onUnmount Hook

Called just before a page is removed from the DOM.

Use cases:

  • Remove event listeners
  • Cancel pending requests
  • Clear timers/intervals
  • Cleanup third-party libraries
  • Save state
let refreshInterval

const feedElem = document.createElement('div')
feedElem.id = 'feed'
feedElem.innerHTML = '...' // feed HTML

router.registerRoute('/live-feed', {
  domElem: feedElem,
  onRendered: (element) => {
    // Start auto-refresh
    refreshInterval = setInterval(() => {
      updateFeed(element)
    }, 5000)
  },
  onUnmount: (element) => {
    // Stop auto-refresh
    clearInterval(refreshInterval)
    
    // Cancel any pending requests
    abortController.abort()
    
    console.log('Feed page cleaned up')
  }
})

Complete Examples

Basic Setup

import { VanillaUiRouter } from '@minute-spa/vanilla-ui-router'

// Create router
const router = new VanillaUiRouter(document.getElementById('app'))

// Create page elements
const homeElem = document.createElement('div')
homeElem.innerHTML = '<h1>Home Page</h1><p>Welcome!</p>'

const aboutElem = document.createElement('div')
aboutElem.innerHTML = '<h1>About Us</h1><p>Learn more about our company.</p>'

const contactElem = document.createElement('div')
contactElem.innerHTML = '<h1>Contact</h1><form>...</form>'

// Register routes
router
  .registerRoute('/home', { domElem: homeElem })
  .registerRoute('/about', { domElem: aboutElem })
  .registerRoute('/contact', { domElem: contactElem })

// Start router
router.initialNav()

Full-Featured Application

import { VanillaUiRouter } from '@minute-spa/vanilla-ui-router'
import { homePage } from './pages/home.js'
import { profilePage } from './pages/profile.js'
import { loginPage } from './pages/login.js'

// Initialize router with pages
const pages = [
  { path: '/home', page: homePage },
  { path: '/profile', page: profilePage },
  { path: '/login', page: loginPage }
]

const router = new VanillaUiRouter(document.getElementById('app'), pages)

// Add authentication
router.onPathAuth((path, { redirectTo }) => {
  const isAuthenticated = checkAuthStatus()
  
  if (!isAuthenticated && path !== '/login') {
    sessionStorage.setItem('returnTo', path)
    return redirectTo('/login')
  }
  
  if (isAuthenticated && path === '/login') {
    const returnTo = sessionStorage.getItem('returnTo') || '/home'
    sessionStorage.removeItem('returnTo')
    return redirectTo(returnTo)
  }
  
  return true
})

// Track navigation
router.registerNavListener((path) => {
  console.log('Navigated to:', path)
  updateBreadcrumbs(path)
})

// Initialize
router.initialNav()

License

MIT License - See notice in index.js comments.