Why route-with-dynamic-outlets?

Most panels/tabs implementations bypass the Angular Router entirely, losing guards, resolvers, lazy loading, and nested routing. This library keeps all those features while adding first-class support for outlets whose names are determined at runtime from the URL.

❌ Without this library

  • Custom state management for active panels
  • No route guards per panel
  • No lazy-loaded panel modules
  • Outlet names must be known at build time
  • No deep-link support for panel state
  • No nested routing inside panels

✅ With this library

  • Full Angular Router integration
  • Per-panel route guards & resolvers
  • Lazy-loaded panel components
  • Outlet names come from the URL at runtime
  • Deep-linkable panel state via URL
  • Fully nestable dynamic outlets
🔀

Dynamic outlet names

Outlet names are extracted directly from the URL — open any panel you want without touching your routes config.

🪆

Fully nestable

Dynamic outlets can contain further dynamic outlets, enabling arbitrarily deep panel hierarchies.

🛡️

Guards & resolvers

Every dynamically-created route participates fully in Angular's routing pipeline — guards, resolvers, data — all work.

🔗

Deep-linkable

The URL is the state. Share a URL and the exact same panels open in the same order.

🎮 Interactive Demo

Click the navigation buttons to open/close panels. The URL bar updates to reflect the router state, just as it would in a real Angular application.

/
router-outlet (primary)
☝️ Click a navigation button above to open dynamic outlets.
00:00:00 App initialized — no outlets active

💡 In a real app each panel would be a full Angular component loaded via <router-outlet [name]="outlet">. The library calls updateRoutes() automatically whenever the URL matcher runs, keeping the child routes in sync with the active outlets.

🚀 Getting Started

Install

bash
npm install route-with-dynamic-outlets

Create a component that renders dynamic outlets

Subscribe to the outlets$ stream from route data and render a <router-outlet> for each active outlet name.

dynamic-outlets.component.ts
import { Component } from '@angular/core';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { map, Observable, switchMap } from 'rxjs';
import { CommonModule } from '@angular/common';
import { OutletsMap } from 'route-with-dynamic-outlets';

@Component({
  standalone: true,
  selector: 'app-dynamic-outlets',
  template: `
  <a [routerLink]="[{ outlets: { A: [''], B: [''] } }]">Open A and B</a>
  <router-outlet
    *ngFor="let outlet of outlets$ | async"
    [name]="outlet"
  ></router-outlet>
  `,
  imports: [RouterModule, CommonModule],
})
export class DynamicOutletsComponent {
  constructor(protected activatedRoute: ActivatedRoute) {}

  outlets$ = this.activatedRoute.data.pipe(
    switchMap(({ outlets$ }) => outlets$ as Observable<OutletsMap>),
    map(outlets => Object.keys(outlets ?? {}))
  );
}

Register the route with createRouteWithDynamicOutlets

Wrap your route definition in createRouteWithDynamicOutlets() and provide a dynamicOutletFactory that returns the child route config for each outlet.

app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { createRouteWithDynamicOutlets } from 'route-with-dynamic-outlets';
import { DynamicOutletsComponent } from './dynamic-outlets/dynamic-outlets.component';
import { PanelComponent } from './panel/panel.component';

const routes: Routes = [
  createRouteWithDynamicOutlets({
    path: '',
    component: DynamicOutletsComponent,
    // Called once per active outlet — return the child route config
    dynamicOutletFactory: () => ({
      path: '',
      component: PanelComponent,
    }),
  }),
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

Navigate to open panels

Use Angular's standard routerLink or Router.navigate() with named outlets. The library automatically creates child routes for each outlet present in the URL.

template snippet
<!-- open two panels simultaneously -->
<a [routerLink]="[{ outlets: { A: [''], B: [''] } }]">
  Open A and B
</a>

<!-- close panel A -->
<a [routerLink]="[{ outlets: { A: null } }]">
  Close A
</a>

⚙️ Advanced Usage

The dynamicOutletFactory receives the full routing context, letting you customize each outlet's route based on segment data.

Custom URL matcher (e.g. /@username)

routing.ts
createRouteWithDynamicOutlets({
  // Match /@username style segments
  matcher: (segments) => {
    if (segments[0]?.path.startsWith('@')) {
      return { consumed: [segments[0]] };
    }
    return null;
  },
  component: ProfileShellComponent,
  dynamicOutletFactory: (segments, group, route, outletName) => ({
    path: '',
    component: PanelComponent,
    data: { outletName },   // pass context to the component
  }),
})

Nested dynamic outlets

routing.ts
const panelRoute = createRouteWithDynamicOutlets({
  path: '',
  component: PanelComponent,
  dynamicOutletFactory: () => ({
    path: '',
    component: SubPanelComponent,
  }),
});

createRouteWithDynamicOutlets({
  path: '',
  component: WorkspaceComponent,
  dynamicOutletFactory: () => panelRoute,  // ← nest!
})

API reference

types
/** Alias for the children map on a UrlSegmentGroup */
type OutletsMap = UrlSegmentGroup['children'];

/** Extend Route with your factory */
type RouteWithDynamicOutlets = Route & {
  dynamicOutletFactory: (
    segments: UrlSegment[],
    group:    UrlSegmentGroup,
    route:    Route,
    outlet:   string
  ) => Omit<Route, 'outlet'>;
};

/** Convert a RouteWithDynamicOutlets → a plain Route */
function createRouteWithDynamicOutlets(
  route: RouteWithDynamicOutlets
): Route;