Click here to Skip to main content
14,976,594 members
Articles / Web Development / HTML5
Article
Posted 29 Jan 2017

Stats

19.3K views
318 downloads
14 bookmarked

Angular 2 & .NET Core Developing Web App from Scratch Part 2: Implementing Front-end Part

Rate me:
Please Sign up or sign in to vote.
5.00/5 (11 votes)
4 Feb 2017CPOL6 min read
Great article to learn how you can create web application from scratch using Angular2 & .NET CORE WEB API

Contents

Introduction

After preparing the server part, we will start to build the client part (front-end part) using Angular 2 framework.

Our application is a single page application composed by four modules:

  • home:

    is the main page, used to expose available products to clients.

  • Login:

    is the authentication page, that allows users to log into the application and have access to the back-office side.

  • Subscription:

    is the subscription page, which provides a form to create a new user's account.

  • Product Management:

    this module provides a form to insert new products into the database.

 

In this article, I relied on the following links:

Background

To better understand this demo, it is preferable that you have a good knowledge about :

  • Programming in C#, JavaScript and HTML
  • Angular 2
  • MVC architecture
  • Data Binding
  • Entity Framework
  • Visual Studio Code (optional)

Prerequisites

Using the code

A) Create and Configure Angular 2 Project

First, use CMD to create a new folder (new project) named 'angular2fromscratch' in which you download the Angular 2 project from the offical Angular 2 documentation.

After, you should create the same configuration files (based on the official Angular 2 documentation):

  • package.json
  • tsconfig.json
  • typings.json

Next you should install TypeScript, typings, webpack, cookies service and angular2-material using npm:

  • Npm install –g typescript
  • Npm install -g typings
  • Npm install angular2-cookie
  • npm install @angular2-material/sidenav
  • npm install @angular2-material/input
  • npm install @angular2-material/button
  • npm install @angular2-material/core
  • npm install @angular2-material/card
  • npm install @angular2-material/icon
  • npm install @angular2-material/toolbar
  • npm install @angular2-material/progress-circle
  • npm install @angular2-material/sidenav

The final treeview should look like this:

Image 1

Before you start your implementation, you should make some changes on the systemjs.config.js file, to define the alias for the Angular 2 packages:

JavaScript
(function (global) {
  System.config({
    paths: {
      // paths serve as alias
      'npm:': 'node_modules/'
    },
    // map tells the System loader where to look for things
    map: {
      // our app is within the app folder
      app: 'app',
      // angular bundles
      '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
      '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
      '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
      '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
      '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
      '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
      '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
      '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
      '@angular2-material/input': 'npm:@angular2-material/input/input.umd.js',
      '@angular2-material/core': 'npm:@angular2-material/core/core.umd.js',
      '@angular2-material/card': 'npm:@angular2-material/card/card.umd.js',
      '@angular2-material/button': 'npm:@angular2-material/button/button.umd.js',
      '@angular2-material/icon': 'npm:@angular2-material/icon/icon.umd.js',
      '@angular2-material/toolbar': 'npm:@angular2-material/toolbar/toolbar.umd.js',
      '@angular2-material/progress-circle': 'npm:@angular2-material/progress-circle/progress-circle.umd.js',
      '@angular2-material/sidenav': 'npm:@angular2-material/sidenav/sidenav.umd.js',
       'angular2-cookie': 'npm:angular2-cookie',
      // other libraries
      'rxjs':                      'npm:rxjs',
      'angular-in-memory-web-api': 'npm:angular-in-memory-web-api',
    },
    // packages tells the System loader how to load when no filename and/or no extension
    packages: {
    
      app: {
        main: './main.js',
        defaultExtension: 'js'
      },
      rxjs: {
        defaultExtension: 'js'
      },
      'angular-in-memory-web-api': {
        main: './index.js',
        defaultExtension: 'js'
      },
       'angular2-cookie': {
        main: './core.js',
        defaultExtension: 'js'
      },
    }
  });
})(this);

B) Setup Angular 2 Project

In the app folder, create the following files:

  • app.module.ts

This file is used to:

  • Define routes using Router Module: "RouterModule"
  • Import needed Angular 2 modules via the word key: "imports"
  • Declare components via the word key: "declarations"
  • Declare services via the word key: "providers",
  • Specify the root components to include into index.html file via the word key: "bootstrap"
JavaScript
import { NgModule }             from '@angular/core';
import { BrowserModule }        from '@angular/platform-browser';
import { FormsModule }          from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { MdInputModule } from '@angular2-material/input';
import { MdCardModule } from '@angular2-material/card';
import { MdButtonModule } from '@angular2-material/button';
import { MdIconModule } from '@angular2-material/icon';
import { MdToolbarModule } from '@angular2-material/toolbar';
import { MdIconRegistry } from '@angular2-material/icon';
import { CookieService } from 'angular2-cookie/services/cookies.service';
import { AppComponent }   from './app.component';
import { loginComponenent }   from './app.loginComponenent';
import { homeComponenent }   from './app.homeComponenent';
import { subscriptionComponenent }   from './app.subscriptionComponenent';
import { productManageComponent }   from './app.productManageComponent';
import { PageNotFoundComponent }   from './app.pageNotFoundComponent';
import { MdProgressCircleModule } from '@angular2-material/progress-circle';
import { MdSidenavModule } from '@angular2-material/sidenav';

import { AuthentificationService } from './services/authentificationService';
import { ManageProductService } from './services/manageProductService'

const appRoutes: Routes = [
  { path: 'home', component: homeComponenent },
  { path: 'login', component: loginComponenent },
  { path: 'subscription', component: subscriptionComponenent },
  { path: 'productManagement', component: productManageComponent },
  { path: '', redirectTo: '/home',  pathMatch: 'full'},
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({

   imports:  [ FormsModule , BrowserModule,MdSidenavModule, MdProgressCircleModule, MdInputModule, MdToolbarModule, MdCardModule, MdButtonModule, MdIconModule,  RouterModule.forRoot(appRoutes)],
    declarations: [ AppComponent, loginComponenent, homeComponenent, subscriptionComponenent, productManageComponent, PageNotFoundComponent],
    providers: [ CookieService, MdIconRegistry, AuthentificationService, ManageProductService ],
    bootstrap: [ AppComponent ]
})
export class AppModule { }
  • app.component.ts

This file is used as code behind for the app.html template, in which you will have implementation of:

  • constructor: used to initialize our master page (app.html), and to declare the router listener that controls user navigation depending on current session value (session is managed by cookies).
  • logOut event: this event is used to free the current session by clearing cookies, and redirect user to home page.
JavaScript
import { Component } from '@angular/core';
import {MdToolbar} from '@angular2-material/toolbar';
import {CookieService} from 'angular2-cookie/core';
import {Router, NavigationStart, NavigationEnd} from '@angular/router'
@Component({
  selector: 'my-app',
  templateUrl: 'app/templates/app.html',
     
})
export class AppComponent {
  title = 'Angular 2 Application';
  showMenu : boolean = false;
  login : string = "";
  constructor(private _cookieService: CookieService, private router : Router){
      this.showMenu = false;
      router.events.subscribe((val) => {
        // see also 
        if(val instanceof NavigationStart) {
            this.login = this._cookieService.get("login");
            if(this.login != null && this.login != ""){
                this.showMenu = true;
                if(val.url.endsWith("login") || val.url.endsWith("subscription")){
                    router.navigateByUrl("/home");
                }
            } 
            else{
                this.showMenu = false;
                if(val.url.endsWith("productManagement")){
                      router.navigateByUrl("/home");
                  }
            }
        }
     });
  
  }
  public logOut(right){
    this.showMenu = false;
    right.close();
    this._cookieService.remove("login");
    location.reload();
    //this.router.navigateByUrl("/home");
  } 
}
  • app.homeComponenent.ts

Constitute the code behind for our home template, in which you will have implementation of:

  • ngOnInit event: refresh the product list on each page initialization
  • Remove event: deletes an existing product (identified by its "unique key") by calling "Delete method" of "_service variable"
  • loadProducts event: load all available products form remote server via the call of loadProducts method of _service variable
JavaScript
import { Component , OnInit} from '@angular/core';
import { Product } from './model/product'
import { ManageProductService } from './services/manageProductService'
import {CookieService} from 'angular2-cookie/core';

@Component({
  templateUrl: 'app/templates/home.html'
})
export class homeComponenent extends OnInit {
  title = 'Products';
  showProgress : boolean = false; 
  showError : boolean = false;
  showEmpty : boolean = false;
  isLogged  : boolean = false;
  listProduct : Product[];

  constructor(private _service : ManageProductService, private _cookieService: CookieService){
         super();
         console.log("constructor");
  }

  ngOnInit() {
     this.loadProducts();

     this.isLogged = (this._cookieService.get("login")!= null)?true: false;
  }

  public Remove(id: number) {
    var result = confirm("Do you want to continue the suppression process ?");
    let _self = this;
    _self.showError = false;
    _self.showEmpty = false;
    if (result == true) {
       
           this._service.deleteProduct(id).then(function(){
                _self.listProduct = _self.listProduct.filter(value=> {
                    return (value.id != id);
                });
                if(_self.listProduct.length==0){
                    _self.showEmpty = true;
                }
           }).catch(function(){
               _self.showError = true;
           });
    } else {
        
    }

  }
  public loadProducts(){
      
      let _self = this;
      _self.showError = false;
      _self.showEmpty = false;
      _self.showProgress = true;
      _self._service.loadProducts().then(function(response){
        
         _self.showProgress = false;
         if(response.length >0){
         
           _self.listProduct = response;
      
         }else{
            _self.showEmpty = true;
         }
         
      }).catch(function(error: any){
         _self.showProgress = false;
          _self.showError = true;
      });

  }
}
  • app.loginComponenent.ts

Constitute the code behind for the login template, in which you will have the implementation of:

  • tryToConnect event: open a new session with server using user credential (email, password)
JavaScript
import { Component } from '@angular/core';
import {Router} from '@angular/router'
import { User } from './model/user';
import { AuthentificationService } from './services/authentificationService';
import {CookieService} from 'angular2-cookie/core';

@Component({
  templateUrl: 'app/templates/login.html'
})

export class loginComponenent {
  public title = 'Login';
  public model : User = new User();
  constructor(private _service : AuthentificationService, private _cookieService:CookieService, private router: Router){

  }

  tryToConnect(){
     let _self = this;
      _self._service.Login(_self.model).then(function (response){
          
          _self._cookieService.put("login", _self.model.login);
          _self.router.navigateByUrl('/home');
       })
       .catch(function(error : any){
          alert("Fail to login");
       });
  }
}
  • app.productManageComponent.ts

Constitute the code behind for our productManagement template, in which you will have the implementation of:

  • addNewProduct event: called when the user clicks on the submit button for submitting the form data to register a new product. It will call the addProduct method of the _service variable.
JavaScript
 import { Component } from '@angular/core';

import { Product } from './model/Product';
import { ManageProductService } from './services/manageProductService';
@Component({
  templateUrl: 'app/templates/productManagement.html'
})
export class productManageComponent {
  title = 'Manage Product';
  model : Product;
  constructor(private _service : ManageProductService){
      this.model = new Product();
  }
  onChange(event) {
        var file = event.srcElement.files[0];
        this.model.pictureFile = file;
    
    }
  public addNewProduct(){

    let _self = this;
    let formData = new FormData();
    formData.append("title",_self.model.title);
    formData.append("fullDescription",_self.model.fullDescription);
    formData.append("price",_self.model.price);
    formData.append("file",_self.model.pictureFile, _self.model.pictureFile.name);
    _self._service.addProduct(formData).then(function(response){
      alert("new product was successfully created");
    }).catch(function(error: any){
      alert("Server error");
    });
  }
}
  • app.subscriptionComponenent.ts

Constitute the code behind for our singup template, in which you will have the implementation of :

Subscription event: raised when user try to submit new user details. It will call the addProduct method of the _service variable.

JavaScript
import { Component } from '@angular/core';
import { User } from './model/user';
import { AuthentificationService } from './services/authentificationService';
import {  NgModule }  from '@angular/core';
@Component({
  templateUrl: 'app/templates/singup.html'
})
export class subscriptionComponenent {
    title = 'Subscription';
  public model : User;

  constructor(private _service : AuthentificationService){
    this.model = new User();
  }
     
  public Subscription(){
    let _self = this;
    this._service.Signup(this.model).then(function(response){
 
       alert("Congratulation, the user with login : "+ _self.model.login  + " has been created");
    })
     .catch(function(error : any){
          alert("Fail to create new user");
      });
  }
}

Next, you should create the different folders and files:

1) model folder :

  • product.ts

Used to deserialize JSON data into a product object.

JavaScript
export class Product{
     id : number;
     title : string = '';
     fullDescription : string = '';
     price : number = 0;
     picture : string;
     pictureFile : any;
}
  • user.ts

Used to deserialize JSON data into a user object.

JavaScript
export class User{
     login : string = '';
     pwd : string = '';
     pwdConfirmation : string = '';
}

2) services folder:

  • authentificationService.ts

Implements needed methods to ensure user authentication and user registration:

  • Login: invokes an external web service "api/Services/Login" to create session with server.
  • Signup: calls a remote web service "api/Services/Subscription" to register the user.
JavaScript
import { Injectable } from '@angular/core';
import { Http, Response, RequestOptions, Headers } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import {User} from '../model/user';

@Injectable()
export class AuthentificationService {

    constructor( private _http : Http){
    }

     Login(model : User): Promise<any> {
            
            let _url = "http://localhost:5000/api/Services/Login";

            let bodyString = JSON.stringify(model);
            let headers    = new Headers({ 'Content-Type': 'application/json' }); // ... Set content type to JSON
            let options    = new RequestOptions({ headers: headers });
    
            return this._http.post(_url, bodyString, options)
            .toPromise();
 
    }
    
    Signup(model : any): Promise<any> {
            
            let _url = "http://localhost:5000/api/Services/Subscription";

            let bodyString = JSON.stringify(model);
            let headers    = new Headers({ 'Content-Type': 'application/json' }); // ... Set content type to JSON
            let options    = new RequestOptions({ headers: headers });
    
            return this._http.post(_url, bodyString, options)
            .toPromise();
       
    }
 
    protected handleErrorPromise(error: any): Promise<void> {

        try {
            error = JSON.parse(error._body);
        } catch (e) {
        }
        
        return Promise.reject(error);
    }
 
}
  • manageProductService.ts

This class offers services to manage product such as:

  • loadProducts: loads the available products list by calling an external web service via "api/Services/GetProducts" address
  • addProduct: it calls an external web service "api/Services/AddProduct" to create a new product
  • deleteProduct: deletes a specific product from database by calling an existing web service "api/Services/DeleteProduct/"
JavaScript
import { Injectable } from '@angular/core';
import { Http, Response, RequestOptions, Headers } from '@angular/http';
import { Product} from '../model/Product';
import 'rxjs/add/operator/toPromise';


@Injectable()
export class ManageProductService {
    constructor(private _http: Http) { }

   loadProducts(): Promise<Product[]> {
                
       let _url = "http://localhost:5000/api/Services/GetProducts";
        return this._http.get(_url)
            .toPromise()
            .then(response => this.extractArray(response));

    }      

    addProduct(formData: any): Promise<any>{
          let _url = "http://localhost:5000/api/Services/AddProduct";
        return new Promise(function (resolve, reject) {

            let xhr = new XMLHttpRequest();
            xhr.open('POST', _url, true);

            xhr.onload = function (e) {
                resolve(JSON.parse(xhr.response));
            };
            xhr.onerror = function (e) {
                reject(e);
            }

            xhr.send(formData);
        });
    }
  
    deleteProduct(id: number) {
        let _url = "http://localhost:5000/api/Services/DeleteProduct/";
        return this._http.delete(_url+'?id=' + id).toPromise();
    }    

    protected extractArray(res: Response, showprogress: boolean = true) {
        let data = res.json();
        return data || [];
    }

}

3) templates folder:

  • app.html

This is an HTML template and is used as a master page. It contains:

  • md-toolbar: it considered as a naviation menu, from which you can navigate to home, login and subscription pages.
  • md-sidenav: is the right-side navigation menu, which displays information about:
    • email of current user
    • navigation button: used to redirect to the productManagement page
    • logout button: to delete current session and redirect to the home page
  • router-outlet: is placeholder used to display specific view based on router state (read more about router-outlet).
HTML
<md-toolbar >
  <button md-icon-button  class="md-raised md-primary" routerLink="/home">
      <md-icon >home</md-icon>
  </button> 

  <span>{{title}}</span>
     <span flex></span>
    
        <a md-button routerLink="/login"   *ngIf="showMenu == false"  routerLinkActive="active">
            <i md-icon></i>Login
        </a>
        <a md-button routerLink="/subscription"  *ngIf="showMenu == false"  routerLinkActive="active">
                <i md-icon></i>Sign up
        </a>
 
      <button md-icon-button *ngIf="showMenu == true"  class="md-raised md-primary" (click)="right.toggle()">
          <md-icon  class="md-24">list</md-icon>  
    </button>
</md-toolbar>
 <md-sidenav-layout>
 
    <md-sidenav  #right align="end" layout-padding>
      <h5 style="text-align:center"><md-icon>account_circle</md-icon></h5>
      <h6 style="text-align:center"> {{login}}</h6>
      <hr>
          <div style="text-align:center">
            <button md-button  routerLink="/productManagement"  class="md-icon-button" style="width: 100%" (click)="right.close()" routerLinkActive="active">
                      <h5> Manage</h5>
              </button>
        </div>
       <div style="text-align:center">
            <button  md-button    class="md-icon-button"  style="width: 100%" (click)="logOut(right)" routerLinkActive="active">
                   <h5>Logout</h5>
            </button>
         </div>
    </md-sidenav>
    <div  layout-padding>
             <router-outlet></router-outlet>    
    </div>
  </md-sidenav-layout>
  • home.html

Through this page, the user can see a list of available products with details. If the user is connected, it will have a possibility to remove some products from the current list.

HTML
<div >
    <h1>{{title}}</h1>
    <div class="row">
    
        <md-progress-circle color="primary" mode="indeterminate"  *ngIf="showProgress" [style.width]="'40px'"  [style.margin]="'0 auto'" ></md-progress-circle>
        <div *ngIf="showEmpty" class="alert alert-info">
            <strong>Info :</strong> Empty Result
        </div>

        <div *ngIf="showError" class="alert alert-danger">
            <strong>Error :</strong> Problem happened in server side.
        </div>
    <div>
 
    <div class="row"  >
        <div  *ngFor="let obj of listProduct" class="col col-lg-4" style="margin-top:1px;  " > 
             <md-card >
                <md-card-title-group>
                    <img md-card-md-image  [src]="obj.picture">
                    <md-card-title>{{obj.title}}</md-card-title>
                    <md-card-subtitle>Price : {{obj.price}} $</md-card-subtitle>
                  
                </md-card-title-group>
                
                <md-card-actions *ngIf="isLogged == true" style="text-align: center">
                      <button md-icon-button  class="md-primary" (click)="Remove(obj.id)" >
                            <md-icon    class="md-24" >delete</md-icon>
                      </button>
                </md-card-actions>
            </md-card> 
            <div class="clearfix" ></div>
        </div>
    </div>
</div>
  • login.html

This page offers a connection form, that allows the user to enter his credential (email and password) and to open new session.

HTML
<div>
    <h1>{{title}}</h1>
    <div >
        <form>
            <div> 
                <label class="vSpace">  Login :</label>  <md-input type="email" name="login" [(ngModel)]="model.login" placeholder="Email" >  </md-input>
            </div>
            <div> 
                 <label class="vSpace"> Password : </label>  <md-input type="password" name="pwd" [(ngModel)]="model.pwd" placeholder="Password" >  </md-input>
            </div>
            <div> 
                <button type="button" md-raised-button   [disabled]="(model.login == '' || model.pwd =='')"  (click)="tryToConnect()"  class="md-primary" value="submit"  > 
                        submit
                </button>
            </div>
        </form>
    </div>
</div>
  • productManagement.html

This page offers user possibility to add a new product through using a form. He must provide information about: product Name (title), description, price, thumbnail (Picture).

HTML
<div>
    <h1>{{title}}</h1>
    <div >
        <form>
            <div> 
                <label class="vSpace">  Title :</label>  <md-input type="text" name="title" [(ngModel)]="model.title" placeholder="title">  </md-input>
            </div>
             <div> 
                <label class="vSpace">  Full description :</label>  <md-input type="text"name="fullDescription" [(ngModel)]="model.fullDescription" placeholder="Description">  </md-input>
            </div>
            <div> 
                 <label class="vSpace"> Price ($) : </label>  <md-input type="number" name="price" [(ngModel)]="model.price"  placeholder="Price" ></md-input>
            </div>
            <div> 
                <label class="vSpace"> Piture : </label>
                <label class="btn btn-default btn-file">
                    Load File <input type="file" id="file" name="pictureFile" (change)="onChange($event)" class="md-primary" class="hidden">
                </label>
            </div>
            <div> 
   
                <button type="button" md-raised-button  [disabled]="(model.title == '' || model.fullDescription =='')" (click)="addNewProduct()"   class="md-primary" value="submit"  > 
                        Add
                </button>
            </div>
        </form>
    </div>
</div>
  • singup.html

Through this page, any anonymous user can create their own account, to have the privileges to access the back-office of application.

To create a new account, the user should complete the subscription form by filling following information: login, password.

HTML
<div>
    <h1>{{title}}</h1>
    <div>
          <form id="newProductForm" >
            <div> 
            <label class="vSpace">Login :</label> 
                 <md-input type="email"  name="login" [(ngModel)]="model.login" placeholder="Email" >  </md-input>
            </div>
            <div> 
            <label class="vSpace">Password :</label>
                 <md-input type="password" name="pwd"  [(ngModel)]="model.pwd" placeholder="Password" >  </md-input>
            </div>
            <div> 
            <label class="vSpace">Confirm password :</label> 
               <md-input type="password"  name="pwdConfirmation"  [(ngModel)]="model.pwdConfirmation" placeholder="Confirm password" >  </md-input>
            </div>
            <div> 
 
            <button md-raised-button type="button"  [disabled]="(model.login == '' || model.pwd =='' || model.pwdConfirmation != model.pwd)" (click)="Subscription()"  class="md-primary" value="submit"  > 
                            submit
            </button>
            </div>
        </form>
    </div>
</div>

To run the demo, you should write the following command-line using CMD, but first be sure that you are in the root directory of your application:

  • npm start: to transpile TS files to JavaScript files and start application.

Image 2

When you try to open application via the given url (http://localhost:3000), you will get the below result:

Image 3

To get this result, you should start your server application (see Part 1 of this article : Server side implementation). Make sure your web service is accessible from this address: http://localhost:5000

References

Points of Interest

I hope that you appreciated this series of articles. Try to download the source code, and I'm waiting for your questions and comments.

History

  • v1 29/01/2017 : Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

O.Nasri
Technical Lead
France France
Microsoft certified professional in C# ,HTML 5 & CSS3 and JavaScript, Asp.net, and Microsoft Azure.

Comments and Discussions

 
QuestionTry creating project using Angular template in VS 2017 Pin
Larry @Datasmith6-Oct-17 8:52
MemberLarry @Datasmith6-Oct-17 8:52 
AnswerRe: Try creating project using Angular template in VS 2017 Pin
O.Nasri6-Nov-17 8:51
professionalO.Nasri6-Nov-17 8:51 
QuestionCan i have one Info, please ? Pin
daniweb333-Feb-17 1:05
Memberdaniweb333-Feb-17 1:05 
AnswerRe: Can i have one Info, please ? Pin
O.Nasri4-Feb-17 2:47
professionalO.Nasri4-Feb-17 2:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.