Pre-requisites
-
Download and install the current version of Node.js from https://nodejs.org
-
Install Angular CLI by running the following command:
npm install @angular/cli -g
Check your version of Angular CLI (ng -v
). It has to be 1.5.0 or later.
Let’s use Angular 5 to develop a myStore app where users can see featured products and view details of the selected product. The landing page of this app will look like this:
When the user clicks on the product title, the product details view is rendered
Note
|
The product data for this app will be hard-coded in the class ProductService so you won’t need any external server supplying data. If you want to create an Angular app that will get the data from a Java Spring server, following the instructions from the file myStore_instructions_spring_server.html.
|
We’ll start with generating a new Angular CLI project. After that, will create the home and product detail views by creating additional Angular components, and will create a service to fetch the products info.
Generate a new myStore project:
ng new myStore
Go to the directory myStore and start the app:
cd myStore
ng serve -o
Your browser shows the generated app with Anguar logo and three links.
Stop the dev server (Ctrl-C).
Open the myStore directory in your IDE.
In the myStore app, we’ll use the Bootstrap 4 framework for styling. Let’s install Bootstrap and its dependencies (jQuery and popper.js). Open the integrated Terminal window in your IDE and run the following command:
npm i bootstrap jquery popper.js -P
Add Bootstrap styles to the styles
section in .angular-cli.json so it looks as follows:
"styles": [
"styles.css",
"../node_modules/bootstrap/dist/css/bootstrap.css"
],
Add the required scripts for Bootstrap, and jQuery by modifying the section scripts
in .angular-cli.json to look as follows:
"scripts": [
"../node_modules/jquery/dist/jquery.js",
"../node_modules/popper.js/dist/umd/popper.js",
"../node_modules/bootstrap/dist/js/bootstrap.js"
],
Build the app and open it in the browser at localhost:4200:
ng serve -o
The app looks the same as before.
Our app will be consist of several components, e.g. App, Home, Footer, Navbar, etc. The App component has been generated already, and now we’ll generate more components using the Angular CLI’s command ng generate component
(or ng g c
). Open another Terminal window and run the following commands:
ng g c home -spec false
ng g c footer -spec false
ng g c navbar -spec false
ng g c product-item -spec false
ng g c product-detail -spec false
ng g c search -spec false
Each of the above components is generated in a separate folder. Open the file app.module.ts - Angular CLI has added required import statements and declared all of the above components there.
Note
|
Angular CLI 1.5.0 has a bug and you need to manually update the first line in the generated .ts files of the components to look like this: |
import {Component, OnInit, ViewEncapsulation} from '@angular/core';
Using the command ng g s
generate the product service that will provide the data to some of the above components. Since we’ll use this service in more than one component, we’ll generate it in the shared dir. Specify the -m
option so it’ll add ProductService
the providers
property in @NgModule
the app.module.ts:
ng g s shared/product -spec false -m app.module
To navigate to the product detail view we’ll be using Angular router. Add the following import statement to the app.module.ts:
import {RouterModule} from '@angular/router';
Add the routes configuration to the imports
section of @NgModule
:
imports: [
RouterModule.forRoot([
{path: '', component: HomeComponent},
{path: 'products/:productId', component: ProductDetailComponent}
]),
...
]
Our root module is configured, but the running app looks the same. Let’s use/modify the generated components and a service.
The generated application component is the root component of myStore and it serves as a host for all other components. The component’s source code consists of four files with the following extensions: .ts, .html, .css, and .spec.ts.
Replace the content of app.component.html to include the Navbar, Search, Footer, and the router outlet (we use the Bootstrap CSS classes):
<app-navbar></app-navbar>
<div class="container">
<div class="row">
<div class="col-md-3">
<app-search></app-search>
</div>
<div class="col-md-9">
<router-outlet></router-outlet>
</div>
</div>
</div>
<app-footer></app-footer>
Run the app with ng serve -o
and you’ll see the following:
Enjoy the initial version of myStore!
Note
|
If you don’t see the above window, open the browser’s console - it should have reported some errors. |
Replace the content of the navbar.component.html with a sample Bootstrap Navbar borrowed from https://getbootstrap.com/docs/4.0/components/navbar:
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Dropdown
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">Disabled</a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form>
</div>
</nav>
The browser shows the window with a ligh grey Navbar on top:
Tip
|
Make the window width smaller and see how the toolbar and page layout changes. |
Replace the content of the search.component.html with this:
<h4>Advanced Search</h4>
<form #f="ngForm">
<div class="form-group">
<label for="title">Product title:</label>
<input id="title"
placeholder="Title" type="text"
name="title" ngModel>
</div>
<div class="form-group">
<label for="price">Product price:</label>
<input id="price"
placeholder="Price" type="number"
name="price" ngModel>
</div>
<div class="form-group">
<label for="category">Category:</label><br/>
<select id="category"
placeholder="Category"
name="category" ngModel>
<option>books</option>
<option>electronics</option>
<option>hardware</option>
</select>
</div>
<div class="form-group">
<button type="submit"
class="btn btn-primary btn-block">Search</button>
</div>
</form>
The browser stopped rendering the app. Its console shows an error - it doesn’t know about ngForm
, which is a part of Angular Forms API. Add the FormsModule
to the imports
section of app.module.ts:
import {FormsModule} from "@angular/forms";
@NgModule({
...
imports: [
FormsModule,
...
The app looks like this now:
Replace the content of the footer.component.html with this:
<div class="container">
<hr>
<footer>
<div class="row">
<div class="col-lg-12">
<p>Copyright © My Store 2017</p>
</div>
</div>
</footer>
</div>
The product service will be responsible for service product data. In the shared directory, let’s create a file product.ts defining the Product
type:
export interface Product {
id: number;
title: string;
price: number;
rating: number;
shortDescription: string;
description: string;
categories: string[];
}
The file product.service.ts will contain a class ProductService
with methods getProducts()
and getProductById()
, and an array with hard-coded products
The provider for ProductService
is already declared in AppModule
. This service will be injected into HomeComponent
and ProductDetailComponent
.
Replace the code of product.service.ts with the following:
import {Injectable} from '@angular/core';
import {Product} from './product';
@Injectable()
export class ProductService {
getProducts(): Product[] {
return products;
}
getProductById(productId: number): Product {
return products.find(p => p.id === productId);
}
}
const products = [
{
"id": 0,
"title": "First Product",
"price": 24.99,
"rating": 4.3,
"shortDescription": "This is a short description of the First Product",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"categories": ["electronics", "hardware"]
},
{
"id": 1,
"title": "Second Product",
"price": 64.99,
"rating": 3.5,
"shortDescription": "This is a short description of the Second Product",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"categories": ["books"]
},
{
"id": 2,
"title": "Third Product",
"price": 74.99,
"rating": 4.2,
"shortDescription": "This is a short description of the Third Product",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"categories": ["electronics"]
},
{
"id": 3,
"title": "Fourth Product",
"price": 84.99,
"rating": 3.9,
"shortDescription": "This is a short description of the Fourth Product",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"categories": ["hardware"]
},
{
"id": 4,
"title": "Fifth Product",
"price": 94.99,
"rating": 5,
"shortDescription": "This is a short description of the Fifth Product",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"categories": ["electronics", "hardware"]
},
{
"id": 5,
"title": "Sixth Product",
"price": 54.99,
"rating": 4.6,
"shortDescription": "This is a short description of the Sixth Product",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"categories": ["books"]
}
];
ProductItemComponent
will know how to render one product that’s passed by its parent via the @Input()
property product
. Modify the file product-item.component.ts to look like this:
import {Component, Input} from '@angular/core';
import {Product} from '../shared/product';
@Component({
selector: 'app-product-item',
templateUrl: './product-item.component.html',
styleUrls: ['./product-item.component.css']
})
export class ProductItemComponent {
@Input() product: Product;
}
We’ll use HTML 5 <figure>
, <figcaption>
and Bootstrap styles in the file product-item.component.html. The routerLink
directive will be used to nabigate to the product detail view. Change its content to the following:
<figure class="figure">
<img src="http://placehold.it/320x150" class="figure-img img-fluid rounded">
<figcaption class="figure-caption">
<h5><a [routerLink]="['/products', product.id]">{{product.title}}</a>
<span>{{product.price | currency}}</span>
</h5>
<p>{{product.shortDescription}}</p>
</figcaption>
</figure>
To add some margins around the <figure>
element, let’s add styles to product-item.component.css:
figure {
margin-top: 1em;
margin-bottom: 1em;
margin-left: 5px;
margin-right: 5px;
}
By default, the home component will occupy the router outlet area and will render several featured product items. Modify the content of home.component.ts to look like this:
import {Component, OnInit} from '@angular/core';
import {Product} from '../shared/product';
import {ProductService} from '../shared/product.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
products: Product[]=[];
constructor(private productService: ProductService) { }
ngOnInit() {
this.products = this.productService.getProducts();
}
}
Angular invokes the lifecycle method ngOnInit()
after the instance of a component is created. We populate the products
array there.
Replace the content of home.component.html to loop through the array products
with *ngFor
and render each product:
<div class="row">
<div *ngFor="let product of products" class="col-sm-4 col-lg-4 col-md-4">
<app-product-item [product]="product"></app-product-item>
</div>
</div>
Each product will be represented by the same HTML template. The *ngFor
directive iterates through the products
array rendering HTML template for each element.
Because *ngFor
is inside <div>
, each loop iteration will render a <div>
with the content of the corresponding <app-product-item>
inside. To pass an instance of a product to ProductItemComponent
, you use the square brackets for property binding: [product]="prod"
, where [product]
refers to the property named product
inside the <app-product-item>
component, and product
is a local template variable declared on the fly in the *ngFor
directive as let product
.
Now your app should look like this:
The styles col-sm-4 col-lg-4 col-md-4
come from the Bootstrap framework where the viewport’s width is divided into 12 invisible columns. We want to allocate 4 columns (one third of the <div>
’s width) if a device has small width (sm
means 576px or more), large (lg
is for 1200px or more), and medium (md
is for 992px or more). Since we didn’t provide any layout for extra small devices (xs
means less than 576px), the ProductItemComponent
will take the entire width of the viewport. Read about the Bootstrap grid system at https://v4-alpha.getbootstrap.com/layout/grid/.
See how your app will be rendered on smaller devices by using the Toggle Device option in Google Dev Tools:
To navigate to the product detail view we’ll use Angular router configured in section "Adding components, service, and routes".
The ProductDetailComponent
is rendered in the router outlet area when the user clicks on the title in the ProductItemComponent
.
The ProductDetailComponent
receives the product ID from the parent (via ActivatedRoute
), and then makes requests to ProductService
to retrieve the details of the selected product.
Modify the code in product-detail.component.ts to look as follows:
import {Component, OnInit} from '@angular/core';
import {ProductService} from '..//shared/product.service';
import {Product} from '..//shared/product';
import {ActivatedRoute} from '@angular/router';
@Component({
selector: 'app-product-detail',
templateUrl: './product-detail.component.html',
styleUrls: ['./product-detail.component.css']
})
export class ProductDetailComponent implements OnInit {
product: Product;
constructor(private route: ActivatedRoute, private productService: ProductService) {}
ngOnInit() {
let prodId: number = parseInt(this.route.snapshot.params['productId']);
this.product = this.productService.getProductById(prodId);
}
}
The product detail template will render the product image (i.e. a large gray rectangle) with product details.
Modify the content of product-detail.component.html to look like this:
<figure class="figure">
<img src="http://placehold.it/820x320" class="figure-img img-fluid rounded">
<figcaption class="figure-caption">
<h4>Title: {{product.title}}</h4>
<h5>Price: {{product.price | currency}}</h5>
<h5> Description: {{product.description}}</h5>
<h5> Rating: {{product.rating}}</h5>
<h5>Categories:
<ul>
<li *ngFor="let category of product.categories">
{{category}}
</li>
</ul></h5>
</figcaption>
</figure>
Add a margin and colors in product-detail.component.css:
figure {
margin-top: 1em;
}
h4 {
color: blue;
}
h5 {
color: brown;
}
On the home page, click on the title of a product and you’ll see its details, for example:
The end!
P.S. I’m blogging on Angular-related topics at https://yakovfain.com.
P.S.S. NOTE: If you want to run the version of this app that communicates with the Spring Boot server, see instructions in AngularForJavaDevs/myStore_spring_boot.html.