Transform your Angular apps with seamless data flow between components โ From basics to advanced patterns with live examples
Have you ever found yourself scratching your head, wondering how to elegantly pass data from one Angular component to another through routing?
If you're nodding right now, you're not alone. This is one of the most common challenges Angular developers face, especially when building complex single-page applications where components need to communicate effectively.
Imagine you're building an e-commerce app where a user clicks on a product card and needs to see detailed information on the next page. Or perhaps you're working on a dashboard where filter selections from one view should persist when navigating to another. Sound familiar?
What You'll Master by the End of This Article
By the time you finish reading (and coding along!), you'll have a complete toolkit for:
โ
Route Parameters - The fundamental way to pass simple data
โ
Query Parameters - Perfect for optional data and filters
โ
Route State - Passing complex objects without URL pollution
โ
Route Data - Static configuration data for your routes
โ
Resolver Pattern - Pre-loading data before component initialization
โ
Router Outlet Data Binding - Modern Angular v14+ feature for layout communication
Plus, you'll get 6 complete working examples that you can copy, paste, and customize for your own projects!
1. Route Parameters: The Foundation ๐๏ธ
Route parameters are your go-to solution for passing essential data that defines what the page should display. Think of them as the "ID" of your content.
When to Use Route Parameters
- User profiles (
/user/123
) - Product details (
/product/laptop-dell-xps
) - Article pages (
/blog/angular-routing-guide
)
Live Example: User Profile Navigation
// app-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { UserListComponent } from './user-list/user-list.component'; import { UserDetailComponent } from './user-detail/user-detail.component'; const routes: Routes = [ { path: 'users', component: UserListComponent }, { path: 'user/:id', component: UserDetailComponent }, { path: '', redirectTo: '/users', pathMatch: 'full' } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
// user-list.component.ts import { Component } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-user-list', template: ` <div class="user-grid"> <div class="user-card" *ngFor="let user of users" (click)="navigateToUser(user.id)"> <h3>{{user.name}}</h3> <p>{{user.role}}</p> </div> </div> `, styles: [` .user-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; } .user-card { padding: 1rem; border: 1px solid #ddd; border-radius: 8px; cursor: pointer; } .user-card:hover { box-shadow: 0 4px 8px rgba(0,0,0,0.1); } `] }) export class UserListComponent { users = [ { id: 1, name: 'Sarah Johnson', role: 'Frontend Developer' }, { id: 2, name: 'Mike Chen', role: 'Full Stack Developer' }, { id: 3, name: 'Anna Williams', role: 'UI/UX Designer' } ]; constructor(private router: Router) {} navigateToUser(userId: number) { this.router.navigate(['/user', userId]); } }
// user-detail.component.ts import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-user-detail', template: ` <div class="user-profile" *ngIf="user"> <div class="profile-header"> <img [src]="user.avatar" [alt]="user.name" class="avatar"> <div> <h1>{{user.name}}</h1> <p class="role">{{user.role}}</p> </div> </div> <div class="profile-details"> <p><strong>Email:</strong> {{user.email}}</p> <p><strong>Department:</strong> {{user.department}}</p> <p><strong>Experience:</strong> {{user.experience}} years</p> </div> </div> `, styles: [` .profile-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem; } .avatar { width: 80px; height: 80px; border-radius: 50%; } .role { color: #666; font-size: 1.1rem; } .profile-details p { margin: 0.5rem 0; } `] }) export class UserDetailComponent implements OnInit { user: any = null; // Mock user database private userDatabase = { 1: { id: 1, name: 'Sarah Johnson', role: 'Frontend Developer', email: 'sarah@company.com', department: 'Engineering', experience: 5, avatar: 'https://via.placeholder.com/80' }, 2: { id: 2, name: 'Mike Chen', role: 'Full Stack Developer', email: 'mike@company.com', department: 'Engineering', experience: 7, avatar: 'https://via.placeholder.com/80' }, 3: { id: 3, name: 'Anna Williams', role: 'UI/UX Designer', email: 'anna@company.com', department: 'Design', experience: 4, avatar: 'https://via.placeholder.com/80' } }; constructor(private route: ActivatedRoute) {} ngOnInit() { // Get the user ID from route parameters const userId = Number(this.route.snapshot.paramMap.get('id')); this.user = this.userDatabase[userId as keyof typeof this.userDatabase]; } }
๐ก Pro Tip
Always validate route parameters! Users can manually edit URLs, so check if the ID exists before trying to use it.
2. Query Parameters: The Flexible Option ๐ง
Query parameters are perfect for optional data, filters, and search criteria that don't define the core content but enhance the user experience.
When to Use Query Parameters
- Search filters (
/products?category=electronics&price=100-500
) - Pagination (
/articles?page=2&limit=10
) - Optional preferences (
/dashboard?theme=dark&layout=grid
)
Live Example: Product Search with Filters
// product-search.component.ts import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-product-search', template: ` <div class="search-container"> <div class="filters"> <h3>Filters</h3> <div class="filter-group"> <label>Category:</label> <select [(ngModel)]="filters.category" (change)="applyFilters()"> <option value="">All Categories</option> <option value="electronics">Electronics</option> <option value="clothing">Clothing</option> <option value="books">Books</option> </select> </div> <div class="filter-group"> <label>Price Range:</label> <select [(ngModel)]="filters.priceRange" (change)="applyFilters()"> <option value="">Any Price</option> <option value="0-50">$0 - $50</option> <option value="50-100">$50 - $100</option> <option value="100-200">$100 - $200</option> </select> </div> <div class="filter-group"> <label>Sort By:</label> <select [(ngModel)]="filters.sortBy" (change)="applyFilters()"> <option value="name">Name</option> <option value="price">Price</option> <option value="rating">Rating</option> </select> </div> </div> <div class="products"> <div class="product-card" *ngFor="let product of filteredProducts"> <h4>{{product.name}}</h4> <p class="price">\${{product.price}}</p> <p class="category">{{product.category}}</p> <div class="rating">โญ {{product.rating}}/5</div> </div> </div> </div> `, styles: [` .search-container { display: grid; grid-template-columns: 250px 1fr; gap: 2rem; } .filters { padding: 1rem; border: 1px solid #ddd; border-radius: 8px; height: fit-content; } .filter-group { margin-bottom: 1rem; } .filter-group label { display: block; margin-bottom: 0.5rem; font-weight: bold; } .filter-group select { width: 100%; padding: 0.5rem; } .products { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; } .product-card { padding: 1rem; border: 1px solid #ddd; border-radius: 8px; } .price { font-size: 1.2rem; font-weight: bold; color: #007bff; } .category { color: #666; text-transform: capitalize; } `] }) export class ProductSearchComponent implements OnInit { filters = { category: '', priceRange: '', sortBy: 'name' }; allProducts = [ { id: 1, name: 'Laptop Pro', price: 1299, category: 'electronics', rating: 4.5 }, { id: 2, name: 'Wireless Headphones', price: 199, category: 'electronics', rating: 4.2 }, { id: 3, name: 'Cotton T-Shirt', price: 25, category: 'clothing', rating: 4.0 }, { id: 4, name: 'JavaScript Guide', price: 35, category: 'books', rating: 4.8 }, { id: 5, name: 'Running Shoes', price: 89, category: 'clothing', rating: 4.3 }, { id: 6, name: 'Smartphone', price: 699, category: 'electronics', rating: 4.6 } ]; filteredProducts = [...this.allProducts]; constructor( private router: Router, private route: ActivatedRoute ) {} ngOnInit() { // Read query parameters on component initialization this.route.queryParams.subscribe(params => { this.filters.category = params['category'] || ''; this.filters.priceRange = params['priceRange'] || ''; this.filters.sortBy = params['sortBy'] || 'name'; this.filterProducts(); }); } applyFilters() { // Update URL with new query parameters this.router.navigate([], { relativeTo: this.route, queryParams: { category: this.filters.category || null, priceRange: this.filters.priceRange || null, sortBy: this.filters.sortBy }, queryParamsHandling: 'merge' }); } private filterProducts() { let filtered = [...this.allProducts]; // Filter by category if (this.filters.category) { filtered = filtered.filter(p => p.category === this.filters.category); } // Filter by price range if (this.filters.priceRange) { const [min, max] = this.filters.priceRange.split('-').map(Number); filtered = filtered.filter(p => p.price >= min && p.price <= max); } // Sort products filtered.sort((a, b) => { switch (this.filters.sortBy) { case 'price': return a.price - b.price; case 'rating': return b.rating - a.rating; default: return a.name.localeCompare(b.name); } }); this.filteredProducts = filtered; } }
๐ฏ Key Insight
Query parameters automatically update the browser's URL, making your filters bookmarkable and shareable. Users can copy the URL and return to the exact same filtered view later!
3. Route State: Passing Complex Objects ๐ฆ
Sometimes you need to pass complex data objects without cluttering the URL. Route state is perfect for this scenario.
When to Use Route State
- Complex form data between steps
- Objects with sensitive information
- Large datasets that shouldn't be in the URL
Live Example: Multi-Step Form Navigation
// form-step1.component.ts import { Component } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-form-step1', template: ` <div class="form-container"> <h2>Step 1: Personal Information</h2> <form (ngSubmit)="goToStep2()"> <div class="form-group"> <label>First Name:</label> <input type="text" [(ngModel)]="formData.firstName" name="firstName" required> </div> <div class="form-group"> <label>Last Name:</label> <input type="text" [(ngModel)]="formData.lastName" name="lastName" required> </div> <div class="form-group"> <label>Email:</label> <input type="email" [(ngModel)]="formData.email" name="email" required> </div> <div class="form-group"> <label>Phone:</label> <input type="tel" [(ngModel)]="formData.phone" name="phone" required> </div> <button type="submit" class="next-btn">Next Step โ</button> </form> </div> `, styles: [` .form-container { max-width: 500px; margin: 2rem auto; padding: 2rem; border: 1px solid #ddd; border-radius: 8px; } .form-group { margin-bottom: 1rem; } .form-group label { display: block; margin-bottom: 0.5rem; font-weight: bold; } .form-group input { width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; } .next-btn { background: #007bff; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; } `] }) export class FormStep1Component { formData = { firstName: '', lastName: '', email: '', phone: '', preferences: { notifications: true, newsletter: false, theme: 'light' }, metadata: { startTime: new Date(), userAgent: navigator.userAgent, referrer: document.referrer } }; constructor(private router: Router) {} goToStep2() { // Navigate with complex state object this.router.navigate(['/form/step2'], { state: { formData: this.formData } }); } }
// form-step2.component.ts import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-form-step2', template: ` <div class="form-container"> <h2>Step 2: Preferences</h2> <div class="user-info" *ngIf="formData"> <p><strong>Welcome, {{formData.firstName}} {{formData.lastName}}!</strong></p> <p>Email: {{formData.email}}</p> </div> <form (ngSubmit)="submitForm()"> <div class="form-group"> <label> <input type="checkbox" [(ngModel)]="formData.preferences.notifications" name="notifications"> Enable push notifications </label> </div> <div class="form-group"> <label> <input type="checkbox" [(ngModel)]="formData.preferences.newsletter" name="newsletter"> Subscribe to newsletter </label> </div> <div class="form-group"> <label>Theme Preference:</label> <select [(ngModel)]="formData.preferences.theme" name="theme"> <option value="light">Light</option> <option value="dark">Dark</option> <option value="auto">Auto</option> </select> </div> <div class="button-group"> <button type="button" (click)="goBack()" class="back-btn">โ Back</button> <button type="submit" class="submit-btn">Complete Registration</button> </div> </form> </div> `, styles: [` .form-container { max-width: 500px; margin: 2rem auto; padding: 2rem; border: 1px solid #ddd; border-radius: 8px; } .user-info { background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-bottom: 1rem; } .form-group { margin-bottom: 1rem; } .form-group label { display: block; margin-bottom: 0.5rem; } .form-group input[type="checkbox"] { margin-right: 0.5rem; } .form-group select { width: 100%; padding: 0.5rem; } .button-group { display: flex; gap: 1rem; justify-content: space-between; } .back-btn { background: #6c757d; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; } .submit-btn { background: #28a745; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; } `] }) export class FormStep2Component implements OnInit { formData: any = null; constructor(private router: Router) {} ngOnInit() { // Access the state passed from the previous route const navigation = this.router.getCurrentNavigation(); if (navigation?.extras.state) { this.formData = navigation.extras.state['formData']; } else { // Handle case where user directly accessed this URL console.warn('No form data found. Redirecting to step 1.'); this.router.navigate(['/form/step1']); } } goBack() { this.router.navigate(['/form/step1'], { state: { formData: this.formData } }); } submitForm() { console.log('Form submitted:', this.formData); // Here you would typically send data to your backend alert('Registration completed successfully!'); this.router.navigate(['/dashboard']); } }
โ ๏ธ Important Note
Route state data is only available during navigation and doesn't persist on page refresh. For data that needs to survive page reloads, consider using services with localStorage or sessionStorage.
4. Route Data: Static Configuration Made Easy โ๏ธ
Route data is perfect for passing static configuration information that doesn't change based on user interaction.
When to Use Route Data
- Page titles and metadata
- Permission levels
- Feature flags
- Static configuration
Live Example: Dynamic Page Configuration
// app-routing.module.ts const routes: Routes = [ { path: 'dashboard', component: DashboardComponent, data: { title: 'Dashboard', breadcrumb: 'Home > Dashboard', requiredRole: 'user', showSidebar: true, theme: 'default' } }, { path: 'admin', component: AdminComponent, data: { title: 'Admin Panel', breadcrumb: 'Home > Admin', requiredRole: 'admin', showSidebar: false, theme: 'dark' } }, { path: 'profile', component: ProfileComponent, data: { title: 'User Profile', breadcrumb: 'Home > Profile', requiredRole: 'user', showSidebar: true, theme: 'default', features: { canEditProfile: true, canDeleteAccount: true, canExportData: false } } } ];
// base-page.component.ts import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-base-page', template: ` <div class="page-container" [class]="pageConfig?.theme"> <div class="page-header"> <nav class="breadcrumb">{{pageConfig?.breadcrumb}}</nav> <h1>{{pageConfig?.title}}</h1> </div> <div class="page-content" [class.with-sidebar]="pageConfig?.showSidebar"> <aside class="sidebar" *ngIf="pageConfig?.showSidebar"> <ul class="nav-menu"> <li><a routerLink="/dashboard">Dashboard</a></li> <li><a routerLink="/profile">Profile</a></li> <li *ngIf="isAdmin"><a routerLink="/admin">Admin</a></li> </ul> </aside> <main class="main-content"> <ng-content></ng-content> </main> </div> </div> `, styles: [` .page-container.dark { background: #2d3748; color: white; } .page-header { padding: 1rem; border-bottom: 1px solid #e2e8f0; } .breadcrumb { font-size: 0.9rem; color: #718096; margin-bottom: 0.5rem; } .page-content.with-sidebar { display: grid; grid-template-columns: 250px 1fr; } .sidebar { background: #f7fafc; padding: 1rem; border-right: 1px solid #e2e8f0; } .nav-menu { list-style: none; padding: 0; } .nav-menu li { margin-bottom: 0.5rem; } .nav-menu a { text-decoration: none; color: #4a5568; padding: 0.5rem; display: block; border-radius: 4px; } .nav-menu a:hover { background: #e2e8f0; } .main-content { padding: 2rem; } `] }) export class BasePageComponent implements OnInit { pageConfig: any = null; isAdmin = false; // This would typically come from your auth service constructor(private route: ActivatedRoute) {} ngOnInit() { // Access static route data this.pageConfig = this.route.snapshot.data; // Set page title dynamically if (this.pageConfig?.title) { document.title = `MyApp - ${this.pageConfig.title}`; } // Check user permissions (mock implementation) this.checkUserPermissions(); } private checkUserPermissions() { // Mock user role check const userRole = 'user'; // This would come from your auth service if (this.pageConfig?.requiredRole && userRole !== this.pageConfig.requiredRole) { console.warn('User does not have required permissions'); // Handle unauthorized access } this.isAdmin = userRole === 'admin'; } }
5. Resolver Pattern: Pre-loading Data Like a Pro ๐
Resolvers are the most advanced technique - they allow you to load data before the component initializes, ensuring your users never see empty loading states.
When to Use Resolvers
- Critical data that must be available immediately
- Data that determines what the component should display
- When you want to handle loading states at the route level
Live Example: User Profile with Data Pre-loading
// user.service.ts import { Injectable } from '@angular/core'; import { Observable, of, delay } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class UserService { private users = [ { id: 1, name: 'Sarah Johnson', email: 'sarah@example.com', role: 'Developer', projects: ['Project A', 'Project B'] }, { id: 2, name: 'Mike Chen', email: 'mike@example.com', role: 'Designer', projects: ['Project C', 'Project D'] }, { id: 3, name: 'Anna Williams', email: 'anna@example.com', role: 'Manager', projects: ['Project E'] } ]; getUserById(id: number): Observable<any> { // Simulate API call with delay const user = this.users.find(u => u.id === id); return of(user).pipe(delay(1000)); // Simulate network delay } getUserProjects(userId: number): Observable<any[]> { const user = this.users.find(u => u.id === userId); const projects = user?.projects.map(name => ({ name, status: Math.random() > 0.5 ? 'Active' : 'Completed', progress: Math.floor(Math.random() * 100) })) || []; return of(projects).pipe(delay(800)); } }
// user-resolver.service.ts import { Injectable } from '@angular/core'; import { Resolve, ActivatedRouteSnapshot } from '@angular/router'; import { Observable, forkJoin } from 'rxjs'; import { UserService } from './user.service'; @Injectable({ providedIn: 'root' }) export class UserResolver implements Resolve<any> { constructor(private userService: UserService) {} resolve(route: ActivatedRouteSnapshot): Observable<any> { const userId = Number(route.paramMap.get('id')); // Load multiple pieces of data in parallel return forkJoin({ user: this.userService.getUserById(userId), projects: this.userService.getUserProjects(userId) }); } }
// Updated routing with resolver const routes: Routes = [ { path: 'user/:id', component: UserDetailComponent, resolve: { userData: UserResolver }, data: { title: 'User Profile', showLoadingSpinner: true } } ];
// user-detail-with-resolver.component.ts import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-user-detail-resolver', template: ` <div class="user-profile"> <div class="profile-header"> <h1>{{user.name}}</h1> <p class="role">{{user.role}}</p> <p class="email">{{user.email}}</p> </div> <div class="projects-section"> <h2>Projects ({{projects.length}})</h2> <div class="projects-grid"> <div class="project-card" *ngFor="let project of projects"> <h3>{{project.name}}</h3> <div class="project-status" [class]="project.status.toLowerCase()"> {{project.status}} </div> <div class="progress-bar"> <div class="progress-fill" [style.width.%]="project.progress"></div> </div> <span class="progress-text">{{project.progress}}% Complete</span> </div> </div> </div> </div> `, styles: [` .user-profile { max-width: 800px; margin: 2rem auto; padding: 2rem; } .profile-header { text-align: center; margin-bottom: 3rem; padding-bottom: 2rem; border-bottom: 1px solid #e2e8f0; } .role { font-size: 1.2rem; color: #4a5568; margin: 0.5rem 0; } .email { color: #718096; } .projects-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; } .project-card { padding: 1.5rem; border: 1px solid #e2e8f0; border-radius: 8px; } .project-status { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.8rem; font-weight: bold; } .project-status.active { background: #c6f6d5; color: #22543d; } .project-status.completed { background: #bee3f8; color: #2a4365; } .progress-bar { width: 100%; height: 8px; background: #e2e8f0; border-radius: 4px; margin: 1rem 0 0.5rem; overflow: hidden; } .progress-fill { height: 100%; background: #4299e1; transition: width 0.3s ease; } .progress-text { font-size: 0.9rem; color: #718096; } `] }) export class UserDetailWithResolverComponent implements OnInit { user: any = {}; projects: any[] = []; constructor(private route: ActivatedRoute) {} ngOnInit() { // Data is already loaded by the resolver! const resolvedData = this.route.snapshot.data['userData']; this.user = resolvedData.user; this.projects = resolvedData.projects; // No loading states needed - data is immediately available console.log('User data loaded:', this.user); console.log('Projects loaded:', this.projects); } }
๐ฅ Pro Benefits of Resolvers
- No loading states - Users see complete data immediately
- Error handling at route level - Failed data loads can redirect to error pages
- Better UX - No flickering or partial content rendering
- SEO friendly - Search engines see complete content on first render
6. Router Outlet Data Binding: Angular v14+ Game Changer ๐
This is the newest and most elegant solution! Starting from Angular v14, you can directly pass data to child components through the <router-outlet>
element using property binding. This approach is perfect for layout-based communication and eliminates the need for complex services or route metadata.
When to Use Router Outlet Data Binding
- Passing layout-specific data (theme, user info, permissions)
- Sharing parent component state with routed children
- Communication between shell/layout and feature components
- Dynamic configuration that changes based on parent state
Live Example: Dynamic Layout with Theme and User Context
// app.component.ts (Parent/Shell Component) import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-root', template: ` <div class="app-shell" [class]="currentTheme"> <header class="app-header"> <div class="header-content"> <h1>MyApp</h1> <div class="user-controls" *ngIf="currentUser"> <span>Welcome, {{currentUser.name}}!</span> <button (click)="toggleTheme()" class="theme-toggle"> {{currentTheme === 'dark' ? 'โ๏ธ' : '๐'}} </button> <button (click)="logout()" class="logout-btn">Logout</button> </div> </div> </header> <nav class="app-nav"> <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a> <a routerLink="/profile" routerLinkActive="active">Profile</a> <a routerLink="/settings" routerLinkActive="active">Settings</a> </nav> <main class="app-main"> <!-- ๐ฏ This is the magic! Direct data binding to router-outlet --> <router-outlet [data]="{ user: currentUser, theme: currentTheme, layoutConfig: layoutConfig, permissions: userPermissions, notificationCount: notificationCount }"> </router-outlet> </main> <footer class="app-footer"> <p>© 2024 MyApp. Current theme: {{currentTheme}}</p> </footer> </div> `, styles: [` .app-shell { min-height: 100vh; display: flex; flex-direction: column; } .app-shell.dark { background: #1a202c; color: white; } .app-shell.light { background: #f7fafc; color: #2d3748; } .app-header { background: #4299e1; color: white; padding: 1rem; } .header-content { display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; } .user-controls { display: flex; gap: 1rem; align-items: center; } .theme-toggle, .logout-btn { background: transparent; border: 1px solid white; color: white; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; } .theme-toggle:hover, .logout-btn:hover { background: rgba(255,255,255,0.1); } .app-nav { background: #3182ce; padding: 1rem; } .app-nav a { color: white; text-decoration: none; padding: 0.5rem 1rem; margin-right: 1rem; border-radius: 4px; } .app-nav a.active, .app-nav a:hover { background: rgba(255,255,255,0.1); } .app-main { flex: 1; padding: 2rem; max-width: 1200px; margin: 0 auto; width: 100%; } .app-footer { background: #e2e8f0; text-align: center; padding: 1rem; } .app-shell.dark .app-footer { background: #2d3748; } `] }) export class AppComponent implements OnInit { currentUser = { id: 1, name: 'Sarah Johnson', email: 'sarah@example.com', role: 'admin', avatar: 'https://via.placeholder.com/40' }; currentTheme = 'light'; notificationCount = 3; layoutConfig = { showSidebar: true, compactMode: false, animationsEnabled: true }; userPermissions = { canEdit: true, canDelete: true, canExport: false, isAdmin: true }; ngOnInit() { // Load user preferences, theme, etc. this.loadUserPreferences(); } toggleTheme() { this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light'; // Save theme preference localStorage.setItem('theme', this.currentTheme); } logout() { this.currentUser = null; // Handle logout logic } private loadUserPreferences() { // Load saved theme const savedTheme = localStorage.getItem('theme'); if (savedTheme) { this.currentTheme = savedTheme; } } }
// dashboard.component.ts (Child Component) import { Component, Input, OnInit } from '@angular/core'; @Component({ selector: 'app-dashboard', template: ` <div class="dashboard" [class]="layoutData?.theme"> <div class="welcome-section"> <h2>Dashboard</h2> <p *ngIf="layoutData?.user"> Welcome back, <strong>{{layoutData.user.name}}</strong>! <span class="notification-badge" *ngIf="layoutData?.notificationCount > 0"> {{layoutData.notificationCount}} new notifications </span> </p> </div> <div class="dashboard-grid" [class.compact]="layoutData?.layoutConfig?.compactMode"> <!-- Stats Cards --> <div class="stat-card"> <h3>Total Projects</h3> <div class="stat-number">12</div> </div> <div class="stat-card"> <h3>Active Tasks</h3> <div class="stat-number">28</div> </div> <div class="stat-card" *ngIf="layoutData?.permissions?.isAdmin"> <h3>Team Members</h3> <div class="stat-number">15</div> </div> <!-- Quick Actions --> <div class="action-card"> <h3>Quick Actions</h3> <div class="action-buttons"> <button class="action-btn" *ngIf="layoutData?.permissions?.canEdit"> ๐ Create New Project </button> <button class="action-btn" *ngIf="layoutData?.permissions?.canExport"> ๐ Export Data </button> <button class="action-btn" *ngIf="layoutData?.permissions?.isAdmin"> ๐ฅ Manage Team </button> </div> </div> <!-- Recent Activity --> <div class="activity-card"> <h3>Recent Activity</h3> <div class="activity-list"> <div class="activity-item"> <span class="activity-time">2 hours ago</span> <span class="activity-text">Project Alpha updated</span> </div> <div class="activity-item"> <span class="activity-time">5 hours ago</span> <span class="activity-text">New team member added</span> </div> </div> </div> </div> <!-- Debug Panel (remove in production) --> <div class="debug-panel" *ngIf="showDebug"> <h4>๐ Layout Data Debug</h4> <pre>{{layoutData | json}}</pre> </div> </div> `, styles: [` .dashboard { padding: 1rem; } .welcome-section { margin-bottom: 2rem; } .notification-badge { background: #e53e3e; color: white; padding: 0.25rem 0.5rem; border-radius: 12px; font-size: 0.8rem; margin-left: 1rem; } .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; } .dashboard-grid.compact { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; } .stat-card, .action-card, .activity-card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .dashboard.dark .stat-card, .dashboard.dark .action-card, .dashboard.dark .activity-card { background: #2d3748; } .stat-number { font-size: 2.5rem; font-weight: bold; color: #4299e1; } .action-buttons { display: flex; flex-direction: column; gap: 0.5rem; } .action-btn { background: #4299e1; color: white; border: none; padding: 0.75rem; border-radius: 4px; cursor: pointer; text-align: left; } .action-btn:hover { background: #3182ce; } .activity-item { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #e2e8f0; } .activity-time { color: #718096; font-size: 0.9rem; } .debug-panel { margin-top: 2rem; padding: 1rem; background: #f7fafc; border-radius: 4px; font-family: monospace; } .dashboard.dark .debug-panel { background: #1a202c; } `] }) export class DashboardComponent implements OnInit { @Input() data: any; // ๐ฏ This receives data from router-outlet! layoutData: any = null; showDebug = false; // Set to true for debugging ngOnInit() { this.layoutData = this.data; console.log('๐ฅ Received layout data:', this.layoutData); } }
// profile.component.ts (Another Child Component) import { Component, Input, OnInit } from '@angular/core'; @Component({ selector: 'app-profile', template: ` <div class="profile-page" [class]="layoutData?.theme"> <div class="profile-header"> <img [src]="layoutData?.user?.avatar" [alt]="layoutData?.user?.name" class="profile-avatar"> <div class="profile-info"> <h1>{{layoutData?.user?.name}}</h1> <p class="profile-email">{{layoutData?.user?.email}}</p> <span class="profile-role">{{layoutData?.user?.role | titlecase}}</span> </div> </div> <div class="profile-sections"> <section class="profile-section"> <h2>Account Settings</h2> <div class="setting-item"> <label>Theme Preference</label> <span class="current-value">{{layoutData?.theme | titlecase}}</span> </div> <div class="setting-item"> <label>Compact Mode</label> <span class="current-value">{{layoutData?.layoutConfig?.compactMode ? 'Enabled' : 'Disabled'}}</span> </div> </section> <section class="profile-section" *ngIf="layoutData?.permissions?.isAdmin"> <h2>Admin Settings</h2> <p>๐ You have administrator privileges</p> <div class="admin-actions"> <button class="admin-btn">Manage Users</button> <button class="admin-btn">System Settings</button> </div> </section> <section class="profile-section"> <h2>Notifications</h2> <p *ngIf="layoutData?.notificationCount > 0"> You have {{layoutData.notificationCount}} unread notifications </p> <p *ngIf="layoutData?.notificationCount === 0"> No new notifications </p> </section> </div> </div> `, styles: [` .profile-page { max-width: 800px; margin: 0 auto; } .profile-header { display: flex; align-items: center; gap: 1.5rem; margin-bottom: 2rem; padding: 2rem; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .profile-page.dark .profile-header { background: #2d3748; } .profile-avatar { width: 80px; height: 80px; border-radius: 50%; } .profile-email { color: #718096; margin: 0.5rem 0; } .profile-role { background: #4299e1; color: white; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.8rem; } .profile-sections { display: grid; gap: 1.5rem; } .profile-section { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .profile-page.dark .profile-section { background: #2d3748; } .setting-item { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 0; border-bottom: 1px solid #e2e8f0; } .setting-item:last-child { border-bottom: none; } .current-value { color: #4299e1; font-weight: 500; } .admin-actions { display: flex; gap: 1rem; margin-top: 1rem; } .admin-btn { background: #e53e3e; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; } .admin-btn:hover { background: #c53030; } `] }) export class ProfileComponent implements OnInit { @Input() data: any; // ๐ฏ Receives the same data from router-outlet! layoutData: any = null; ngOnInit() { this.layoutData = this.data; console.log('๐ฅ Profile received layout data:', this.layoutData); } }
๐ฏ Key Benefits of Router Outlet Data Binding
- No Service Dependencies - Direct parent-to-child communication
- Real-time Updates - Changes in parent immediately reflect in child
- Type Safety - Can use TypeScript interfaces for data contracts
- Cleaner Architecture - Eliminates complex state management for layout data
- Better Performance - No need for subject/observable patterns for simple data
๐ Comparison with Traditional Approaches
// โ Old way: Using a service @Injectable() export class LayoutService { private themeSubject = new BehaviorSubject('light'); theme$ = this.themeSubject.asObservable(); setTheme(theme: string) { this.themeSubject.next(theme); } } // Child component needs to inject and subscribe constructor(private layoutService: LayoutService) {} ngOnInit() { this.layoutService.theme$.subscribe(theme => { this.currentTheme = theme; }); }
// โ
New way: Direct data binding // Parent: <router-outlet [data]="{ theme: currentTheme }"></router-outlet> // Child: @Input() data: any; // ngOnInit() { this.currentTheme = this.data.theme; }
๐ก Pro Tips for Router Outlet Data Binding
- Use TypeScript interfaces for better type safety:
interface LayoutData { user: User; theme: 'light' | 'dark'; permissions: UserPermissions; } @Input() data: LayoutData;
- Handle undefined data gracefully:
ngOnInit() { if (this.data) { this.layoutData = this.data; } else { console.warn('No layout data received'); } }
- Combine with OnPush strategy for better performance:
@Component({ changeDetection: ChangeDetectionStrategy.OnPush, // ... })
Real-World Performance Tips ๐
1. Combine Techniques Strategically
// Powerful combination example this.router.navigate(['/dashboard'], { queryParams: { tab: 'analytics', period: '30d' }, // For UI state state: { previousFilters: this.currentFilters } // For complex objects });
2. URL-Friendly Data Transformation
// Convert objects to URL-safe strings const filters = { category: 'tech', tags: ['angular', 'javascript'] }; const queryParam = btoa(JSON.stringify(filters)); // Base64 encode // Navigate with encoded data this.router.navigate(['/search'], { queryParams: { f: queryParam } }); // Decode in destination component const decodedFilters = JSON.parse(atob(this.route.snapshot.queryParams['f']));
3. Memory Management Best Practices
import { Component, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; @Component({...}) export class SmartComponent implements OnDestroy { private subscriptions = new Subscription(); ngOnInit() { // Always unsubscribe from route observables this.subscriptions.add( this.route.params.subscribe(params => { // Handle params }) ); this.subscriptions.add( this.route.queryParams.subscribe(queryParams => { // Handle query params }) ); } ngOnDestroy() { this.subscriptions.unsubscribe(); } }
Common Pitfalls to Avoid โ ๏ธ
โ Don't Do This
// Putting sensitive data in URLs this.router.navigate(['/profile'], { queryParams: { password: user.password, // NEVER! creditCard: user.ccNumber // NEVER! } }); // Forgetting to handle missing data ngOnInit() { const userId = this.route.snapshot.params['id']; this.loadUser(userId); // What if userId is undefined? }
โ Do This Instead
// Use route state for sensitive data this.router.navigate(['/profile'], { state: { secureData: encryptedUserData // Much safer } }); // Always validate route parameters ngOnInit() { const userId = this.route.snapshot.params['id']; if (!userId || isNaN(Number(userId))) { this.router.navigate(['/error'], { queryParams: { message: 'Invalid user ID' } }); return; } this.loadUser(Number(userId)); }
Bonus: TypeScript Interfaces for Type Safety ๐ก๏ธ
Make your route data bulletproof with TypeScript:
// Define interfaces for your route data interface UserRouteParams { id: string; } interface ProductFilters { category?: string; priceRange?: string; sortBy: 'name' | 'price' | 'rating'; } interface RouteStateData { formData?: any; returnUrl?: string; preserveFilters?: boolean; } // Use them in your components export class TypeSafeComponent implements OnInit { constructor(private route: ActivatedRoute) {} ngOnInit() { // Type-safe parameter access const params = this.route.snapshot.params as UserRouteParams; const userId = Number(params.id); // Type-safe query parameter access const queryParams = this.route.snapshot.queryParams as ProductFilters; console.log('Sort by:', queryParams.sortBy); } }
Quick Reference Cheat Sheet ๐
Technique | Best For | URL Example | Code Pattern |
---|---|---|---|
Route Params | Essential ID data | /user/123 | route.snapshot.params['id'] |
Query Params | Optional filters | /search?q=angular&sort=date | route.snapshot.queryParams['q'] |
Route State | Complex objects | /form/step2 (hidden data) | router.getCurrentNavigation()?.extras.state |
Route Data | Static config | Any route | route.snapshot.data['title'] |
Resolvers | Pre-loaded data | Any route | route.snapshot.data['userData'] |
Router Outlet Data | Layout communication | Any route | <router-outlet [data]="layoutData"> |
What's Next? ๐
You've just mastered the complete toolkit for Angular route data passing! Here are some advanced topics to explore next:
- Route Guards - Protect your routes with authentication
- Lazy Loading - Improve app performance with code splitting
- Route Animations - Add smooth transitions between pages
- Advanced Resolvers - Handle complex data dependencies
- Route Testing - Unit test your routing logic
๐ฏ Your Turn,ย Devs!
๐ Did this article spark new ideas or help solve a real problem?
๐ฌ I'd love to hear about it!
โ
Are you already using this technique in your Angular or frontend project?
๐ง Got questions, doubts, or your own twist on the approach?
Drop them in the comments belowโ-โlet's learn together!
๐ Let's Grow Together!
If this article added value to your dev journey:
๐ Share it with your team, tech friends, or communityโ-โyou never know who might need it right now.
๐ Save it for later and revisit as a quick reference.
๐ Follow Me for More Angular & Frontend Goodness:
I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.
๐ LinkedInโ-โLet's connect professionally
๐ฅ Threadsโ-โShort-form frontend insights
๐ฆ X (Twitter)โ-โDeveloper banter + code snippets
๐ฅ BlueSkyโ-โStay up to date on frontend trends
๐ฅ๏ธ GitHub Projectsโ-โExplore code in action
๐ Websiteโ-โEverything in one place
๐ Medium Blogโ-โLong-form content and deep-dives
๐ฌ Dev Blogโ-โLong-form content and deep-dives
๐ If you found this article valuable:
Leave a ๐ Clap
Drop a ๐ฌ Comment
Hit ๐ Follow for more weekly frontend insights
Let's build cleaner, faster, and smarter web appsโ-โtogether.
Stay tuned for more Angular tips, patterns, and performance tricks! ๐งช๐ง ๐
Top comments (0)