Angular Named Router Outlet + Popup Example

By Arvind Rai, August 27, 2021
This page will walk through Angular named router outlet and popup example. Named outlet is used to perform a task in popup view. The named outlet will stay open even when we switch between pages in the application. Named outlet will close only when we close it. The component which needs to open in popup view cannot use same router outlet which other pages of application uses. Named outlet is that outlet which is given a name using name attribute in <router-outlet> tag. Router supports only one unnamed outlet per template, that is called primary outlet. Named outlet can be more than one per template. The component which needs to open in named outlet will configure its path with target outlet name in routing module using outlet property of Route interface. More than one named outlet can stay open together. Named outlet uses secondary routes to open a component. Secondary routes are independent of primary route. We can navigate to named outlet using RouterLink directive as well as Router.navigate() method. Named outlet will close only when we pass null path to it. On this page we will provide how to create named outlet, configuring path and navigation. Find the complete example step-by-step.

Technologies Used

Find the technologies being used in our example.
1. Angular 12.1.0
2. Node.js 12.14.1
3. NPM 7.20.3

Project Structure

Find the project structure of our demo application.
angular-demo
|
|--src
|   |
|   |--app 
|   |   |
|   |   |--animations
|   |   |    |
|   |   |    |--on-off.animation.ts
|   |   |    |--round-anticlock.animation.ts
|   |   |    |--fly-in-out.animation.ts
|   |   | 
|   |   |--book.ts
|   |   |--book.service.ts
|   |   |--book.component.ts
|   |   |--book.component.html
|   |   |--addbook.component.ts
|   |   |--addbook.component.html
|   |   |--book-detail.component.ts
|   |   |--book-detail.component.html
|   |   |--book-update.component.ts
|   |   |--book-update.component.html
|   |   |--book-update.component.css
|   |   |--app.component.ts
|   |   |--app-routing.module.ts
|   |   |--app.module.ts 
|   | 
|   |--main.ts
|   |--index.html
|   |--styles.css
|
|--node_modules
|--package.json 

Step-1: Creating Named Outlet

To create a named outlet, <router-outlet> has attribute name that is used as follows.
<router-outlet name="bookList"></router-outlet> 
The above <router-outlet> has outlet name as booklist. We can create more than one named outlet with our primary outlet. Unnamed outlet is primary outlet.
<router-outlet></router-outlet>	
<router-outlet name="bookList"></router-outlet>
<router-outlet name="bookPopup"></router-outlet> 
In the above code snippet, we have three router outlet, out of which two are named outlet whose names are bookList and bookPopup.

Step2: Creating Routes for Named Outlet

To open a route in a named outlet, we need to use outlet property of Route interface. It is used as follows.
outlet: 'bookPopup' 
where bookPopup is the name of named router outlet. Find the code snippet of routing module.
const routes: Routes = [
	{
	   path: 'book',
	   component: BookComponent
	},
	{
	   path: 'add-book',
	   component: AddBookComponent,
	   outlet: 'bookPopup'
	},	
	{
	   path: 'update-book/:book-id',
	   component: BookUpdateComponent,
	   outlet: 'bookPopup'
	},				
	{
	   path: 'book-detail',
	   component: BookDetailComponent,
	   outlet: 'bookList'
	},	
	{
	   path: '',
	   redirectTo: '/book',
	   pathMatch: 'full'
	}	
]; 
In the above route configurations,
1. AddBookComponent will open in bookPopup named outlet.
2. BookUpdateComponent will open in bookPopup named outlet.
3. BookDetailComponent will open in bookList named outlet.
4. BookComponent will open in unnamed outlet i.e. primary outlet.
We can navigate to named outlet using RouterLink directive as well as Router.navigate() method.
A. Using RouterLink directive

1. Path:
{
   path: 'update-book',
   component: BookUpdateComponent,
   outlet: 'bookPopup'
}
Navigation:
<a [routerLink]="[{ outlets: { bookPopup: ['update-book'] } }]" > 
2. Path:
{
   path: 'update-book/:book-id',
   component: BookUpdateComponent,
   outlet: 'bookPopup'
}
Navigation:
<a [routerLink]="[{ outlets: { bookPopup: ['update-book', book.id] } }]" > 
3. Path:
{
   path: ':book-id',
   component: BookUpdateComponent,
   outlet: 'bookPopup'
}
Navigation:
<a [routerLink]="[{ outlets: { bookPopup: [book.id] } }]" >
B. Using Router.navigate() method

1. Path:
{
   path: 'update-book',
   component: BookUpdateComponent,
   outlet: 'bookPopup'
}
Navigation:
this.router.navigate([{ outlets: { bookPopup: [ 'update-book' ] }}]); 
2. Path:
{
   path: 'update-book/:book-id',
   component: BookUpdateComponent,
   outlet: 'bookPopup'
}
Navigation:
this.router.navigate([{ outlets: { bookPopup: [ 'update-book', book.id ] }}]); 
3. Path:
{
   path: ':book-id',
   component: BookUpdateComponent,
   outlet: 'bookPopup'
}
Navigation:
this.router.navigate([{ outlets: { bookPopup: [ book.id ] }}]); 

Step4: Close Named Outlet

Once a named outlet is opened for a route, it can be closed by passing route as null .
close() {
     this.router.navigate([{ outlets: { bookPopup: null }}]);
} 
Because null is not any route, so the outlet will be closed. A named outlet works as a container in which we open our component corresponding to a route. If a component is opened using a route in a named outlet and when we navigate to another route which has same named outlet then the component corresponding to previous route will be removed and component corresponding to new route will open. When null is passed as a path to named outlet, no component opens and hence popup view outlet is closed.

Secondary routes

Named outlet will open as secondary route within a (). Suppose we have opened a route with unnamed outlet and then we visit a route that will open in named outlet, we will observe the path on browser address bar that both path exists there. The named outlet path is called secondary routes and will open in () . Secondary routes will be appended with unnamed router outlet path. All the open routes using primary and named outlet will be activated when using routerLinkActive property. Let us discuss secondary routes for some cases.
Case-1: Visit a path with named outlet
http://localhost:4200/book(bookPopup:add-book) 
The above URL contains the path for two routes. Path for unnamed router outlet:
http://localhost:4200/book 
Path for named router outlet i.e. secondary routes.
(bookPopup:add-book) 
Secondary route has opened a named outlet with name bookPopup and path add-book. Find the print screen.
Angular Named Router Outlet + Popup Example
We will observe that all the open links are activated and named outlet has opened as popup view.
Case-2: Visit paths with two different named outlet.
http://localhost:4200/book(bookList:book-detail//bookPopup:add-book) 
In the above URL, we will observe that secondary route will contain all named outlet which we have opened and not closed. Find the print screen.
Angular Named Router Outlet + Popup Example
We will observe that all the open links are activated and named outlet has opened as popup view.
Case-3: Visit a path which contains parameter in named outlet.
http://localhost:4200/book(bookPopup:update-book/3) 
Find the print screen.
Angular Named Router Outlet + Popup Example
We will observe that named outlet has opened as popup view.

Complete Example

app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
	<nav [ngClass] = "'parent-menu'">
	  <ul>
            <li><a routerLink="/book" routerLinkActive="active">Book</a></li>	  
            <li><a [routerLink]="[{ outlets: { bookPopup: ['add-book'] } }]" routerLinkActive="active">Add Book</a></li>			 
	    <li><a [routerLink]="[{ outlets: { bookList: ['book-detail'] } }]" routerLinkActive="active">Book Details</a></li>	
	  </ul> 
	</nav>  
	<router-outlet></router-outlet>	
	<router-outlet name="bookList"></router-outlet>
	<router-outlet name="bookPopup"></router-outlet>
  `
})
export class AppComponent { 
} 
app-routing.module.ts
import { NgModule }      from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { BookComponent }  from './book.component';
import { AddBookComponent }  from './addbook.component';
import { BookDetailComponent }  from './book-detail.component';
import { BookUpdateComponent }  from './book-update.component';

const routes: Routes = [
	{
	   path: 'book',
	   component: BookComponent
	},
	{
	   path: 'add-book',
	   component: AddBookComponent,
	   outlet: 'bookPopup'
	},	
	{
	   path: 'update-book/:book-id',
	   component: BookUpdateComponent,
	   outlet: 'bookPopup'
	},				
	{
	   path: 'book-detail',
	   component: BookDetailComponent,
	   outlet: 'bookList'
	},	
	{
	   path: '',
	   redirectTo: '/book',
	   pathMatch: 'full'
	}	
];
@NgModule({
  imports: [ 
          RouterModule.forRoot(routes) 
  ],
  exports: [ 
          RouterModule 
  ]
})
export class AppRoutingModule{ }  
app.module.ts
import { NgModule }   from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { AppComponent }  from './app.component';
import { BookComponent }  from './book.component';
import { AddBookComponent }  from './addbook.component';
import { BookDetailComponent }  from './book-detail.component';
import { BookUpdateComponent }  from './book-update.component';
import { BookService }  from './book.service';
import { AppRoutingModule }  from './app-routing.module';

@NgModule({
  imports: [     
        BrowserModule,
	FormsModule,
	AppRoutingModule,
	BrowserAnimationsModule
  ],
  declarations: [
        AppComponent,
        BookComponent,
	AddBookComponent,
	BookDetailComponent,
	BookUpdateComponent
  ],
  providers: [ BookService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { } 
book.ts
export interface Book {
  id: number;
  name: string;
  author: string;
  state: string;
} 
book.service.ts
import { Injectable } from '@angular/core';
import { Book } from './book';

const BOOKS: Book[] = [
	{ "id": 1, "name": "Java", "author": "Mahesh", "state": "off" },
	{ "id": 2, "name": "Angular", "author": "Mahesh", "state": "off" },
	{ "id": 3, "name": "Spring", "author": "Krishna", "state": "off" },
	{ "id": 4, "name": "Hibernate", "author": "Krishna", "state": "off" }
];
let booksPromise = Promise.resolve(BOOKS);

@Injectable()
export class BookService {
	getBooks() {
		return booksPromise;
	}
	addBook(book: Book) {
		return this.getBooks()
			.then(books => {
				let maxIndex = books.length - 1;
				let bookWithMaxIndex = books[maxIndex];
				book.id = bookWithMaxIndex.id + 1;
				book.state = 'off';
				books.push(book);
				return book;
			});
	}
	getBook(id: number) {
		return this.getBooks()
			.then(books => books.find(book => book.id === id));
	}
	resetBookState(book: Book) {
		return this.getBooks().then(books => {
			books.map(book => book.state = 'off');
			book.state = 'on';
			return books;
		});
	}
} 
book.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

import { BookService } from './book.service';
import { Book } from './book';
import { ON_OFF_ANIMATION } from './animations/on-off.animation';

@Component({
  templateUrl: 'book.component.html',
  animations: [
    ON_OFF_ANIMATION
  ]
})
export class BookComponent implements OnInit {
  books = {} as Promise<Book[]>;
  constructor(private bookService: BookService, private router: Router) {
  }
  ngOnInit() {
    this.books = this.bookService.getBooks();
  }
  edit(book: Book) {
    this.bookService.resetBookState(book).then(() =>
      this.router.navigate([{ outlets: { bookPopup: ['update-book', book.id] } }])
    );
  }
} 
book.component.html
<h3>Click to Update Book</h3>
<ul [ngClass]= "'sub-menu'">
   <li *ngFor="let book of books | async" [@onOffTrigger] = "book.state" (click)="edit(book)"> 
	   {{book.id}}. {{book.name}}
   </li>
</ul> 
book-update.component.ts
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { switchMap } from 'rxjs/operators';

import { BookService } from './book.service';
import { Book } from './book';

@Component({
  templateUrl: './book-update.component.html',
  styleUrls: ['./book-update.component.css']
})
export class BookUpdateComponent implements OnInit {
  book = {} as Book | undefined;
  constructor(private bookService: BookService,
    private router: Router,
    private route: ActivatedRoute) {
  }
  ngOnInit() {
    this.route.params.pipe(
      switchMap((params: Params) => this.bookService.getBook(+params['book-id']))
    ).subscribe(book => this.book = book);
  }
  update() {
    this.router.navigate([{ outlets: { bookPopup: null } }]);
  }
} 
book-update.component.html
<div *ngIf="book">
	<h3>Update Book for Id: {{book.id}}</h3>
	<p>Book Name: <input [(ngModel)]="book.name"> </p>
	<p>Book Author: <input [(ngModel)]="book.author"> </p>
	<p> <button type="button" (click)="update()">Update</button> </p>
</div> 
book-update.component.css
:host { 
   position: absolute; 
   background-color: #e0d7d5;
   top: 20%;
   left: 15%;
   border: 3px solid black;
} 
addbook.component.ts
import { Component, HostBinding } from '@angular/core';
import { Router } from '@angular/router';

import { BookService } from './book.service';
import { Book } from './book';
import { ROUND_ANTICLOCK_ANIMATION } from './animations/round-anticlock.animation';
@Component({
  templateUrl: './addbook.component.html',
  styles: [':host { position: absolute; top: 20%; left: 5%; border: 3px solid black; }'],
  animations: [
    ROUND_ANTICLOCK_ANIMATION
  ]
})
export class AddBookComponent {
  @HostBinding('@roundAntiClockTrigger') roundAntiClockTrigger = 'in';
  book = {} as Book;
  constructor(private bookService: BookService, private router: Router) {
  }
  add() {
    this.bookService.addBook(this.book).then(
      () => this.router.navigate([{ outlets: { bookPopup: null } }])
    );
  }
} 
addbook.component.html
<h3>Add Book</h3>
<p>Enter Name: <input [(ngModel)]="book.name"> </p>
<p>Enter Author: <input [(ngModel)]="book.author"> </p>
<p> <button type="button" (click)="add()">Add</button> </p> 
book-detail.component.ts
import { Component, OnInit, HostBinding } from '@angular/core';
import { Router } from '@angular/router';

import { FLY_IN_OUT_ANIMATION } from './animations/fly-in-out.animation';
import { BookService } from './book.service';
import { Book } from './book';

@Component({
  templateUrl: './book-detail.component.html',
  styles: [':host { position: absolute; top: 20%; left: 5%; border: 3px solid black; }'],
  animations: [
    FLY_IN_OUT_ANIMATION
  ]
})
export class BookDetailComponent implements OnInit {
  @HostBinding('@flyInOutTrigger') flyInOutTrigger = 'in';
  books: Promise<Book[]>;
  constructor(private bookService: BookService, private router: Router) {
    this.books = this.bookService.getBooks();
  }
  ngOnInit() {
  }
  close() {
    this.router.navigate([{ outlets: { bookList: null } }]);
  }
} 
book-detail.component.html
<h3>Book Details</h3>
<p *ngFor="let book of books | async"> 
	<b>Id:</b> {{book.id}}, <b>Name:</b> {{book.name}}, <b>Author:</b> {{book.author}}
</p>
<button type="button" (click)="close()">CLOSE</button> 
on-off.animation.ts
import { animate, state, style, transition, trigger } from '@angular/animations';

export const ON_OFF_ANIMATION =
	trigger('onOffTrigger', [
		state('off', style({
		  backgroundColor: '#E5E7E9',
		  color: '#1C2833',
		  fontSize: '18px',		  
		  transform: 'scale(1)'
		})),
		state('on',   style({
		  backgroundColor: '#17202A',
		  color: '#F0F3F4',
		  fontSize: '19px',
		  transform: 'scale(1.1)'
		})),
		transition('off => on', animate(100)),
		transition('on => off', animate(100))
	]);  
round-anticlock.animation.ts
import { animate, state, style, transition, trigger, sequence } from '@angular/animations';

export const ROUND_ANTICLOCK_ANIMATION =
  trigger('roundAntiClockTrigger', [
	state('in', style({
	    backgroundColor: '#E5E7E9',
	    color: '#1B2172'
	})),  
        transition('void => *', sequence([
            style({
		       transform: 'rotate(270deg)',
  		       opacity: 0,
		       backgroundColor: '#0D6063'
	    }),
            animate('0.6s ease-in-out')
        ])),
        transition('* => void',[ 
	    style({backgroundColor: '#0D6063'}),
            animate('0.6s ease-out', style({transform: 'rotate(-270deg)', opacity: 0}))
        ])
  ]);  
fly-in-out.animation.ts
import { animate, state, style, transition, trigger, group } from '@angular/animations';

export const FLY_IN_OUT_ANIMATION =
  trigger('flyInOutTrigger', [
        state('in', style({
		backgroundColor: '#7BBEFC',
	        color: '#080809', 
		transform: 'translateX(0)', 
		opacity: 1
	})),
        transition(':enter', [
          style({
	        backgroundColor: '#E3E8EC',
		transform: 'translateX(300%)',
		opacity: 0
	  }),
          group([
            animate('0.5s 0.1s ease-in', style({
               transform: 'translateX(0)',
            })),
            animate('0.3s 0.1s ease', style({
               opacity: 1
            }))
          ])
       ]),
       transition(':leave', [
          style({
	        backgroundColor: '#9DCEFC',
	  }),	
          group([
            animate('0.5s ease-out', style({
               transform: 'translateX(300%)'
            })),
            animate('0.3s 0.1s ease', style({
               opacity: 0
            }))
          ])
       ])
  ]); 
styles.css
.parent-menu ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
    overflow: hidden;
    background-color: #333;
}
.parent-menu li {
    float: left;
}
.parent-menu li a {
    display: block;
    color: white;
    text-align: center;
    padding: 15px 15px;
    text-decoration: none;
}
.parent-menu li a:hover:not(.active) {
    background-color: #111;
}
.parent-menu .active{
    background-color: #4CAF50;
}
.sub-menu ul {
   list-style-type: none;
   padding: 0;
}
.sub-menu li {
   display: block;
   width: 120px;
   line-height: 50px;
   padding: 0 10px;
   box-sizing: border-box;
   border-radius: 4px;
   margin: 10px;
   cursor: pointer;
   overflow: hidden;
   white-space: nowrap;
} 

Run Application

To run the demo application, find following steps.
1. Download source code using download link given below on this page.
2. Use downloaded src in your Angular CLI application. To install Angular CLI, find the link.
3. Run ng serve using command prompt.
4. Access the URL http://localhost:4200

References

Routing & Navigation
Angular Animations Example

Download Source Code

POSTED BY
ARVIND RAI
ARVIND RAI
LEARN MORE








©2024 concretepage.com | Privacy Policy | Contact Us