How to create store products management app using Angular & TypeScript?
In this post, you will learn how to create store products management app using Angular & TypeScript. The name that I came up for this app is “Sweetos” – a sweets store products management app. Think about this app as a basic app that allows the store employees to view the products in stock with some other details like cost, expiry date, customer ratings, etc.
This is a good way to understand the basics of new version of Angular as this app has multiple views for user-interface, service to consume data, client-side routing, etc. By learning this, the concepts can be applied to other types of projects as well.
The source of this app can be found here. Before looking at the code, let us look at the finished user-interface of this app below.

Outline
This post will talk about following sub-topics:
- Tools/Dependencies
- Products Data & Service
- Components
- Bootstrap Process
- Reusable Component
- Custom Filter / Pipes
- Conclusion
Tools/Dependencies
By the time you read this post, there may be new versions available. I used Visual Code to work on this application. Feel free to use a code editor/IDE of your choice. Following are the main tools and dependencies that I’ve used to create this application:
- Node.js (version: 4.5.0)
- Lite-server (version: 1.8.10)
- Angular (version: 2.0.0-beta.15)
- TypeScript (version: 1.8.0)
- TypeScript Type Definitions
- SystemJS (version: 0.19.26)
- RxJS (version: 5.0.0-beta.2)
- Bootstrap (versin: 3.3.6)
There are other node modules as well, detail of which you can read in the packages.json file’s content below:
{
"name": "sweetos",
"version": "1.0.0",
"author": "Siddharth Pandey",
"description": "Package for Sweetos - sweet store products management application",
"scripts": {
"start": "concurrently \"npm run tsc:w\" \"npm run lite\" ",
"tsc": "tsc",
"tsc:w": "tsc -w",
"lite": "lite-server",
"typings": "typings",
"postinstall": "sudo typings install"
},
"license": "ISC",
"dependencies": {
"angular2": "2.0.0-beta.15",
"systemjs": "0.19.26",
"es6-shim": "^0.35.0",
"reflect-metadata": "0.1.2",
"rxjs": "5.0.0-beta.2",
"zone.js": "0.6.10",
"bootstrap": "^3.3.6"
},
"devDependencies": {
"concurrently": "^2.0.0",
"lite-server": "^2.2.0",
"typescript": "^1.8.10",
"typings": "^0.7.12"
},
"repository": {}
}
Products Data & Service
In a real world application, data will be stored in a database which will be served by a back-end server to a client such as browser to show it to the users. To mimic this backend, I’m using a `products.json` file which is used by an Angular service to give a collection of sweet products to the consumer. The content of this file is shown below:
[
{
"productId": 1,
"productName": "Green Donut",
"productCode": "SW-001",
"expiryDate": "March 19, 2016",
"description": "A donut with chocolate frosting and green sprinkles.",
"price": 4.95,
"starRating": 3.2,
"availableUnits": 4,
"imageUrl": "https://openclipart.org/download/248991/Green-Donut-2.svg"
},
{
"productId": 2,
"productName": "Pink Donut",
"productCode": "SW-002",
"expiryDate": "March 21, 2016",
"description": "A donut with chocolate frosting and pink/white sprinkles.",
"price": 4.10,
"starRating": 3.2,
"availableUnits": 2,
"imageUrl": "https://openclipart.org/download/248993/Pink-Donut-2.svg"
},
{
"productId": 3,
"productName": "Sprinky Donut",
"productCode": "SW-003",
"expiryDate": "August 22, 2016",
"description": "A donut with chocolate frosting and mixed sprinkles.",
"price": 4.50,
"starRating": 4.50,
"availableUnits": 10,
"imageUrl": "https://openclipart.org/download/248990/Christmas-Donut.svg"
}
]
To use Typescript’s strongly typed feature, I’ve created an interface named `IProduct` which mimics the structure of the data that we expect to get from our service. The definition is mentioned below:
export interface IProduct {
productId: number;
productName: string;
productCode: string;
releaseDate: string;
price: number;
description: string;
starRating: number;
imageUrl: string;
availableUnits: number;
}
Next step is to create a service that will be responsible to encapsulate service calls to backend-server or a JSON file in this app’s case. So, if any part of the application needs products data, a service named `ProductService` can be consumed. The definition of this service is below:
import { Injectable } from 'angular2/core';
import { Http, Response } from 'angular2/http';
import { Observable } from 'rxjs/Observable';
import { IProduct } from './product';
@Injectable()
export class ProductService {
private _productUrl = 'api/products/products.json';
constructor(private _http: Http){ }
getProducts() : Observable<IProduct[]>{
return this._http.get(this._productUrl)
.map((response: Response) => <IProduct[]>response.json())
.do(responseData => console.log('All: ' + JSON.stringify(responseData)))
.catch(this.handleError);
}
getProduct(id: number): Observable<IProduct> {
return this.getProducts()
.map((products: IProduct[]) => products.find(p => p.productId === id));
}
private handleError(error: Response){
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
}
Let us now look at the `index.html` file which is the main layout of the app and responsible for loading all the resources like JavaScript and CSS files.
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Sweetos - sweets store products management</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="node_modules/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />
<link href="app/app.component.css" rel="stylesheet" />
<!-- 1. Load libraries -->
<!-- IE required polyfills, in this exact order -->
<script src="node_modules/angular2/es6/dev/src/testing/shims_for_IE.js"></script>
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<!--Required for http-->
<script src="node_modules/angular2/bundles/http.dev.js"></script>
<!--Required for routing-->
<script src="node_modules/angular2/bundles/router.dev.js"></script>
<!-- 2. Configure SystemJS -->
<script>
System.config({
packages: {
app: {
format: 'register',
defaultExtension: 'js'
},
shared: {
format: 'register',
defaultExtension: 'js'
}
}
});
System.import('app/main')
.then(null, console.error.bind(console));
</script>
</head>
<body>
<app>Loading Sweetos app...</app>
</body>
</html>
Components
This app consists of following component classes:
- AppComponent
- WelcomeComponent
- ProductListComponent
- ProductDetailComponent
Bootstrap Process
Note that `index.html` sets up SystemJS script which is responsible to load `main.js` file that takes care of bootstrapping our angular application. It also uses `import` syntax to load resources on demand like this:
import { bootstrap } from 'angular2/platform/browser';
// Our main component
import { AppComponent } from './app.component';
bootstrap(AppComponent);
To bootstrap, `App` component is used with a selector as `app`. This component is the main component that is responsible for routing definitions and a shell to display other components which is possible using `<router-outlet></router-outlet>`. The definition of this component is below:
import { Component, OnInit } from 'angular2/core';
import { HTTP_PROVIDERS } from 'angular2/http';
import { ROUTER_PROVIDERS, RouteConfig, ROUTER_DIRECTIVES } from 'angular2/router';
import 'rxjs/Rx'; // load all features from reactive extensions library - instruction to module loader; but actually imports nothing!
import {ProductListComponent} from './products/product-list.component'
import {ProductDetailComponent} from './products/product-detail.component'
import {WelcomeComponent} from './home/welcome.component'
import {ProductService} from './products/product.service';
@Component({
selector: 'app',
template: `
<div>
<nav class='navbar navbar-default'>
<div class='container-fluid'>
<a class='navbar-brand'>{{ pageTitle }}</a>
<ul class='nav navbar-nav'>
<li><a [routerLink]="['Welcome']"><span class="glyphicon glyphicon-home" aria-hidden="true"></span></a></li>
<li><a [routerLink]="['Products']"><span class="glyphicon glyphicon-th-list" aria-hidden="true"></span></a></li>
</ul>
</div>
</nav>
<div class='container'>
<router-outlet></router-outlet>
</div>
</div>
`,
directives: [ROUTER_DIRECTIVES],
providers: [ProductService, HTTP_PROVIDERS, ROUTER_PROVIDERS]
})
@RouteConfig([
{ path: '/welcome', name: 'Welcome', component: WelcomeComponent, useAsDefault: true },
{ path: '/products', name: 'Products', component: ProductListComponent },
{ path: '/product/:id', name: 'ProductDetail', component: ProductDetailComponent }
])
export class AppComponent implements OnInit {
pageTitle: string = 'Sweetos';
constructor() { }
ngOnInit() { }
}
This uses `@Component()` to register a component, `@RouteConfig()` to define 3 routes. It also registers some directives and providers dependency that can be consumed by this component and other nested children components. This is also a good way to define the common dependencies at some level and nested components can consume those providers/services. Whenever a route is activated, a linked component is then rendered.
The default route is to render `WelcomeComponent` which has a very simple view that acts as a landing view for user. The definition of this component is below:
import { Component } from 'angular2/core';
@Component({
template: `<div class="panel panel-info">
<div class="panel-heading">
{{pageTitle}}
</div>
<div class="panel-body" >
<div class="jumbotron">
<h1 class="text-center">Sweetos</h1>
<p class="text-center">... a sweets store products management app</p>
</div>
</div>
</div>`
})
export class WelcomeComponent {
public pageTitle: string = "Welcome";
}
Let us look at the definition and template for `ProductListComponent` which is responsible to show the products. It also consumes the product service to get available products. Note that dependency of service is not required here as it is already specified in the parent component. It uses another directive named as `StarComponent`, details of which you can find below in the post.
import { Component, OnInit } from 'angular2/core';
import { ROUTER_DIRECTIVES } from 'angular2/router';
import { IProduct } from './product';
import { ProductFilterPipe } from './product-filter.pipe';
import { StarComponent } from '../shared/star.component';
import { ProductService } from './product.service';
@Component({
templateUrl: 'app/products/product-list.component.html',
styleUrls: ['app/products/product-list.component.css'],
pipes: [ ProductFilterPipe ],
directives: [ StarComponent, ROUTER_DIRECTIVES ]
})
export class ProductListComponent implements OnInit {
pageTitle: string = 'Sweets';
imageWidth: number = 100;
imageMargin: number = 5;
showImage: boolean = true;
listFilter: string;
products: IProduct[] = []
errorMessage: string;
constructor(private _productService: ProductService) { }
toggleImage(): void{
this.showImage = !this.showImage;
}
ngOnInit() : void{
this._productService.getProducts().subscribe(
products => this.products = products,
error => this.errorMessage = <any> error);
}
onRatingClicked(message: string) : void{
console.log('rating has changed');
}
}
The template that shows a table of products with images is mentioned below. It uses a custom filter for searching the products named `ProductFilterPipe`.
<div class="panel panel-info">
<div class="panel-heading">
{{pageTitle}}
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-2">Filter by:</div>
<div class="col-md-4">
<input type="text" [(ngModel)]='listFilter'>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Filtered by: {{ listFilter }}</h3>
</div>
</div>
<div class="table-responsive" *ngIf='products.length'>
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>Product</th>
<th>Available Units</th>
<th>Expires on</th>
<th>Price</th>
<th>5 Star Rating</th>
<th>
<button class="btn btn-primary" (click)='toggleImage()'>
{{showImage ? 'Hide' : 'Show'}} Image
</button>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor='#product of products | productFilter:listFilter '>
<td>
<a [routerLink]="['ProductDetail', { id: product.productId }]">
{{ product.productName }}
</a>
</td>
<td>{{ product.availableUnits }} (set of 4)</td>
<td>{{ product.expiryDate }}</td>
<td>{{ product.price | currency:'USD':true:'1.2-2' }}</td>
<td><star [rating]='product.starRating' (ratingClicked)='onRatingClicked($event)'></star></td>
<td>
<a [routerLink]="['ProductDetail', { id: product.productId }]">
<img *ngIf='showImage' [src]='product.imageUrl' [alt]='product.productName' [title]='product.productName | uppercase'
[style.width.px]='imageWidth'
[style.margin.px]='imageMargin'>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
The `ProductDetailComponent` is responsible to show the detail of a selected product by getting the `id` of product from `routeParams`. Then this `id` is used to ask `ProductService` to get that product using `ngOnInit()` hook. This can be useful when only a subset of that data is shown in the list view and when detail is requested, client goes to the server to get all the detail of it.
import { Component, OnInit } from 'angular2/core';
import { RouteParams, Router } from 'angular2/router';
import { IProduct } from './product';
import { ProductService } from './product.service';
import { StarComponent } from '../shared/star.component';
@Component({
templateUrl: 'app/products/product-detail.component.html',
directives: [StarComponent]
})
export class ProductDetailComponent implements OnInit {
pageTitle: string = 'Product Detail';
product: IProduct;
errorMessage: string;
constructor(private _productService: ProductService,
private _router: Router,
private _routeParams: RouteParams) {
}
ngOnInit() {
if (!this.product) {
let id = +this._routeParams.get('id');
this.getProduct(id);
}
}
getProduct(id: number) {
this._productService.getProduct(id)
.subscribe(
product => this.product = product,
error => this.errorMessage = <any>error);
}
onBack(): void {
this._router.navigate(['Products']);
}
}
The template of `ProductDetailComponent` is below which is responsible to show the detail page of a product.
<div class='panel panel-info' *ngIf='product'>
<div class='panel-heading' style='font-size:large'>
{{pageTitle + ': ' + product.productName }}
</div>
<div class='panel-body'>
<div class='row'>
<div class='col-md-6'>
<div class='row'>
<div class='col-md-3'>Name:</div>
<div class='col-md-6'>{{product.productName}}</div>
</div>
<div class='row'>
<div class='col-md-3'>Code:</div>
<div class='col-md-6'>{{product.productCode}}</div>
</div>
<div class='row'>
<div class='col-md-3'>Description:</div>
<div class='col-md-6'>{{product.description}}</div>
</div>
<div class='row'>
<div class='col-md-3'>Availability:</div>
<div class='col-md-6'>{{product.expiryDate}}</div>
</div>
<div class='row'>
<div class='col-md-3'>Price:</div>
<div class='col-md-6'>{{product.price|currency:'USD':true}}</div>
</div>
<div class='row'>
<div class='col-md-3'>5 Star Rating:</div>
<div class='col-md-6'>
<ai-star [rating]='product.starRating'></ai-star>
</div>
</div>
</div>
<div class='col-md-6'>
<img class='center-block img-responsive'
[style.width.px]='200'
[style.margin.px]='2'
[src]='product.imageUrl'
[title]='product.productName'>
</div>
</div>
</div>
<div class='panel-footer'>
<a class='btn btn-default' (click)='onBack()' style='width:80px'>
<i class='glyphicon glyphicon-chevron-left'></i> Back
</a>
</div>
</div>
Reusable Component
The components that I’ve mentioned above – `ProductListComponent` and `ProductDetailComponent` are not reusable components. To show how to create a reusable component, I’ve created a `StartComponent` with a selector as `star` so that any product in the store can just use it. Both `ProductListComponent` and `ProductDetailComponent` uses this reusable component. This component also has a class, template and style listed below:
import { Component, OnInit, OnChanges, Input, Output, EventEmitter } from 'angular2/core';
@Component({
selector: 'star',
templateUrl: 'app/shared/star.component.html',
styleUrls: [ 'app/shared/star.component.css' ]
})
export class StarComponent implements OnInit, OnChanges {
@Input() rating: number;
starWidth: number;
@Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();
constructor() { }
ngOnInit() { }
ngOnChanges() : void {
this.starWidth = this.rating * 86 / 5;
}
onClick() : void{
this.ratingClicked.emit(`The rating ${this.rating} was clicked`);
}
}
<div class="crop"
[style.width.px]='starWidth'
[title]="rating"
(click)='onClick()'>
<div style="width: 86px">
<span class="glyphicon glyphicon-star"></span>
<span class="glyphicon glyphicon-star"></span>
<span class="glyphicon glyphicon-star"></span>
<span class="glyphicon glyphicon-star"></span>
<span class="glyphicon glyphicon-star"></span>
</div>
</div>
.crop {
overflow: hidden;
}
div {
cursor: pointer;
}
Custom Filter / Pipes
Using Angular, filters can be created to transform data while the templates process the data. This app has a search input box, using which users can search for a product. Now, it is down to your app to whether search from a local available data or go to a remote look-up API to perform a search. I’ve created a filter named `productFilter` that perform search based on data available locally. The definition of this filter is listed below:
import { Pipe, PipeTransform } from 'angular2/core';
import { IProduct } from './product';
@Pipe({
name: 'productFilter'
})
export class ProductFilterPipe implements PipeTransform {
transform(value: IProduct[], args: string[]): IProduct[] {
let filter: string = args[0] ? args[0].toLocaleLowerCase(): null;
return filter ? value.filter((product: IProduct) => product.productName.toLocaleLowerCase().indexOf(filter) != -1) : value;
}
}
Conclusion
This sums up my post that tells you how to create store products management app using Angular & TypeScript. I hope by reading this you now know how to create components, services, routing, filter. I intend to update this app later to add other behaviours as well, in which case, I’ll write another post.
Let me know if you face any issues in the comments and I’ll try to help you. If you would like to learn more about how to integrate Angular and TypeScript, refer to my Angular and AngularJS posts. If you would like to learn something specific then get in touch and I’ll write a post about it.
