Leverage Angular Router's full power — nested routes, guards, resolvers — for panels & tabs with dynamically named 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.
Outlet names are extracted directly from the URL — open any panel you want without touching your routes config.
Dynamic outlets can contain further dynamic outlets, enabling arbitrarily deep panel hierarchies.
Every dynamically-created route participates fully in Angular's routing pipeline — guards, resolvers, data — all work.
The URL is the state. Share a URL and the exact same panels open in the same order.
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.
💡 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.
npm install route-with-dynamic-outlets
Subscribe to the outlets$ stream from route data
and render a <router-outlet> for each active
outlet name.
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 ?? {})) ); }
createRouteWithDynamicOutlets
Wrap your route definition in
createRouteWithDynamicOutlets() and provide a
dynamicOutletFactory that returns the child route
config for each outlet.
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 {}
Use Angular's standard routerLink or
Router.navigate() with named outlets. The library
automatically creates child routes for each outlet present in
the URL.
<!-- 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>
The dynamicOutletFactory receives the full routing
context, letting you customize each outlet's route based on segment
data.
/@username)
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 }), })
const panelRoute = createRouteWithDynamicOutlets({ path: '', component: PanelComponent, dynamicOutletFactory: () => ({ path: '', component: SubPanelComponent, }), }); createRouteWithDynamicOutlets({ path: '', component: WorkspaceComponent, dynamicOutletFactory: () => panelRoute, // ← nest! })
/** 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;