Skip to content

[Research] SPA Routing in Web Components #229

@AlexMikhalev

Description

@AlexMikhalev

Research Objective

Investigate client-side routing solutions for Web Components-based SPA replacing Tinro.

Current State (App.svelte)

  • Using Tinro router for Svelte
  • Routes: /, /chat, /graph, /config/wizard, /config/json
  • Active link highlighting
  • History API integration
  • Declarative routing with <Route> components

Reference: desktop/src/App.svelte:81-88

Research Questions

  1. How to implement routing in Web Components without framework?
  2. Which routing libraries work with Web Components?
  3. Declarative vs imperative routing?
  4. How to handle route parameters and query strings?
  5. Browser history management?
  6. Active link styling?

Routing Approaches

Approach 1: Vaadin Router

Web Components-specific router by Vaadin team.

Pros:

  • Built for Web Components
  • Type-safe (TypeScript)
  • Declarative or imperative
  • Lazy loading support
  • Lifecycle hooks
  • Well-documented

Cons:

  • Another dependency
  • Learning curve

Example:

import { Router } from '@vaadin/router';

const router = new Router(document.querySelector('#outlet'));
router.setRoutes([
  { path: '/', component: 'terraphim-search' },
  { path: '/chat', component: 'terraphim-chat' },
  { path: '/graph', component: 'terraphim-rolegraph' },
]);

Repository: https://github.com/vaadin/router

Approach 2: page.js

Minimalist client-side router inspired by Express.

Pros:

  • Tiny (~1KB gzipped)
  • Simple API
  • Express-like routing
  • Well-established

Cons:

  • Imperative only
  • Manual component mounting
  • No TypeScript definitions

Example:

import page from 'page';

page('/', () => mountComponent('terraphim-search'));
page('/chat', () => mountComponent('terraphim-chat'));
page('/graph', () => mountComponent('terraphim-rolegraph'));
page();

Repository: https://github.com/visionmedia/page.js

Approach 3: Custom Router (Vanilla)

Build minimal router using History API.

Pros:

  • Zero dependencies
  • Full control
  • Exactly what we need
  • Learning experience

Cons:

  • Need to handle edge cases
  • Testing burden
  • Maintenance

Example:

class Router {
  routes = new Map();
  
  add(path, component) {
    this.routes.set(path, component);
  }
  
  navigate(path) {
    const component = this.routes.get(path);
    if (component) {
      this.outlet.innerHTML = '';
      this.outlet.appendChild(document.createElement(component));
      history.pushState({}, '', path);
    }
  }
  
  init() {
    window.addEventListener('popstate', () => {
      this.navigate(location.pathname);
    });
  }
}

Approach 4: Lit Router

Routing solution specifically for Lit-based Web Components.

Pros:

  • Integrates with Lit
  • Reactive
  • Decorators

Cons:

  • Requires Lit framework
  • Tied to Lit ecosystem

Action: Only if we choose Lit for build tooling

Repository: https://github.com/lit/lit/tree/main/packages/labs/router

Approach 5: Navigo

Lightweight router with hash and history support.

Pros:

  • Small (~4KB)
  • TypeScript support
  • Hooks
  • Nested routes

Cons:

  • Less Web Components specific

Repository: https://github.com/krasimir/navigo

Features to Maintain

From Tinro (Current):

  • Declarative routes
  • Active link highlighting (use:active directive)
  • History API integration
  • Nested routes
  • Exact path matching
  • Route parameters (not currently used)

New Requirements:

  • Web Components compatible
  • TypeScript support
  • Lazy loading support
  • Guard/middleware support
  • Query parameter handling
  • Hash routing option (for Tauri)

Routing Patterns

Pattern 1: Declarative with Router Outlet

<terraphim-app>
  <terraphim-router>
    <terraphim-route path="/" component="terraphim-search"></terraphim-route>
    <terraphim-route path="/chat" component="terraphim-chat"></terraphim-route>
    <terraphim-route path="/graph" component="terraphim-rolegraph"></terraphim-route>
  </terraphim-router>
</terraphim-app>

Pros:

  • Clear structure
  • Easy to understand
  • Similar to current Tinro

Cons:

  • More complex implementation
  • Need custom elements for router and route

Pattern 2: Imperative Configuration

class TerraphimApp extends HTMLElement {
  connectedCallback() {
    this.router = new Router(this.shadowRoot.querySelector('#outlet'));
    this.router.setRoutes([
      { path: '/', component: 'terraphim-search' },
      { path: '/chat', component: 'terraphim-chat' },
      { path: '/graph', component: 'terraphim-rolegraph' },
    ]);
  }
}

Pros:

  • Simple
  • Less boilerplate
  • Dynamic routing

Cons:

  • Less declarative
  • Harder to see structure

Active Link Styling

Current: Tinro's use:active directive

Web Components Solution:

class TerraphimNav extends HTMLElement {
  connectedCallback() {
    this.router.addEventListener('route-changed', () => {
      this.updateActiveLinks();
    });
  }
  
  updateActiveLinks() {
    const links = this.querySelectorAll('a');
    links.forEach(link => {
      const isActive = link.pathname === location.pathname;
      link.classList.toggle('active', isActive);
    });
  }
}

Tauri Considerations

Problem: File protocol (file://) doesn't support pushState well

Solution:

  • Use hash routing (#/, #/chat)
  • Or configure base URL properly
  • Vaadin Router handles this automatically

Acceptance Criteria

  • Router library selected (or custom built)
  • Routing pattern documented
  • Prototype with all routes working
  • Active link highlighting functional
  • History API integration tested
  • Tauri compatibility verified
  • TypeScript types available
  • Migration path from Tinro
  • Performance acceptable

References

Documentation

Findings will be documented in: .docs/research-spa-routing.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    researchResearch and investigation tasksweb-componentsWeb Components migration

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions