or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-features.mddirectives.mdindex.mdnavigation-state.mdroute-config.mdrouting-core.mdurl-handling.md
tile.json

directives.mddocs/

Router Directives

Template directives for integrating routing into Angular templates and managing route-based UI updates. Essential components for declarative routing in templates.

Capabilities

RouterOutlet Directive

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' } }
]);

RouterLink Directive

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;
  }
}

RouterLinkActive Directive

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;
}

RouterOutletContract Interface

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();
    }
  }
}

Types

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;
}