Build A Master-Details Angular Application

In this article, I’ll discuss how you can quickly build a simple application based on the master-details user interface pattern using Angular CLI and Bootstrap 4.

See a live demo of the application here.

Setup

First, we need to install Angular CLI by running this command:

npm i -g @angular/cli

Second, install ngx-bootstrap and Bootstrap 4 (Release 4.0.0-beta.2). It’s important to note that ngx-bootstrap needs to know you are using Bootstrap 4, because by default, it assumes you are using Bootstrap 3.

npm i ngx-bootstrap bootstrap@4.0.0-beta.2 -S

After installing the packages, open styles.css and reference Bootstrap’s CSS:

@import '../node_modules/bootstrap/dist/css/bootstrap.min.css';

There is no need to install jQuery, which is a Bootstrap requirement. The ngx-bootstrap package installation takes care of this.
So why use Bootstrap, anyway? Because it is great for designing quick prototypes that don’t require a customized “look and feel”, so you focus more on logic instead of worrying about the “cosmetics” and front-end functionality.
Now, run these commands to create a new application. Here, I create a new project called master-details.

ng new master-details
cd master-details

The Scenario

Our master page will display a list of products. When a selection has been made from the list, the user will be sent to the details page, along with the product id as the parameter. On the details page, the product id will be used to fetch the appropriate details of the user-selected product from the master page.

The Model

We’ll need some product information to play with, so let’s create the data source for our application.

products.ts

export class Product {
    constructor(
        public id: number,
        public name: string,
        public type: string,
        public status: string,
        public price: number,
        public description: string
    ) {}    
}
export const PRODUCTS: Product[] = [
    { id: 1, name: 'Medavac Survival Kit', type: 'Camping Gear', status: 'In stock', price: 14.99, description: 'Comes in a waterproof case. Includes fire starter, compass, wire saw, emergency whistle, can opener, suture thread and needle, bandages and more!' },
    { id: 2, name: 'WazSUP Stand Up Paddle Board', type: 'Outdoor Sports Equipment', status: 'In stock', price: 975.00, description: '10ft in length, 60/40 rails hybrid stand up board. Excellent in surf or flat water conditions.' },
    { id: 3, name: 'Pole Benda Fishing Rod', type: 'Fishing Gear', status: 'Out of stock', price: 129.99, description: 'Graphite composite saltwater fishing rod. 12ft. in length. Stainless steel guides. Line capacity rings.' },
    { id: 4, name: 'The Great Outdoors Dome Tent', type: 'Camping Equipment', status: 'In stock', price: 300.00, description: '2-person capacity. Strong, waterproof, and easy to assemble. Made of durable extra-strength nylon.' },
    { id: 5, name: 'KalusTum Conventional Reel', type: 'Fishing Gear', status: 'In stock', price: 47.98, description: 'Reinforced anodized aluminum spool. Precision brass gears. Stainless steel parts for superior corrosion protection.' },
    { id: 6, name: 'ClearBlue Dive Mask', type: 'Diving Gear', status: 'On Order', price: 96.00, description: 'Single lens mask with excellent field of view. Silicone skirt for an improved fit. Fog resistant. Shatterproof.' }
];

Routing

In order for the master page to communicate with the details page, we need to setup an Angular Router. I like to create a separate module for routing. Then we can reference our routing module in the main module. We won’t use a standard navigation that can link to either of the two pages. We’ll pass the parameter via a defined route in appRoutes. Where you decide to place the router-outlet directive marks the spot where these pages are to be displayed.

app.routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductDetailsComponent } from './product-details/product-details.component';
// ROUTES
const appRoutes: Routes = [
    
    { path: '', redirectTo: 'product-list', pathMatch: 'full' },
    { path: 'product-list', component: ProductListComponent },
    { path: 'product-details/:id', component: ProductDetailsComponent }
];
@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true })
  ],
  exports: [
      RouterModule
  ]
})
export class AppRoutingModule { }

Take note of the path: product-details/:id. We define the parameter for the product id to be accepted through the url string from the product-list page. Let’s place the router-outlet in the main app’s component template.

app.component.html

<router-outlet></router-outlet>

Service

Creating a service is a great way to use the same set of information with other classes. Components consume the data from a service through their constructor method by way of dependency injection. Let’s create a service that will grab information about the products from our model.

Run this command to create a service:

ng g service services/product

Note that you need to manually register the service in your app.module.ts.

app/services/product.service.ts

import { Injectable } from '@angular/core';
import { PRODUCTS } from '../products';
@Injectable()
export class ProductService {
    constructor() {}
    getProducts() {
        return PRODUCTS;
    }
}

Pipe

Let’s create a custom pipe. Our custom pipe will filter our Product model and find the product whose id matches the passed parameter from the product-list page. We then display the product’s details on the details page.

app/pipes/selected-product.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';
import { Product } from '../products';
@Pipe({
    
    name: 'selectedProduct'
})
export class SelectedProductPipe implements PipeTransform {
    transform(allProducts: Product[], productId: number): any {
        return allProducts.filter(p => p.id === productId);
    }
}

The Components

Let’s create two components. One will be named product-list and the other product-details.

Create the product-list component using this command:

ng g component product-list

In addition to the default code that was generated after running the command, here’s the complete code:

app/product-list/product-list.component.ts

import { Component } from '@angular/core';
import { Product } from '../products';
import { Router } from '@angular/router';
// USING A SERVICE INSTEAD
import { ProductService } from '../services/product.service';
@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css'],
  providers: [ ProductService]
})
export class ProductListComponent {
    products: Product[];
    constructor(productService: ProductService) {
        this.products = productService.getProducts();
    }
}

app/product-list/product-list.component.html

<div class="container mt-5">
    <h1>Product List</h1>
    <div class="list-group">
        <a class="list-group-item list-group-item-action" *ngFor="let p of products" [routerLink]="['/product-details', p.id]">{{p.name}}</a>
    </div>
</div>

Run this command to generate the product-details component:

ng g component product-details

The complete code:

app/product-details/product-details.component.ts

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Product } from '../products';
import { ActivatedRoute } from '@angular/router';

// USING A SERVICE INSTEAD
import { ProductService } from '../services/product.service';

@Component({
  selector: 'app-product-details',
  templateUrl: './product-details.component.html',
  styleUrls: ['./product-details.component.css'],
  providers: [ProductService]
})
export class ProductDetailsComponent implements OnInit, OnDestroy {

    private id: number;
    products: Product[];
    private sub: any;

    prodIdSnapshot: number;

    constructor(private productService: ProductService, private route: ActivatedRoute) {

        this.products = productService.getProducts();
    }

    ngOnInit() {
        
        this.sub = this.route.params.subscribe(params => {

            this.id = +params['id'];
        });
    }

    ngOnDestroy() {

        this.sub.unsubscribe();
    }
}

app/product-details/product-details.component.html

<div class="container mt-5 mb-5">
    <h1>Product Details</h1>
    <div class="card-columns" *ngFor="let p of (products | selectedProduct: id)">
      <div class="card mb-3">
        <div class="card-body">
          <h5 class="card-title">Name</h5>
          <p class="card-text">{{p.name}}</p>
        </div>
      </div>
      <div class="card mb-3">
        <div class="card-body">
          <h5 class="card-title">Type</h5>
          <p class="card-text">{{p.type}}</p>
        </div>
      </div>
      <div class="card mb-3">
        <div class="card-body">
          <h5 class="card-title">Status</h5>
          <p class="card-text">{{p.status}}</p>
        </div>
      </div>
       <div class="card mb-3">
        <div class="card-body">
          <h5 class="card-title">Price</h5>
          <p class="card-text">${{p.price}}</p>
        </div>
      </div>
       <div class="card mb-3">
        <div class="card-body">
          <h5 class="card-title">Description</h5>
          <p class="card-text">{{p.description}}</p>
        </div>
      </div>
    </div>
    <a class="btn btn-primary" [routerLink]="['/product-list']">Back To Product Listing</a>
</div>

The nice thing about Angular CLI is it automatically registers the newly generated component in your main module. However, this is not the case for a service. You will need to reference the service manually.

Take note of the element class names in both components’ HTML template. These are Bootstrap class names. You can checkout the Bootstrap examples and apply them to your templates. Just as I did in the example above, you can copy and paste the HTML markup from Bootstrap’s card columns markup and insert Angular interpolation binding syntax where needed.

The root Module

Let’s make sure we have everything in the root App Module to make this happen.

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductDetailsComponent } from './product-details/product-details.component';
import { ProductService } from './services/product.service';
import { SelectedProductPipe } from './pipes/selected-product.pipe';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
  declarations: [
    AppComponent,
    ProductListComponent,
    ProductDetailsComponent,
    SelectedProductPipe
  ],
  imports: [
    AppRoutingModule,
    BrowserModule,
    FormsModule
  ],
  providers: [ProductService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Run the app.

ng serve

Open your browser, go to http://localhost:4200/ to see the app in action!

Conclusion

This simple implementation demonstrated a common and intuitive navigation pattern that you see in today’s mobile and desktop applications. I hope this article has inspired you to get started on creating your own application based on this simple and effective navigation design.

Checkout the source code for this application on GitHub.

Featured photo by  

unsplash-logoYanko Peyankov