Welcome!

From the Trenches of the Enterprise Software

Yakov Fain

Subscribe to Yakov Fain: eMailAlertsEmail Alerts
Get Yakov Fain via: homepageHomepage mobileMobile rssRSS facebookFacebook twitterTwitter linkedinLinkedIn


Blog Feed Post

Angular 2: Implementing master-detail using router

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>.

master-detail2https://yakovfain.files.wordpress.com/2016/11/master-detail2.png?w=1520&... 1520w, https://yakovfain.files.wordpress.com/2016/11/master-detail2.png?w=150&h=70 150w, https://yakovfain.files.wordpress.com/2016/11/master-detail2.png?w=300&h... 300w, https://yakovfain.files.wordpress.com/2016/11/master-detail2.png?w=768&h... 768w, https://yakovfain.files.wordpress.com/2016/11/master-detail2.png?w=1024&... 1024w" sizes="(max-width: 760px) 100vw, 760px" />

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.


Read the original blog entry...

More Stories By Yakov Fain

Yakov Fain is a Java Champion and a co-founder of the IT consultancy Farata Systems and the product company SuranceBay. He wrote a thousand blogs (http://yakovfain.com) and several books about software development. Yakov authored and co-authored such books as "Angular 2 Development with TypeScript", "Java 24-Hour Trainer", and "Enterprise Web Development". His Twitter tag is @yfain