Angular 2/4 : HTTP

HTTP service is the important part of any application which communicates with the server. Most front-end applications communicate with backend services over the HTTP protocol.

Till now, we are getting the dummy products data from MockData.Products.

In this tutorial, we will add the data persistence features in our application with the help of Http Services of Angular.

We will use the different methods like get(), post(), put() and delete() of Http to perform CRUD operations of product.

So let’s start… the first step is to enable HTTP services

  • Note

Here, we will use Angular 4 to perform HTTP calls. (same procedure for Angular 2). we will use Http service of @angular/http library for HTTP calls.

If you are Angular 5, read HTTP Calling using Angular 5.

Angular provides @angular/http library for communicating with a remote server over HTTP.

We will call Http Web Services (REST Services) using Http  service of  @angular/http.

To make Http available everywhere in the app,

  1. open the root AppModule,
  2. import the HttpModule symbol from @angular/http.
  3. add it to the @NgModule.imports array.

As shown below,

import { ProductService } from './service/product.service';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
...
@NgModule({
...
imports: [
...
HttpModule
],
...
})
export class AppModule { }

Now we require remote data server which provides HTTP Web Service, which we can call using Angular Front End.

You can create Web Service in any language like Java, PHP, .Net or NodeJS. We can consume this web service using Http in an angular app.

We don’t have any web services for our application, we can create web services as I talked above, but instead of creating web services, we can simulate a data server, which work same as other web services.

The advantage of simulated data server is, you can start your front end application development without waiting for the web service from the remote web server. once web services available from the remote server, you just need to change the URL in Http calls.

We will mimic communication with a remote data server by using the In-memory Web API module.

After installing the module, the app will make requests to and receive responses from the Http without knowing that the In-memory Web API is intercepting those requests, applying them to an in-memory data store, and returning simulated responses.

This facility is a great convenience for the tutorial. You won’t have to set up a server to learn about Http.

It may also be convenient in the early stages of your own app development when the server’s web API is not yet implemented.

  • Note

the In-memory Web API module has nothing to do with HTTP in Angular.

We are using this only to simulate our remote server, if you have remote services available, then you can directly use that web service also.

Execute below command in terminal to Install the In-memory Web API package

npm install angular-in-memory-web-api –save

Once successfully installed, create an InMemoryProductService in new file called

in-memory-product.service.ts as shown below.

import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryProductService extends InMemoryDbService {
createDb() {
const products = [
{
'id': 11,
'title': 'OPPO F5 Youth (Gold, 32 GB)',
'modelName': 'F5 Youth',
'color': 'Gold',
'productType': 'Mobile',
'brand': 'OPPO',
'price': 16990
},
{
'id': 12,
'title': 'Dell Inspiron 7000',
'modelName': 'Inspiron',
'color': 'Gray',
'productType': 'Laptop',
'brand': 'DELL',
'price': 59990
}
];
return { products };
}
}

We are not discussing InMemoryWebApi Module in detail, for more information refer README.md of In Memory Web Api module.

Now, Import the InMemoryWebApiModule and the InMemoryProductService in the AppModule

Add the InMemoryWebApiModule to the @NgModule.imports array and configure it with the InMemoryProductService, as shown below

import { InMemoryProductService } from './in-memory-product.service';
import { HttpModule } from '@angular/http';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
...
@NgModule({
...
imports: [
...
HttpModule,
InMemoryWebApiModule.forRoot(InMemoryProductService)
],
...
})
export class AppModule { }

The forRoot() configuration method takes an InMemoryProductService class that primes the in-memory database.

This configuration replaces the mock-product-data.ts, which is now safe to delete.

When your server is ready, detach the In-memory Web API, and the app’s requests will go through to the server.

Now back to the Http story.

  • Tip

For the testing purpose, we can also generate the delay in a simulated sever, while performing HTTP calls, by delay property in options of forRoor(), as shown below

InMemoryWebApiModule.forRoot(InMemoryProductService, { delay : 2000})

This will add the delay of two seconds.

Import Http and Headers from @angular/http in the ProductService .

import { Http, Headers } from '@angular/http';

Now inject Http into the constructor of ProductService in a private property called http.

export class ProductService{
...
constructor(private http: Http) {
...
}
...
}

Now let’s get the products from the data server.

Define the productsUrl with the address of the products resource on the server.

Our current In Memory Web API URL is api/products; define productsUrl as below

productsUrl = 'api/products';

Now convert the getProducts() method for Http.

import { Observable } from 'rxjs/Observable';
import { Http, Headers} from '@angular/http'
import 'rxjs/Rx';
...
@Injectable()
export class ProductService {
productsUrl = 'api/products';
constructor(private http: Http) {
}
getProducts(): Observable<Product[]> {
return this.http.get(this.productsUrl)
.map(products => {
console.log("products fetched...");
console.log(products.json());
return products.json() as Product[]})
.catch(this.handleError);
}
...
}

We have swapped http.get for of and the app keeps working without any other changes because both functions return an Observable<Product[]>.

  • Http methods return one value

All Http methods return a RxJS Observable of something.

HTTP is a request/response protocol. You make a request, it returns a single response.

In general, an Observable can return multiple values over time. An Observable from Http always emits a single value and then completes, never to emit again.

-Angular Docs

  • Http.get returns HTTP response

Http.get returns the Observable of type HTTP response. We need to call the json() method of the HTTP Response to extract the data within the response.

The shape of the JSON data is determined by the server’s data API. Here we will get the JSON object of Product Array.

Note: In the older version of InMemoryWebAPI, The response JSON has a single data property, which holds the array of products that the caller wants. So in older version, we will get Product array using response.json().data

We will convert the HTTP response into JSON object before returning it to calling a component method.

We will use the rxjs map operator to process the response before returning the Observable to the calling component method.

To use this operator we need to import map using below statement,

import 'rxjs/add/operator/map';

or to import all the operators from RxJS use

import 'rxjs/Rx';

We will convert the response into JSON format using json() method and then return it as our required observable type for example above we have returned the response as a Product[] type Observable. as shown below,

.map(products => {
console.log("products fetched...");
console.log(products.json());
return products.json() as Product[]})

Things go wrong, when we are getting data from the remote data server, so we need to catch this errors and do something appropriate.

To catch errors in ProductService.getProducts() method, we will use RxJS catch() operator.

We will import the catch operator as below

import 'rxjs/add/operator/catch';

or to import all the operators from RxJS use

import 'rxjs/Rx';

attach catch operator to the http.get() method

.catch(this.handleError);

The catch() operator intercepts an Observable that failed. It passes the error to an error handler that can do what it wants with the error.

We will create error handler called handleError() method, this method reports the error and then returns an appropriate result so that the application keeps working.

Note: as you can see above, we are not passing the failed response to the handleError() handler. It is automatically passed as an argument when handler called.

handleError() error handler will be shared by many ProductService methods so it’s generalized to meet their different needs.

handleError(error: Response | any) {
let errMsg: string;
if (error instanceof Response) {
errMsg = `${error.status} - ${error.statusText}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
if (error.status === 404) {
console.log('Server not Found');
}
console.log(errMsg);
return Observable.throw(error);
}

After reporting the error to console, the handler returns a safe value to the app so it can keep working.

As shown above, we can handle the different error status. For example, we can handle the status 404 and redirect it to the different page or add the logs for that etc.

Now once page refreshed, We will get the below output :

getProducts HTTP Service Output

Now let’s convert other methods for the HTTP Calls.

Most web APIs support a get by id request in the form api/products/:id  (such as api/products/11).

We will replace the getProduct() method in ProductService with below code

 getProduct(id: number): Observable {
const url = `${this.productsUrl}/${id}`;
return this.http.get(url)
.map(product => {
console.log(`fetched product id=${id}`);
return product.json() as Product
})
.catch(this.handleError)
}

There are three significant differences from getProducts().

  • It constructs a request URL with the desired product’s id.
  • The server should respond with a single product rather than an array of products.
  • therefore, getProduct() returns an Observable<Product> (“an observable of Product objects”) rather than an observable of Product arrays.

Note: we are using getProduct() method in product-detail.component.ts, because we have set the delay for the testing purpose, initially product object of product-detail.component.ts will be undefined.

So if you run the application you will get the error on a console as : Cannot read property ‘productType’ of undefined

To resolve this issue, add ? (elvis operator) in product.productType as product?.productType, so that template will access the productType only when a product is not undefined.

In our simulated data server, we can add the product by passing product object in HTTP post call of 'api/products'

Replace the addProduct(product) method with below code

addProduct(product: Product): Observable<Product> {
let headers: Headers = new Headers({'Content-Type':'application/json'});
const reqOpt = { 'headers': headers };
return this.http.post(this.productsUrl, JSON.stringify(product), reqOpt)
.map(p => {
console.log(`added product with id=${p.json().id}`);
return p.json() as Product
})
.catch(this.handleError)
}

We can pass the headers to Http Service using Headers class of @angular/http library.

In this method, we are passing the product in the JSON format. That’s why we will set the Headers 'Content-Type' as 'application/json'.

this.http.post(this.productsUrl, JSON.stringify(product), reqOpt)

As shown above, we need to pass product object in string format using JSON.stringify(product) in the body of post call , and send the Headers in the reqOpt.

Here, the simulated server returns an Observable, which is successfully added. We can use this returned result for showing appropriate message to use if required.

Note: In a previous chapter, we have not converted addProduct() method into an asynchronous method, so in AddProductModelComponent, we are still using synchronous calling of addProduct() method. But now we have converted addProduct() method into an asynchronous method which returns an observable.

So to call getProduct() method we need to subscribe it in AppProductModelComponent as shown below 

this.productService.addProduct(product).subscribe(result => {
this.formSubmitted = true;
this.router.navigateByUrl('/products');
});

In above code, we will navigate to products view only when we get the successful result from the addProduct() method of product.service.ts

The same way we will update the AddProductTemplateComponent.

Replace the removeProduct() method in ProductService with below code.

removeProduct(product: Product | number): Observable<Product> {
let headers: Headers = new Headers();
const reqOpt = { 'headers': headers };
const id = typeof product === 'number' ? product : product.id;
const url = `${this.productsUrl}/${id}`;
return this.http.delete(url, reqOpt)
.map(_ => console.log(`deleted Product id=${id}`))
.catch(this.handleError)
}

It calls Http.delete.

  • the URL is the products resource URL plus the id of the product to delete
  • We can get the product id directly as a number or by getting the product object.
  • To get the multiple types of the same parameter we will use product: Product | number
  • We don’t send data as we did with a post.
  • We still send the reqOpt.

Note: In a previous chapter, we have not converted removeProduct() method into an asynchronous method, so in ProductsComponent, we are still using synchronous calling of removeProduct() method. But now we have converted removeProduct() method into an asynchronous method which returns an observable.

So to call removeProduct() method we need to subscribe it in ProductsComponent, so replace the deleteProduct() method of ProductsComponent as shown below

deleteProduct(product: Product) {
this.productService.removeProduct(product).subscribe();
this.productService.getProducts()
.subscribe(
products => {
this.products = products;
}
);
}

Now refresh the browser and check all the functionality.

Http Service : Final Output

HTTP service call takes some time to get response, So in this delay instead of showing blank space, we can show loader or message to the user.

We can either use loading GIF or rotating icons to display loader.

Here we will create one LoaderComponent in LayoutModule which we can use to display loader for all the views. to control the visibility of LoaderCompnent we will create LoaderService, which can be used by any other component.

so lets first create LoaderComponent, using below CLI command.

ng g c loader
Loader Component CLI command

Now add the following code in loader.component.html

<i class="fa fa-refresh fa-spin fa-3x fa-fw" ></i>

To display loader on page, we will add the <app-loader> selector of LoaderComponent in the app.component.html as shown below

<app-header></app-header>
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display-3">Welcome to Electronic-Shop</h1>
<p class="lead">THE ONLINE MEGASTORE</p>
<hr class="my-2">
</div>
</div>
<div class="container-fluid">
<app-loader >Loading...</app-loader>
<router-outlet></router-outlet>
</div>

Note : we need to export LoaderComponent from the LayoutModule, to use it outside the LayoutModule.

Now refresh the page, you will able to see the loader. 

We need to display the loader only when the HTTP call is in progress, once HTTP call is complete we need to hide the loader. We can control the visibility of the loader by using one loader service.

So add the LoaderService using below CLI command in /service folder

ng g service loader

Now add the following code in the LoaderService

import { Injectable } from '@angular/core';
@Injectable()
export class LoaderService {
visible: boolean;
constructor() { }
showLoader() {
this.visible = true;
}
hideLoader() {
this.visible = false;
}
}
Here, I have created one visible variable, which will be controlled by showLoader() and hideLoader() method To use LoaderService in throughout the application, we will provide it in the @NgModule.providers of AppModule. Now inject the LoaderService in the LoaderComponent. As shown below
export class LoaderComponent implements OnInit {
constructor(public loaderService: LoaderService) { }
ngOnInit() {
}
}

We have created loaderService public, so that we can access it on the template of LoaderComponent.

Replace the loader.component.html as below

<i class="fa fa-refresh fa-spin fa-3x fa-fw" *ngIf="loaderService.visible"></i>

I have added *ngIf directive, which will make the loader visible only when loaderService.visible is true.

Now in whenever you want to make loader visible, use the showLoader() method of LoaderService and hide the loader by using hideLoader().

For example in ProductsComponent  :

import { LoaderService } from './../service/loader.service';
import { ProductService } from './../service/product.service';
import { Component, OnInit } from '@angular/core';
import { Product } from '../models/product';
@Component({
selector: 'app-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.css']
})
export class ProductsComponent implements OnInit {
products: Product[] = [];
constructor(public productService: ProductService,
private loaderService: LoaderService) {
}
ngOnInit() {
this.loaderService.showLoader();
this.productService.getProducts()
.subscribe(
products => {
this.products = products;
this.loaderService.hideLoader();
}
);
}
deleteProduct(product: Product) {
this.loaderService.showLoader();
this.productService.removeProduct(product).subscribe();
this.productService.getProducts()
.subscribe(
products => {
this.products = products;
this.loaderService.hideLoader();
}
);
}
}

The Same way add loader in other components.

import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryProductService extends InMemoryDbService {
createDb() {
const products = [
{
'id': 11,
'title': 'OPPO F5 Youth (Gold, 32 GB)',
'modelName': 'F5 Youth',
'color': 'Gold',
'productType': 'Mobile',
'brand': 'OPPO',
'price': 16990
},
{
'id': 12,
'title': 'Dell Inspiron 7000',
'modelName': 'Inspiron',
'color': 'Gray',
'productType': 'Laptop',
'brand': 'DELL',
'price': 59990
}
];
return { products };
}
}
import { LoaderService } from './service/loader.service';
import { InMemoryProductService } from './in-memory-product.service';
import { ProductService } from './service/product.service';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { ProductsComponent } from './products/products.component';
import { ProductComponent } from './product/product.component';
import { AddProductModelComponent } from './add-product-model/add-product-model.component';
import { AddProductTemplateComponent } from './add-product-template/add-product-template.component';
import { AppRoutingModule } from './app-routing.module';
import { LayoutModule } from './layout/layout.module';
import { ProductDetailComponent } from './product-detail/product-detail.component';
import { HttpModule } from '@angular/http';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
@NgModule({
declarations: [
AppComponent,
ProductsComponent,
ProductComponent,
AddProductModelComponent,
AddProductTemplateComponent,
ProductDetailComponent
],
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
AppRoutingModule,
LayoutModule,
HttpModule,
// The InMemoryWebApiModule module intercepts HTTP requests
// and returns simulated server responses.
// Remove it when a real server is ready to receive requests.
InMemoryWebApiModule.forRoot(InMemoryProductService)
],
providers: [ProductService, LoaderService],
bootstrap: [AppComponent]
})
export class AppModule { }
import { Injectable } from '@angular/core';
import { Product } from '../models/product';
import { Observable } from 'rxjs/Observable';
import { Http, Headers } from '@angular/http'
import 'rxjs/Rx';
@Injectable()
export class ProductService {
productsUrl = 'api/products';
constructor(private http: Http) {
}
getProducts(): Observable<Product[]> {
return this.http.get(this.productsUrl)
.map(products => {
console.log("products fetched...");
console.log(products.json());
return products.json() as Product[]})
.catch(this.handleError);
}
handleError(error: Response | any) {
let errMsg: string;
if (error instanceof Response) {
errMsg = `${error.status} - ${error.statusText}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
if (error.status === 404) {
console.log('Server not Found');
}
console.log(errMsg);
return Observable.throw(error);
}
addProduct(product: Product): Observable<Product> {
let headers: Headers = new Headers({'Content-Type':'application/json'});
const reqOpt = { 'headers': headers };
return this.http.post(this.productsUrl, JSON.stringify(product), reqOpt)
.map(p => {
console.log(`added product with id=${p.json().id}`);
return p.json() as Product
})
.catch(this.handleError)
}
getProduct(id: number): Observable<Product> {
const url = `${this.productsUrl}/${id}`;
return this.http.get(url)
.map(product => {
console.log(`fetched product id=${id}`);
return product.json() as Product
})
.catch(this.handleError)
}
removeProduct(product: Product | number): Observable<Product> {
let headers: Headers = new Headers();
const reqOpt = { 'headers': headers };
const id = typeof product === 'number' ? product : product.id;
const url = `${this.productsUrl}/${id}`;
return this.http.delete(url, reqOpt)
.map(_ => console.log(`deleted Product id=${id}`))
.catch(this.handleError)
}
}
import { LoaderService } from './../service/loader.service';
import { ProductService } from './../service/product.service';
import { Component, OnInit } from '@angular/core';
import { Product } from '../models/product';
@Component({
selector: 'app-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.css']
})
export class ProductsComponent implements OnInit {
products: Product[] = [];
constructor(public productService: ProductService,
private loaderService: LoaderService) {
}
ngOnInit() {
this.loaderService.showLoader();
this.productService.getProducts()
.subscribe(
products => {
this.products = products;
this.loaderService.hideLoader();
}
);
}
deleteProduct(product: Product) {
this.loaderService.showLoader();
this.productService.removeProduct(product).subscribe();
this.productService.getProducts()
.subscribe(
products => {
this.products = products;
this.loaderService.hideLoader();
}
);
}
}
import { LoaderService } from './../../service/loader.service';
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-loader',
templateUrl: './loader.component.html',
styleUrls: ['./loader.component.css']
})
export class LoaderComponent implements OnInit {
constructor(public loaderService: LoaderService) { }
ngOnInit() {
}
}
<i class="fa fa-refresh fa-spin fa-3x fa-fw" *ngIf="loaderService.visible"></i>
import { Injectable } from '@angular/core';
@Injectable()
export class LoaderService {
visible: boolean;
constructor() { }
showLoader() {
this.visible = true;
}
hideLoader() {
this.visible = false;
}
}
import { LoaderService } from './../service/loader.service';
import { ProductService } from './../service/product.service';
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl} from '@angular/forms';
import { Validators } from '@angular/forms';
import { Product } from '../models/product';
import { Router } from '@angular/router';
@Component({
selector: 'app-add-product-model',
templateUrl: './add-product-model.component.html',
styleUrls: ['./add-product-model.component.css']
})
export class AddProductModelComponent implements OnInit {
productTypes = ['Laptop', 'Mobile'];
formSubmitted = false;
myForm: FormGroup;
title: FormControl;
modelName: FormControl;
color: FormControl;
productType: FormControl;
brand: FormControl;
price: FormControl;
constructor(private productService: ProductService,
private router: Router,
private loaderService: LoaderService) { }
ngOnInit() {
this.title = new FormControl('', [Validators.required, Validators.minLength(10)]);
this.modelName = new FormControl();
this.color = new FormControl('', Validators.pattern('[a-zA-Z]*'));
this.productType = new FormControl('', Validators.required);
this.brand = new FormControl('', Validators.required);
this.price = new FormControl('', [Validators.required, Validators.min(1)]);
this.myForm = new FormGroup({
'title': this.title,
'modelName': this.modelName,
'productType': this.productType,
'color': this.color,
'brand': this.brand,
'price': this.price
});
}
addProduct(product: Product) {
this.loaderService.showLoader();
this.productService.addProduct(product).subscribe(result => {
this.formSubmitted = true;
this.loaderService.hideLoader();
this.router.navigateByUrl('/products');
});
}
}
import { ProductService } from './../service/product.service';
import { Product } from './../models/product';
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { LoaderService } from '../service/loader.service';
@Component({
selector: 'app-add-product-template',
templateUrl: './add-product-template.component.html',
styleUrls: ['./add-product-template.component.css']
})
export class AddProductTemplateComponent implements OnInit {
product: Product = new Product();
formSubmitted = false;
productTypes = ['Laptop', 'Mobile'];
constructor(private productService: ProductService,
private router: Router,
private loaderService: LoaderService) {
}
ngOnInit() {
}
addProduct() {
this.loaderService.showLoader();
this.productService.addProduct(this.product).subscribe(result => {
this.formSubmitted = true;
this.loaderService.hideLoader();
this.router.navigateByUrl('/products');
});
}
}
<app-header></app-header>
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display-3">Welcome to Electronic-Shop</h1>
<p class="lead">THE ONLINE MEGASTORE</p>
<hr class="my-2">
</div>
</div>
<div class="container-fluid">
<app-loader >Loading...</app-loader>
<router-outlet></router-outlet>
</div>
import { LoaderService } from './../service/loader.service';
import { ProductService } from './../service/product.service';
import { Product } from './../models/product';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
@Component({
selector: 'app-product-detail',
templateUrl: './product-detail.component.html',
styleUrls: ['./product-detail.component.css']
})
export class ProductDetailComponent implements OnInit {
product: Product;
constructor(private activatedRoute: ActivatedRoute,
private location: Location,
private productService: ProductService,
private loaderService: LoaderService) {
}
ngOnInit() {
let id = +this.activatedRoute.snapshot.paramMap.get('id');
console.log('ID : ' + id);
this.loaderService.showLoader();
this.productService.getProduct(id).subscribe(
product => {
this.product = product;
this.loaderService.hideLoader();
}
);
}
goBack() {
this.location.back();
}
}
<div class="card">
<div class="card-body">
<h4 class="card-title">PRODUCT DETAILS : 
<i *ngIf="product?.productType=='Mobile'" class="fa fa-mobile" aria-hidden="true"></i>
<i *ngIf="product?.productType=='Laptop'" class="fa fa-laptop" aria-hidden="true"></i>
</h4>
<div class="row">
<div class="col-md-6">
<table class="table">
<tr>
<th>Product ID</th>
<td>{{product?.id}}</td>
</tr>
<tr>
<th>Title</th>
<td>{{product?.title}}</td>
</tr>
<tr>
<th>Model Name</th>
<td>{{product?.modelName}}</td>
</tr>
<tr>
<th>Color</th>
<td>{{product?.color}}</td>
</tr>
<tr>
<th>Brand</th>
<td>{{product?.brand}}</td>
</tr>
<tr>
<th>Price</th>
<td>{{product?.price | currency : 'INR'}}</td>
</tr>
</table>
<button type="button" class="btn btn-primary" (click)="goBack()">Back</button>
</div>
</div>
</div>
</div>

In this chapter, we have seen

  • Http Web Service Calling using Angular 4 Http Service
  • Simulate a Data Server using In Memory Web Api
  • Http get(), post(), put() and delete() methods
  • Error Handling
  • map, catch operator of RxJS
  • Loader using different component.

If you like this information and find it interesting or useful - share it
Close Menu