DEV Community

Connie Leung
Connie Leung

Posted on • Edited on • Originally published at blueskyconnie.com

2

Passing Data to Routed Components with RouterOutletData in Angular 19

Angular 19 introduces a convenient way to pass data to routed components using the routerOutletData input of the RouterOutlet directive. This feature simplifies data sharing between parent and child components within the routing hierarchy, enhancing component communication and reducing boilerplate code.

The Problem It Solves

Previously, passing data to routed components often involved complex workarounds, such as using route parameters, query parameters, shared services, router data, route resolver function, and state management libraries. These methods could become cumbersome, especially when dealing with complex data structures or nested routing scenarios. The routerOutletData input provides a more direct and intuitive way to pass data, improving code readability and maintainability.
Here are some standard methods and their drawbacks:

  • Path Parameter: Requires the data to be part of the URL structure, making it less suitable for complex or non-identifying data.
  • Query Parameter: Exposes data in the URL, which might not be desirable for sensitive information and can become unwieldy with large data sets.
  • Services: Introduces a dependency on a shared service, potentially leading to increased complexity and a need for careful state management, especially in larger applications.
  • Router Data: Primarily designed for static or route-specific configuration, not ideal for dynamic data passed during navigation.
  • Router Function for Prefetching Data: While useful for loading data before navigation, it does not directly pass data from a parent component to its routed child in the same way as routerOutletData. A router function can return either a promise or an observable. If the function returns an observable, it is crucial that the observable completes. If it doesn't complete, the application can freeze, showing a loading screen, and preventing the user from navigating to the intended page.
  • State Management Library (e.g., NgRx): It can be an overkill for simple data transfer between parent and child routes, adding significant complexity for a relatively narrow use case.

The routerOutletData input addresses these drawbacks by providing a straightforward, type-safe, efficient, and reactive way to pass data directly from a parent component to its routed child.

Example: Passing Data Through Nested Routes

Let's illustrate the usage of routerOutletData with a practical example. We will create three standalone components: StarwarsListComponent, StarWarsCharacterComponent, and StarWarsMoviesComponent. The StarWarsListComponent component will pass data to the StarWarsCharacterComponent, and the StarWarsCharacterComponent will further pass data to the StarWarsMoviesComponent, demonstrating data flow through nested routes.

1. Project Setup and Routing:

First, let’s define our routes in app.routes.ts, starwars-list.routes.ts, and starwars-character.routes.ts.

// app.routes.ts

export const routes: Routes = [
   {
       path: 'starwars',
       loadChildren: () => import('./starwars/routes/starwars-list.routes')
   }
];
Enter fullscreen mode Exit fullscreen mode

The routes array uses the loadChildern property to lazy load the child routes in the starwars-list.routes.ts file

// starwars-list.routes.ts

const starWarsListRoutes: Routes = [
   {
       path: '',
       component: StarwarsListComponent,
       children: [
           {
               path: ':id',
               title: 'Star Wars Fighter',
               loadChildren: () => import ('./starwars-character.routes'),
           }
       ]
   }
];

export default starWarsListRoutes;
Enter fullscreen mode Exit fullscreen mode

When the route path is starwars, the application navigates to the StarwarsListComponent. When the route path is starwars/:id, the child routes in the starwars-character.routes.ts file are lazily loaded.

// starwars-character.routes.ts

const starWarsCharacterRoutes: Routes = [
   {
       path: '',
       component: StarwarsCharacterComponent,
       children: [
           {
               path: 'films',
               loadComponent: () => import('../starwars-movies/starwars-movies.component'),
           }
       ]
   }
];

export default starWarsCharacterRoutes;
Enter fullscreen mode Exit fullscreen mode

The application displays the StarwarsCharacterComponent when the route path is starwars/:id and lazily loads the StarwarsMoviesComponent when the route path is starwars/:id/films.

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
  ],
};

bootstrapApplication(App, appConfig);
Enter fullscreen mode Exit fullscreen mode

The appConfig provides the route configuration to the application and the bootStrapApplication function bootstraps the App component and the app configurations.

2. App Component (Great Grandparent):

export type FighterList = {
 ids: number[];
 isSith: boolean;
}

@Component({
 selector: 'app-root',
 imports: [RouterOutlet, FormsModule],
 template: `
     <div>
       Select your allegiance:<br/>
       <input type="radio" id="jedi" name="Jedi" value="jedi" [(ngModel)]="allegiance">
       <label for="jedi">Jedi</label><br/>
       <input type="radio" id="sith" name="Sith" value="sith" [(ngModel)]="allegiance">
       <label for="sith">Sith</label><br/>
     </div>
     Selected allegiance: {{ allegiance() }}
     <router-outlet [routerOutletData]="fighterIds()" />
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
 allegiance = signal('jedi');

 // When selecting the radio button, calculates the star wars character ids  
 fighterIds = computed<FighterList>(() => {
   if (this.allegiance() == 'jedi') {
     return { ids: [1, 10, 20, 51, 52, 53, 32], isSith: false };
   }
   return { ids: [4,44, 21, 67], isSith: true };
 });
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • In App component, when users change the allegiance signal, the fighterIds computed signal calculates the character ids.
  • The routerOutletData input of the RouterOutlet is bound to the fighterIds computed signal. The data is to be passed to the StarwarsListComponent.

3. StarwarsListCharacter (Grandparent):

export type StarWarsCharacter = {
   id: number;
   name: string;
   gender: string;
}

export type StarWarsCharacterNature = StarWarsCharacter & { isSith: boolean };
Enter fullscreen mode Exit fullscreen mode
@Component({
 selector: 'app-star-wars-card',
 imports: [RouterLink],
 template: `
   <div class="card">
     @let character = c();
     <span>Id: {{ character.id }}</span>
     <a  [routerLink]="[character.id]" (click)="showFighter.emit(character)"
     >{{ c().name }}</a>
   </div>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StarWarsCardComponent {
 c = input.required<StarWarsCharacterNature>();
 selectedFighter = signal<StarWarsCharacterNature | undefined>(undefined);
 showFighter = output<StarWarsCharacterNature>();
}
Enter fullscreen mode Exit fullscreen mode
@Component({
 selector: 'app-starwars-list',
 imports: [StarWarsCardComponent, RouterOutlet],
 template: `
   <div>
     <h3 class="fighters">Fighters</h3>
     @if (charactersResource.hasValue()) {
       @for (c of charactersResource.value(); track c.id) {
         <app-star-wars-card [c]="c"
             (showFighter)="selectedFighter.set($event)" />
       }
       <router-outlet [routerOutletData]="selectedFighter()" />
     }
   </div>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class StarwarsListComponent {
  // inject the token to obtain the signal
  fighterList = inject(ROUTER_OUTLET_DATA) as Signal<FighterList>;
  starWarsCharactersService = inject(StarWarsCharactersService);

  // Use the Resource API to retrieve the characters by ids
  charactersResource = rxResource({
   request: () => this.fighterList(),
   loader: ({ request }) =>
     this.starWarsCharactersService.retrieveCharacters(request),
   defaultValue: [] as StarWarsCharacterNature[]
  });

  selectedFighter = signal<StarWarsCharacterNature | undefined>(undefined);
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • In StarwarsListComponent, the ROUTER_OUTLET_DATA token is injected to retrieve the fighterList from the App component. The injected value is cast to the expected type Signal<FighterList>.
  • The characterResource uses the fighterList to call the Star Wars API to retrieve characters and display them in the StarWarsCardComponent component.
  • Then, the StarwarsListComponent passes the selectedFighter signal to the StarWarsCharacterComponent.

4. StarwarsCharacterComponent (Parent):

@Component({
 selector: 'app-starwars-character',
 imports: [RouterLink, RouterOutlet],
 template: `
   <div>
     @if (fighter(); as f) {
       <div style="border: 1px solid black;">
           <div class="character">
             <p>Name: {{ f.name }}</p>
             <p>Gender: {{ f.gender }}</p>
             <p>Is Sith? {{ f.isSith }}</p>
           </div>
           <a [routerLink]="['films']" >Show Films</a>
       </div>
       <router-outlet [routerOutletData]="films()" />
     }
   </div>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class StarwarsCharacterComponent {
 fighter = inject(ROUTER_OUTLET_DATA) as Signal<StarWarsCharacterNature>;
 films = computed(() => this.fighter().films);
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The StarwarsCharacterComponent component similarly injects the ROUTER_OUTLET_DATA token to access the Star Wars character from the StarwarsListComponent component.
  • The films computed signal obtains the films array of the Star Wars character and passes it to the routerOutletData of the routerOutlet directive
  • If StarwarsCharacterComponent has a routed child component, it can inject the films for further processing.

5. StarwarsMoviesComponent (Child):

@Component({
 selector: 'app-starwars-movies',
 imports: [],
 template: `
   <div>
     <h3 class="movies">Movies</h3>
     @if (moviesResource.hasValue()) {
         @for (c of moviesResource.value(); track c.title) {
           <p>Title: {{ c.title }}</p>
           <p>Episode: {{ c.episodeId }}</p>
           <p>Release Date: {{ c.releaseDate }}</p>
           <p>Opening Crawl: {{ c.openingCrawl }}</p>
           <hr />
         }
     }
   </div>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class StarwarsMoviesComponent {
 films = inject(ROUTER_OUTLET_DATA) as Signal<string[]>;
 movieService = inject(StarWarsMoviesService);

 moviesResource = rxResource({
   request: () => this.films(),
   loader: ({ request }) =>
     this.movieService.retrieveMovies(request),
   defaultValue: [] as StarWarsMovie[]
 });
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The StarwarsMoviesComponent component similarly injects the ROUTER_OUTLET_DATA token to access the film URLs from the StarwarsCharacterComponent component.
  • The moviesResource resource uses the films signal to make HTTP requests to retrieve the film details. Then, the film details is displayed in the inline template.

Key Benefits

  • Simplified Data Sharing: routerOutletData provides a direct and clean way to pass data between parent and child routed components.
  • Improved Readability: Reduces boilerplate code and makes routing logic more understandable.
  • Enhanced Maintainability: Centralizes data passing within the routing configuration.
  • Type Safety: Using Signal and casting the injected data with the expected type, provides type safety.
  • Component Flexibility: The parent component does not need to know about the details of the routed component. The parent component decides the data available to the routed component and passes the data to it. If the child needs the data, it will inject the ROUTER_OUTLET_DATA token. Otherwise, it ignores the routedOutletData input.

Conclusion

The routerOutletData input in Angular 19 simplifies data sharing in routed components, offering a more efficient and maintainable approach. It is a valuable addition for developers working with complex routing scenarios.

Resources

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (0)

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

DEV shines when you're signed in, unlocking a customized experience with features like dark mode!

Okay