Template directives for integrating routing into Angular templates and managing route-based UI updates. Essential components for declarative routing in templates.
Acts as placeholder that Angular dynamically fills based on router state. Primary mechanism for displaying routed components.
/**
* Directive that acts as placeholder for routed components
* Selector: 'router-outlet'
* Export as: 'outlet'
*/
@Directive({
selector: 'router-outlet',
exportAs: 'outlet'
})
class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
/** Name of outlet for named outlet routing (default: 'primary') */
@Input() name: string;
/** Data for child injector via ROUTER_OUTLET_DATA token (input signal) */
readonly routerOutletData = input<unknown>();
/** Whether outlet is activated (getter) */
isActivated: boolean;
/** Currently activated component instance (getter) */
component: Object;
/** Activated route (getter) */
activatedRoute: ActivatedRoute;
/** Data of activated route (getter) */
activatedRouteData: Data;
// Events
/** Emits when component instantiated */
@Output() activateEvents: EventEmitter<any>;
/** Emits when component destroyed */
@Output() deactivateEvents: EventEmitter<any>;
/** Emits when component reattached */
@Output() attachEvents: EventEmitter<unknown>;
/** Emits when component detached */
@Output() detachEvents: EventEmitter<unknown>;
/**
* Activate outlet with new component
* @param activatedRoute Route to activate
* @param environmentInjector Injector for component
*/
activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector): void;
/** Deactivate current component */
deactivate(): void;
/**
* Detach current component without destroying
* @returns Component reference for reattachment
*/
detach(): ComponentRef<any>;
/**
* Attach previously detached component
* @param ref Component reference to attach
* @param activatedRoute Route for component
*/
attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute): void;
}RouterOutlet Usage Examples:
// Basic usage in template
@Component({
template: `
<nav>
<a routerLink="/home">Home</a>
<a routerLink="/about">About</a>
</nav>
<!-- Primary outlet -->
<router-outlet></router-outlet>
`
})
export class AppComponent {}
// Named outlets
@Component({
template: `
<div class="layout">
<!-- Primary content -->
<main>
<router-outlet></router-outlet>
</main>
<!-- Named outlets -->
<aside>
<router-outlet name="sidebar"></router-outlet>
</aside>
<footer>
<router-outlet name="footer"></router-outlet>
</footer>
</div>
`
})
export class LayoutComponent {}
// Handling outlet events
@Component({
template: `
<router-outlet
(activateEvents)="onActivate($event)"
(deactivateEvents)="onDeactivate($event)"
(attachEvents)="onAttach($event)"
(detachEvents)="onDetach($event)">
</router-outlet>
`
})
export class AppComponent {
onActivate(component: any) {
console.log('Component activated:', component);
// Access activated component
if (component.title) {
document.title = component.title;
}
}
onDeactivate(component: any) {
console.log('Component deactivated:', component);
// Cleanup logic
if (component.cleanup) {
component.cleanup();
}
}
onAttach(component: any) {
console.log('Component attached:', component);
}
onDetach(component: any) {
console.log('Component detached:', component);
}
}
// Accessing outlet programmatically
@Component({
template: `
<router-outlet #outlet="outlet"></router-outlet>
<div *ngIf="outlet.isActivated">
<p>Current component: {{outlet.component.constructor.name}}</p>
<p>Current route: {{outlet.activatedRoute.snapshot.url}}</p>
</div>
`
})
export class AppComponent {}
// Route configuration for named outlets
const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
children: [
{ path: 'stats', component: StatsComponent, outlet: 'sidebar' },
{ path: 'notifications', component: NotificationsComponent, outlet: 'footer' }
]
}
];
// Navigation to named outlets
// URL: /dashboard/(sidebar:stats//footer:notifications)
this.router.navigate([
'dashboard',
{ outlets: { sidebar: 'stats', footer: 'notifications' } }
]);Makes element a link that initiates navigation to a route. Primary directive for declarative navigation in templates.
/**
* Directive that makes element a link for route navigation
* Selector: '[routerLink]'
*/
@Directive({
selector: '[routerLink]'
})
class RouterLink implements OnChanges, OnDestroy {
/** Commands or UrlTree for navigation */
@Input() routerLink: readonly any[] | string | UrlTree | null | undefined;
/** Query parameters for navigation */
@Input() queryParams?: Params | null;
/** Fragment for navigation */
@Input() fragment?: string;
/** How to handle query parameters */
@Input() queryParamsHandling?: QueryParamsHandling | null;
/** State to persist to browser history */
@Input() state?: {[k: string]: any};
/** Transient information about navigation */
@Input() info?: unknown;
/** Route for relative navigation */
@Input() relativeTo?: ActivatedRoute | null;
/** Target attribute for <a> elements */
@Input() target?: string;
/** Whether to preserve URL fragment */
@Input() preserveFragment: boolean;
/** Whether to skip location change */
@Input() skipLocationChange: boolean;
/** Whether to replace current URL in history */
@Input() replaceUrl: boolean;
/** Computed href attribute (getter/setter) */
href: string | null;
}RouterLink Usage Examples:
// Basic navigation
@Component({
template: `
<!-- String path -->
<a routerLink="/home">Home</a>
<!-- Array commands -->
<a [routerLink]="['/users', userId]">User Profile</a>
<!-- With parameters -->
<a [routerLink]="['/products', product.id, 'details']">Product Details</a>
<!-- Relative navigation -->
<a routerLink="../back">Go Back</a>
<a routerLink="./details">View Details</a>
`
})
export class NavigationComponent {
userId = 123;
product = { id: 456 };
}
// Query parameters and fragments
@Component({
template: `
<!-- With query parameters -->
<a [routerLink]="['/search']"
[queryParams]="{q: searchTerm, category: selectedCategory}">
Search
</a>
<!-- With fragment -->
<a [routerLink]="['/docs']"
fragment="installation">
Installation Guide
</a>
<!-- Combining query params and fragment -->
<a [routerLink]="['/articles', article.id]"
[queryParams]="{comments: 'true'}"
fragment="discussion">
View with Comments
</a>
`
})
export class ArticleComponent {
searchTerm = 'angular';
selectedCategory = 'tutorials';
article = { id: 789 };
}
// Advanced navigation options
@Component({
template: `
<!-- Query params handling -->
<a [routerLink]="['/filter']"
[queryParams]="{type: 'new'}"
queryParamsHandling="merge">
Add Filter (merge existing params)
</a>
<!-- Preserve fragment -->
<a [routerLink]="['/settings']"
[preserveFragment]="true">
Settings (keep current fragment)
</a>
<!-- Replace URL in history -->
<a [routerLink]="['/modal']"
[replaceUrl]="true">
Open Modal (replace current URL)
</a>
<!-- Skip location change -->
<a [routerLink]="['/preview']"
[skipLocationChange]="true">
Preview (don't update URL)
</a>
<!-- Custom state -->
<a [routerLink]="['/details']"
[state]="{from: 'search', timestamp: Date.now()}">
View Details (with state)
</a>
`
})
export class AdvancedNavigationComponent {}
// Programmatic href access
@Component({
template: `
<a #link="routerLink"
[routerLink]="['/users', userId]"
[queryParams]="{tab: 'profile'}">
User Profile
</a>
<p>Generated URL: {{link.href}}</p>
`
})
export class HrefExampleComponent {
userId = 123;
}
// Conditional navigation
@Component({
template: `
<a *ngFor="let item of items"
[routerLink]="item.enabled ? ['/item', item.id] : null"
[class.disabled]="!item.enabled">
{{item.name}}
</a>
<!-- Alternative with method -->
<a *ngFor="let item of items"
[routerLink]="getItemLink(item)">
{{item.name}}
</a>
`
})
export class ConditionalNavigationComponent {
items = [
{ id: 1, name: 'Item 1', enabled: true },
{ id: 2, name: 'Item 2', enabled: false },
{ id: 3, name: 'Item 3', enabled: true }
];
getItemLink(item: any): any[] | null {
return item.enabled ? ['/item', item.id] : null;
}
}Tracks whether linked route is currently active and applies CSS classes accordingly. Essential for navigation UI feedback.
/**
* Directive that tracks active router links and applies CSS classes
* Selector: '[routerLinkActive]'
* Export as: 'routerLinkActive'
*/
@Directive({
selector: '[routerLinkActive]',
exportAs: 'routerLinkActive'
})
class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit {
/** CSS classes to apply when active */
@Input() routerLinkActive: string[] | string;
/** Options for determining active state */
@Input() routerLinkActiveOptions: {exact: boolean} | IsActiveMatchOptions;
/** Aria-current attribute value when active */
@Input() ariaCurrentWhenActive?: 'page' | 'step' | 'location' | 'date' | 'time' | true | false;
/** Query for RouterLink directives to track (automatically populated) */
@ContentChildren(RouterLink, {descendants: true}) links!: QueryList<RouterLink>;
/** Whether any tracked links are active (getter) */
isActive: boolean;
/** Emits when active state changes */
@Output() isActiveChange: EventEmitter<boolean>;
}RouterLinkActive Usage Examples:
// Basic active class
@Component({
template: `
<nav>
<a routerLink="/home"
routerLinkActive="active">Home</a>
<a routerLink="/about"
routerLinkActive="active">About</a>
<a routerLink="/contact"
routerLinkActive="active">Contact</a>
</nav>
`,
styles: [`
.active {
font-weight: bold;
color: #007bff;
}
`]
})
export class NavigationComponent {}
// Multiple CSS classes
@Component({
template: `
<nav>
<a routerLink="/dashboard"
[routerLinkActive]="['active', 'highlighted']">
Dashboard
</a>
<a routerLink="/reports"
routerLinkActive="active highlighted">
Reports
</a>
</nav>
`,
styles: [`
.active { color: blue; }
.highlighted { background-color: yellow; }
`]
})
export class MultiClassNavigationComponent {}
// Exact matching
@Component({
template: `
<nav>
<!-- Active for /home only, not /home/sub-routes -->
<a routerLink="/home"
routerLinkActive="active"
[routerLinkActiveOptions]="{exact: true}">
Home
</a>
<!-- Active for /users and /users/123, /users/123/profile, etc. -->
<a routerLink="/users"
routerLinkActive="active">
Users
</a>
</nav>
`
})
export class ExactMatchNavigationComponent {}
// Advanced matching options
@Component({
template: `
<nav>
<a routerLink="/search"
[queryParams]="{category: 'books'}"
routerLinkActive="active"
[routerLinkActiveOptions]="{
paths: 'exact',
queryParams: 'subset',
matrixParams: 'ignored',
fragment: 'ignored'
}">
Books
</a>
</nav>
`
})
export class AdvancedMatchingComponent {}
// Accessibility with aria-current
@Component({
template: `
<nav>
<a routerLink="/page1"
routerLinkActive="active"
ariaCurrentWhenActive="page">
Page 1
</a>
<a routerLink="/page2"
routerLinkActive="active"
ariaCurrentWhenActive="page">
Page 2
</a>
</nav>
`
})
export class AccessibleNavigationComponent {}
// Programmatic access to active state
@Component({
template: `
<nav>
<a #linkActive="routerLinkActive"
routerLink="/dashboard"
routerLinkActive="active"
(isActiveChange)="onActiveChange($event)">
Dashboard
<span *ngIf="linkActive.isActive">(Current)</span>
</a>
</nav>
<p>Dashboard is active: {{linkActive?.isActive}}</p>
`
})
export class ProgrammaticActiveComponent {
onActiveChange(isActive: boolean) {
console.log('Dashboard active state changed:', isActive);
if (isActive) {
// Update page title, analytics, etc.
document.title = 'Dashboard - My App';
}
}
}
// Complex navigation with nested routes
@Component({
template: `
<nav class="main-nav">
<!-- Parent navigation -->
<a routerLink="/products"
routerLinkActive="active"
[routerLinkActiveOptions]="{exact: false}">
Products
</a>
<!-- Child navigation (shown when products section is active) -->
<nav class="sub-nav" *ngIf="isProductsSectionActive()">
<a routerLink="/products/list"
routerLinkActive="sub-active">
Product List
</a>
<a routerLink="/products/categories"
routerLinkActive="sub-active">
Categories
</a>
<a routerLink="/products/new"
routerLinkActive="sub-active">
Add Product
</a>
</nav>
</nav>
`
})
export class NestedNavigationComponent {
constructor(private router: Router) {}
isProductsSectionActive(): boolean {
return this.router.url.startsWith('/products');
}
}
// Custom active class binding
@Component({
template: `
<nav>
<a routerLink="/home"
[class.my-active]="isHomeActive"
#homeLink="routerLinkActive"
routerLinkActive
(isActiveChange)="isHomeActive = $event">
Home
</a>
</nav>
`,
styles: [`
.my-active {
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
color: white;
}
`]
})
export class CustomActiveClassComponent {
isHomeActive = false;
}Defines contract for developing custom router outlets. Used for advanced outlet implementations.
/**
* Interface defining contract for custom router outlets
*/
interface RouterOutletContract {
/** Whether outlet is activated */
isActivated: boolean;
/** Currently activated component instance */
component: Object | null;
/** Data of activated route */
activatedRouteData: Data;
/** Activated route */
activatedRoute: ActivatedRoute | null;
/**
* Activate outlet with new component
* @param activatedRoute Route to activate
* @param environmentInjector Injector for component
*/
activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector | null): void;
/** Deactivate current component */
deactivate(): void;
/**
* Detach current component without destroying
* @returns Component reference for reattachment
*/
detach(): ComponentRef<any>;
/**
* Attach previously detached component
* @param ref Component reference to attach
* @param activatedRoute Route for component
*/
attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute): void;
}Custom Router Outlet Example:
import {
Directive,
ViewContainerRef,
ComponentRef,
EnvironmentInjector,
Output,
EventEmitter
} from '@angular/core';
import { RouterOutletContract, ActivatedRoute, Data } from '@angular/router';
@Directive({
selector: 'custom-outlet'
})
export class CustomOutletDirective implements RouterOutletContract {
@Output() activateEvents = new EventEmitter<any>();
@Output() deactivateEvents = new EventEmitter<any>();
private activated: ComponentRef<any> | null = null;
private _activatedRoute: ActivatedRoute | null = null;
constructor(
private viewContainer: ViewContainerRef,
private environmentInjector: EnvironmentInjector
) {}
get isActivated(): boolean {
return !!this.activated;
}
get component(): Object | null {
return this.activated?.instance || null;
}
get activatedRoute(): ActivatedRoute | null {
return this._activatedRoute;
}
get activatedRouteData(): Data {
return this._activatedRoute?.snapshot.data || {};
}
activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector | null): void {
if (this.isActivated) {
this.deactivate();
}
this._activatedRoute = activatedRoute;
const component = activatedRoute.snapshot.component;
if (component) {
// Custom activation logic
const injector = environmentInjector || this.environmentInjector;
this.activated = this.viewContainer.createComponent(component, { injector });
// Custom component setup
this.setupComponent(this.activated.instance);
this.activateEvents.emit(this.activated.instance);
}
}
deactivate(): void {
if (this.activated) {
const component = this.activated.instance;
// Custom cleanup logic
this.cleanupComponent(component);
this.activated.destroy();
this.activated = null;
this._activatedRoute = null;
this.viewContainer.clear();
this.deactivateEvents.emit(component);
}
}
detach(): ComponentRef<any> {
if (!this.activated) {
throw new Error('No component to detach');
}
const ref = this.activated;
this.activated = null;
this.viewContainer.detach(this.viewContainer.indexOf(ref.hostView));
return ref;
}
attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute): void {
this.activated = ref;
this._activatedRoute = activatedRoute;
this.viewContainer.insert(ref.hostView);
}
private setupComponent(component: any): void {
// Custom component initialization
if (component.onRouteActivate) {
component.onRouteActivate(this._activatedRoute);
}
}
private cleanupComponent(component: any): void {
// Custom component cleanup
if (component.onRouteDeactivate) {
component.onRouteDeactivate();
}
}
}type Data = {[key: string | symbol]: any};
type Params = {[key: string]: any};
type QueryParamsHandling = 'merge' | 'preserve' | 'replace' | '';
interface IsActiveMatchOptions {
/** How to match matrix parameters */
matrixParams: 'exact' | 'subset' | 'ignored';
/** How to match query parameters */
queryParams: 'exact' | 'subset' | 'ignored';
/** How to match URL paths */
paths: 'exact' | 'subset';
/** How to match URL fragment */
fragment: 'exact' | 'ignored';
}
/**
* Injection token for router outlet data
*/
const ROUTER_OUTLET_DATA: InjectionToken<unknown>;
/**
* Service for binding router data to component inputs
*/
class RoutedComponentInputBinder {
bindInputs(outlet: RouterOutletContract, route: ActivatedRoute): void;
}
/**
* Service managing outlet contexts for router outlets
*/
class ChildrenOutletContexts {
getOrCreateContext(childName: string): OutletContext;
getContext(childName: string): OutletContext | null;
}
/**
* Context information for router outlets
*/
class OutletContext {
outlet: RouterOutletContract | null;
route: ActivatedRoute | null;
injector: EnvironmentInjector | null;
children: ChildrenOutletContexts;
attachRef: ComponentRef<any> | null;
}