In my blog about passing data to routes I mentioned that you can subscribe to parameters changing over time. In this blog I’ll show you how to do it by implementing master-detail functionality using the router.
Imagine, you have a list of products and, when the user clicks on one of the products, you need to show product details. There are different ways of implementing such functionality, but I’ll implement the use case when the product details functionality is implemented by a separate component that is created by the Angular router. The master part is represented by the list of mobile phones id/description, and the details about selected phone will be displayed by the component ProductDetailComponent that will be created by the router when the method Router.navigate() will be invoked for the first time. The following screen shot was take when the user selected the phone with ID=3. The bottom portion shows the details of this phone rendered by the ProductDetailComponent (on the cyan background) inside the <router-outlet>.
If the user will select another item from the list, its ID will be passed to the route and the respected product details will be displayed. Note that the URL will be change when the user selects different product.
Let’s start with the ProductDetailComponent. Angular router comes with the class ActivatedRoute that among other things stores the data received by the route. If you’ll read the source code of ActivatedRoute you’ll see the following properties there:
/** * The matrix parameters scoped to this route. * The observable will emit a new value when * the set of the parameters changes. */ params: Observable<Params>; /** * The current snapshot of this route. */ snapshot: ActivatedRouteSnapshot;
In the previous blog I showed the example of using the snapshot property, which is exactly what the name states – a one time snapshot of the router state. The Router would create an instance of the ProductDetailComponent, where we’d inject the instance of ActivatedRoute and get the id of the phone:
constructor(private route: ActivatedRoute) { this.productID = route.snapshot.params['id']; }
If the user would click on the link or invoke navigate() for the first time, the instance of the ProductDetailComponent would be created. But what if the instance of this component has already been created, but the user takes actions that results in multiple invocations of navigate() to the same route? The instance of ProductDetailComponent already exists, its constructor won’t be invoked again, and no new snapshots of the router states will be taken. This is where subscribing to an Observable stream comes in.
constructor(private route: ActivatedRoute) { this.route.params.subscribe( params => this.productID = params['id'] ); }
The code of the constructor is still invoked once, but now it creates a subscription to the data stream that can be pushed to the route. Of course, if the user will navigate to a different route, the instance of ProductDetailComponent will be destroyed and detached from DOM, and the subscription will be over. But in our application we’ll configure just one route so the subscription will stay alive once created. Below is the entire code of the ProductDetailClass:
import {Component} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; @Component({ selector: 'product', template: `<h3 class="product">Details for product id {{productID}}</h3>`, styles: ['.product {background: cyan; width: 200px;} '] }) export class ProductDetailComponent { productID: number; constructor(private route: ActivatedRoute) { this.route.params.subscribe( params => this.productID = params['id'] ); } }
The code that configures the routes, renders the list of products and arranges the navigation is shown next.
import {Component} from '@angular/core'; import {Routes, Router} from '@angular/router'; const routes: Routes = [ {path: 'product/:id', component: ProductDetailComponent} ]; class Product { id: number; description: string; } @Component({ selector: 'app', template: ` <ul style="width: 100px;"> <li *ngFor="let product of products" [class.selected]="product === selectedProduct" (click) = onSelect(product)> {{product.id}} {{product.description}} </li> </ul> <router-outlet></router-outlet> `, styles:[`.selected {background-color: cornflowerblue}`] }) class AppComponent { selectedProduct: Product; products: Product[] = [ {id: 1, description: "iPhone 7"}, {id: 2, description: "Samsung 7"}, {id: 3, description: "MS Lumina"} ]; constructor(private _router: Router){} onSelect(prod: Product): void { this.selectedProduct = prod; this._router.navigate(["/product", prod.id]); } }
The most interesting steps are the following:
1. We have configured one route that expects the product ID as a parameter:
const routes: Routes = [ {path: 'product/:id', component: ProductDetailComponent} ];
2. We asked Angular to inject the Router object so we can invoke its method navigate()
constructor(private _router: Router){}
3. When the user selects a phone from the list, we invoke the click handler passing the selected product to it:
(click) = onSelect(product)>
4. When the product is selected, we want to navigate to the route configured in step 1. If this is the first time the user selects the product, the instance of ProductDetailComponent will be created, otherwise, the existing instance will receive the selected product ID.
onSelect(prod: Product): void { this.selectedProduct = prod; // to apply proper styles to the selected product this._router.navigate(["/product", prod.id]); }
Angular uses RxJS and Observables in multiple places, and in this blog I’ve illustrated the use of observable stream with the router.
That’s all there is to it. To see this app in action, check out this plunk.
Hey Yakov,
Great tutorial! I have been working on an Angular2 Application and I have been implementing a similar pattern.
One issue I have run into: if the app is refreshed (or a specific link is navigated to directly), the master component loses its ‘selectedProduct’ variable, until the user makes a selection. Any thoughts on how to solve this?
Rex
Say, you specify the direct URL for product with ID 3:
http://127.0.0.1:8080/#/product/2
This component will receive params the same way as if you navigated from the main component. Emit an event from there via the @Output propery with the parameter value (e.g. id=2).
The parent will get this event and can programmatically select the corresponding item in the master list.
Interesting idea, thanks Yakov!
How do you display other properties of the phone in the details component?
My ideas :
1) Do you pass one parameter per property when calling routing.navigate (for instance one parameter for the name of the phone)? You have to change the code if product interface changeand you don’t leverage the interface of typescript
2) Do you inject a service to get products in both Master and detail component and store a product[] array in component controllers.
When navigating to the detail component you get the row of the products Array corresponding to the id of the phone passed when navigating between master and detail. I think you can do interface introspection to generically display all the properties of the product (if you add a property it will be added automatically).
Nobody stops you from passing an object containing several parameters in the navigate() method. But I’d prefer to use an injectable service if this is required.
Using introspection should have a serious reason and is not applicable to the example that I used in this blog.
how to pass custome json file i passing but givin service issue how to solve please helme sir
this is my Details service
import { Injectable } from ‘@angular/core’;
import { Http,Response} from ‘@angular/http’;
//mport { Observable } from ‘rxjs/Rx’;
import { Observable } from ‘rxjs/Rx’;
import ‘rxjs/add/operator/map’;
import { Productinfo } from ‘../model/product’;
import ‘rxjs/add/operator/do’;
@Injectable()
export class DetailsService {
constructor(private _http:Http ){}
getDetails(productId:any):Observable{
return this._http.get(“./assets/product/product.json”)
.map((response:Response)=>response.json());
}
}
=================================================================
This is my details component file i want to particular product select
import { Component,OnInit} from ‘@angular/core’;
import { ActivatedRoute }from ‘@angular/router’;
import { DetailsService} from ‘../../Service/DetailsService’;
import { Productinfo } from ‘../../model/product’;
@Component({
//selector:’my-details’,
templateUrl:’./Details.component.html’,
//template:`you have selected id={{productId}}`
providers:[DetailsService]
})
export class DetailsComponent implements OnInit {
//public productId:string;
public productinfo:Productinfo[];
statusMessage:string;
//public info:any;
constructor(private _DetailsService:DetailsService,private _activeRouter:ActivatedRoute){}
ngOnInit(){
let productId:any = this._activeRouter.snapshot.params[‘productId’];
console.log(productId);
//this.productId=productId;
//using productId we can load data in to server
this._DetailsService.getDetails(productId).subscribe(
(proData) =>this.productinfo = proData,
(error)=>{
this.statusMessage=’sorry problem with service’;
console.log(error);
}
);
}
}