Showing posts with label Angular. Show all posts
Showing posts with label Angular. Show all posts

Friday, 5 September 2025

Using Config File in Angular App

To use a configuration file in an Angular project, do these steps below. By default, an Angular project uses 'environments' folder name to put the config files.

First, create environment.ts for development or environment.prod.ts for production then add the config values:
// src/environments/environment.ts
export const environment = {
  production: false,
  apiBaseUrl: 'https://your-dev-url/'
};

// src/environments/environment.prod.ts
export const environment = {
  production: true,
  apiBaseUrl: 'https://your-production-url/'
};

Then we can use the config variable anywhere in our project:
import { environment } from '../../environments/environment'; // import the config files

. . .

private webApiUrl = environment.apiBaseUrl; // use environment.VARIABLE_NAME to refer to a config variable

Using 'ng-serve' will use the dev value, while 'ng build --prod' will use the production value.

Friday, 13 December 2024

Simple Angular Template Driven Form

Angular provides two types of forms in Angular namely Template Driven Form and Reactive Form. On this post, we will see a simple example of a Template Driven Form. Angular version used here is 19.

First, add FormsModule in app.module.ts:
import { FormsModule } from '@angular/forms';
. . .

@NgModule({
  declarations: [. . .],
  imports: [
    . . .
    FormsModule,
    . . .
  ],
  providers: [. . .  ]
})

Then the HTML elements:
<form #studentForm="ngForm" (ngSubmit)="submitStudentForm()">
  <div>
    <label for="firstName">Student Name:</label>
    <input type="text" id="firstName" [(ngModel)]="model.firstName" name="firstName" placeholder="First Name" />
    <input type="text" id="lastName" [(ngModel)]="model.lastName" name="lastName" placeholder="Last Name" />
  </div>
  <div>
    <label for="email">Email:</label>
    <input type="text" id="email" [(ngModel)]="model.email" name="email" />
  </div>
  <button type="submit">Update</button>
</form>
Note that we need to add a template reference variable, '#theFormName' and set its value to ngForm, in our example is <form #studentForm="ngForm">. In the example we also use [(ngModel)] on input fields for two ways data binding. name attribute for each input is also required by ngForm, otherwise it won't work.

Finally on the .ts file:
export class StudentComponent {

  model: Student = {
    firstName: 'John',
    lastName: 'Doe',
    email: 'john.doe@example.com'
  };

  submitStudentForm(): void {
	. . .
  }
}

Friday, 28 June 2024

Simple Example of Angular Material Stepper

This post will show a simple example of using Angular Material Stepper. Angular Material used is version 15. This example have two reactive forms with simple validations.
<mat-stepper linear #stepper>
  <mat-step [stepControl]="firstFormGroup" label="Fill out your name">
    <form [formGroup]="firstFormGroup">
      <div>
        <mat-form-field>
          <mat-label>Name</mat-label>
          <input matInput formControlName="name" required>
          <mat-error *ngIf="firstFormGroup.controls['name'].hasError('required')">
            Name is required!
          </mat-error>
        </mat-form-field>
      </div>
      <div>
        <button mat-button matStepperNext>Next</button>
      </div>
    </form>
  </mat-step>
  <mat-step [stepControl]="secondFormGroup" label="Fill out your address">
    <form [formGroup]="secondFormGroup">
      <div>
        <mat-form-field>
          <mat-label>Address</mat-label>
          <input matInput formControlName="address" required>
          <mat-error *ngIf="secondFormGroup.controls['address'].hasError('required')">
            Address is required!
          </mat-error>
        </mat-form-field>
      </div>
      <div>
        <button mat-button matStepperPrevious>Back</button>
        <button mat-button matStepperNext>Next</button>
      </div>
    </form>
  </mat-step>
  <mat-step label="Done">
    <p>You are now done.</p>
    <div>
      <button mat-button matStepperPrevious>Back</button>
      <button mat-button (click)="stepper.reset()">Reset</button>
    </div>
  </mat-step>
</mat-stepper>

And the TypeScript file:
export class MyComponent {
  firstFormGroup: FormGroup;
  secondFormGroup: FormGroup;

  constructor(private _formBuilder: FormBuilder) {
    this.firstFormGroup = this._formBuilder.group({
      name: ['', Validators.required]
    });

    this.secondFormGroup = this._formBuilder.group({
      address: ['', Validators.required]
    });
  }
}

Friday, 10 November 2023

HTTP Interceptor Library for JSON Web Token Authorisation

I was looking for an interceptor library for HTTP requests made from my Angular web app to backend services. The idea is to inject every request with Authorization header containing user JWT access token. Found a popular one in GitHub, https://github.com/auth0/angular2-jwt.

First install the library:
npm install @auth0/angular-jwt

Then in app.module.ts file, add the codes below. Note that my app has an authentication class (AuthService) that manages all the authentication functionalities, including retrieving user JWT access token.
. . .
import { JwtModule, JWT_OPTIONS } from '@auth0/angular-jwt';
import { AuthService } from './services/auth.service';

export function jwtOptionsFactory(authService: AuthService) {
  return {
    tokenGetter: () => {
      return authService.getUserAccessToken();
    },
    allowedDomains: ["localhost:5555"]  // my local dev environment
  }
}


@NgModule({
  imports: [
    JwtModule.forRoot({
      jwtOptionsProvider: {
        provide: JWT_OPTIONS,
        useFactory: jwtOptionsFactory,
        deps: [AuthService]
      }
    }),
    . . .
  ],
  . . .
})

export class AppModule { }

Now all the HTTP requests will have 'Authorization: Bearer [TOKEN]' added in the headers.

Wednesday, 16 August 2023

Angular Jasmine Unit Test - Child Component with Input and Output Properties

We are going to write some unit tests for an Angular component that calls child components. Our main component template is as follow:
. . .
. . . some content of the main component . . .
. . .
<ng-container *ngFor="let id of studentIds">
  <app-student-profile [studentId]="id"  (alertEmitter)="displayAlert($event)></app-student-profile>
</ng-container>
. . .
Our child component:
<div *ngIf="student" class="studentDetails">
  <button id="btnTest" (click)="sendAlert()">send alert</button>
  <div>
    <div>Student ID</div>
    <div>{{student.studentId}}</div>
  </div>
  <div>
    <div>First Name</div>
    <div>{{student.firstName}}</div>
  </div>
  <div>
    <div>Last Name</div>
    <div>{{student.lastName}}</div>
  </div>
  <div>
    <div>Email Address</div>
    <div>{{student.email}}</div>
  </div>
</div>
The child component also has input and output properties. The input is expecting Student ID to be passed from the parent component and the output will pass a message to the parent component to be displayed.

Some of the codes from child component class:
. . .
@Input() studentId!: number;
@Output() alertEmitter: EventEmitter<string> = new EventEmitter<string>();

sendAlert(): void {
  this.alertEmitter.emit("an alert from student profile component with Student Id: " + this.studentId);
}
. . .

To test the child component, we can use ng-mocks testing library, which is popularly used for Angular testing. Our tests will look like:
describe('MainComponent', () => {
  let component: MainComponent;
  let childComponent: StudentComponent;
  let fixture: ComponentFixture<MainComponent>;

  beforeEach(async () => {

    await TestBed.configureTestingModule({
      declarations: [MainComponent, 
	                 MockComponent(StudentComponent)],
      //schemas: [NO_ERRORS_SCHEMA]
    })
    .compileComponents();

    fixture = TestBed.createComponent(MainComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it("should have correct numbers of <app-student-profile> child component(s)", () => {
    let childComponents = fixture.debugElement.queryAll(By.directive(StudentComponent));
    expect(childComponents.length).toEqual(studentIds.length)
  })

  it("should pass right argument to child components", () => {
    let childComponents = fixture.debugElement.queryAll(By.directive(StudentComponent));
    for (var i = 0; i < component.studentIds.length; i++) {
      let childComponent: StudentComponent = childComponents[i].componentInstance;
      expect(childComponent.studentId).toEqual(component.studentIds[i]);
    }
  })
});
On line 10, we mock the child component with MockComponent().

[NO_ERRORS_SCHEMA] is also not needed in the declaration like in other approaches without using ng-mocks library.

On line 25, we use fixture.debugElement.queryAll(By.directive(CHILD_COMPONENT_NAME)) to find all child components. Notice that with the library, we can find the child component by class name (type). Other approaches need to use a fake component class or querying the html element (using By.css() function).

Line 32, we get the child component object with .componentInstance. Then we will be able to access all its properties and methods. We can check the argument passed to its input property by directly inspecting its class property.


Lastly, we need to test the output property. It will relay an event then call this function on parent component:
displayAlert(message: string): void {
  console.log(message);
}
We can test this interaction with something like:
it("should be able to catch alert from child component", () => {
  const alertMessage: string = "test alert";
  spyOn(console, 'log');
  //spyOn(component, 'displayAlert');   // if we want to test the parent component function is called
  epProfileComponent = fixture.debugElement.query(By.directive(StudentEPProfileComponent)).componentInstance;
  epProfileComponent.alertEmitter.emit(alertMessage);
  //expect(component.displayAlert).toHaveBeenCalledWith(alertMessage);   // if we want to test the parent component function is called
  expect(console.log).toHaveBeenCalledWith(alertMessage);
})
Notice on line 6, we can call the emit() function and then on line 8, check that the parent's function we want to be called is called (in our example is console.log).

Friday, 11 August 2023

Angular Jasmine Unit Test - Faking Service and its Methods

On this post, we will try to create Jasmine unit tests to fake a service and its function that is used in an Angular component.
Used Angular version is 15 and Jasmine is 4.5.0.

Our component page:
export class HomeComponent implements OnInit {
  student?: StudentWithBasicProfile;

  constructor(
    private studentService: StudentService,
    private route: ActivatedRoute) { }

  ngOnInit(): void {
    let studentId = this.route.snapshot.params['id'];

    this.studentService.getStudentBasicProfile(studentId)
      .subscribe(student => {
          this.student = student;
      });
  }  
}
This component retrieves an id from query string then call a method of a service then display the result.

Our tests look like this:
describe('HomeComponent', () => {
  let component: HomeComponent;
  let fixture: ComponentFixture<HomeComponent>;
  let studentServiceSpy: jasmine.SpyObj<StudentService>;
  let response: StudentWithBasicProfile;
  let routeId: number = 123;

  beforeEach(async () => {
    studentServiceSpy = jasmine.createSpyObj('StudentService', ['getStudentBasicProfile']);
    response = {
      studentId: routeId,
      firstName: '',
      lastName: '',
      email: '',
      mobilePhone: ''
    };
    studentServiceSpy.getStudentBasicProfile.and.returnValue(of(response));

    await TestBed.configureTestingModule({
      declarations: [HomeComponent],
      providers: [
        { provide: StudentService, useValue: studentServiceSpy },
        { provide: ActivatedRoute, useValue: { snapshot: { params: { 'id': routeId } } } }  // this one is to fake "this.route.snapshot.params['id']" code
      ]
    })
    .compileComponents();

    fixture = TestBed.createComponent(HomeComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it("should call getStudentBasicProfile() with correct parameter and return data", () => {
    expect(studentServiceSpy.getStudentBasicProfile).toHaveBeenCalledTimes(1);
    expect(studentServiceSpy.getStudentBasicProfile).toHaveBeenCalledWith(routeId);
    expect(component.student).toEqual(response);
  });
});
On lines 4, 9, and 17, we set up a fake StudentService and its function 'getStudentBasicProfile' and set up a return value. Then on line 22, we use this fake service instead of the real service.

On line 23, we fake the query string value in route with "{ snapshot: { params: { 'id': [VALUE] } } }".

Monday, 28 October 2019

Getting and Listing Latest Data in Angular

We will look on how to get and have up to date records to be displayed on a view in Angular 8. Assume we divide our codes with repository, service and presentation layers.

First, on repository, we have our codes returning a Promise:
getAllItems() : Promise<ItemInfo[]> {
 return new Promise((resolve, reject) => {
  this.dbInstance.executeSql("SELECT * FROM Item", [])
   .then((rs) => {
    . . .
    resolve(itemsList);
   })
   .catch((err) => reject(err));   
 });
};

We also have this on the repository layer that will be used by service layer to know that the database is ready to be used:
private dbReady: BehaviorSubject<boolean> = new BehaviorSubject(false);

constructor(private plt: Platform) {
 this.plt.ready().then(() => {
  this.initialise()
   .then(() => {
    this.dbReady.next(true);
   })
   .catch((err) => console.error(err)); 
 });
}

getDatabaseState() {
 return this.dbReady.asObservable();
}

Then on the service. Notice that we will be using BehaviorSubject type and its next() method to announce to listeners that there's a change.
export class ItemService {

    // this is a handy variable used to keep the latest data in memory
    private _itemsData: ItemInfo[] = [];

    private _items: BehaviorSubject<ItemInfo[]> = new BehaviorSubject<ItemInfo[]>([]);

    // getter that will be used by the presentation layer to get the items
    get items(): Observable<ItemInfo[]>  {
        return this._items.asObservable();
    }

    constructor(private databaseService: DatabaseService) {
        this.databaseService.getDatabaseState().subscribe(ready => {
            if (ready) {
                this.databaseService.getAllItems()
                    .then((items) => {
                        this._itemsData = items;

                        this._items.next(this._itemsData);
                    })
                    .catch(error => {
                        console.error(error);
                    });
            }
        });
 }

    addItemDetails(item: ItemForm) {
        return new Promise((resolve, reject) => {
            this.databaseService.insertItemDetails(item)
                .then((newItemId) => {                   
                    this._itemsData.push(new ItemInfo(. . .));
                    this._items.next(this._itemsData);
                    resolve();
                })
                .catch(error => {
                    console.error(error);
                    reject('Error: item cannot be inserted into database.')
                });
        });
    }

    editItemDetails(item: ItemForm) {
        return new Promise((resolve, reject) => {
            this.databaseService.updateItemDetails(item)
                .then(() => {
                    for (let i of this._itemsData) {
                        if (i.itemId == i.itemId) {
                            . . .
                        }
                    }
                    this._items.next(this._itemsData);
                    resolve();
                })
                .catch(error => { reject("Error: item cannot be updated in database."); });
        });
    }

    // similarly when deleting, the local variable _itemsData will need to be updated then call the BehaviorSubject next() method to tell the listeners that there is a change.
}

On our presentation (component .ts file), we use:
export class ListItemsPage implements OnInit {
    items: Observable<ItemInfo[]>;

    constructor(private service: ItemService) {
    }

    ngOnInit() {
        this.items = this.service.items;
    }
}

Finally, on the template .html page, we have:
<ion-list>
 <ion-item *ngFor="let i of items | async">
  <ion-label>
   <div>{{i.name}}</div>
  </ion-label>
 </ion-item>
</ion-list>

Tuesday, 17 September 2019

Converting AngularJS $q to JavaScript Promise

Since I had to upgrade my app from AngularJS to Angular, I would also need to change my promise returned codes. I needed to change all of my codes that were using $q AngularJS service to the new JavaScript Promise.
These codes:
getItems = function () {
  var deferred = $q.defer();

  try {
   . . .
   deferred.resolve(itemsToBeReturned);
  }
  catch(error) {
   . . .
   deferred.reject(error);
  }
  
  return deferred.promise;
};
are converted into:
getItems() {
 return new Promise((resolve, reject) => {
   try {
    . . .
    resolve(itemsToBeReturned);
   }
   catch(error) {
    . . .
    reject(error);
   }
 });
};
The consuming function can remain the same:
this.getItems()
 .then((items) => {
  // success
  . . .
 })
 .catch(error => {
  // error
  . . .
 });

The new JavaScript Promise also allowed the promises returned to be chained:
getItems() {
 return new Promise((resolve, reject) => {
  this.doSomething()
   .then((rs) => {
    . . .
    return Promise.resolve(123); 
   })
   .then((rs) => {
    . . .
    return 456;   // this returns a promise as well
   })
   .then((rs) => { 
    . . .
    resolve(result); 
   })
   .catch(error => reject(error));
 });
};


Reference:
Promise - TypeScript Deep Dive

Wednesday, 4 September 2019

Easy Way to Highlight Invalid Fields in Submitted Angular Form

Here is one way to highlight invalid input fields after submitting a form in Angular (Template Driven Form approach) to help user quickly seeing the invalid input fields.

First we create a variable to indicate whether the form has been submitted or not:
export class PropertyPage {
    isSubmitted: boolean;

    constructor() {
        this.isSubmitted = false;
        . . .
    }
 
    addPropertyDetails(form: NgForm) {
        this.isSubmitted = true;
        . . .
        if (form.valid) {
            . . .
        }  
    }
}

Then use this variable to assign a CSS class to the form using ngClass:
<form #propertyDetailsForm="ngForm" (ngSubmit)="addPropertyDetails(propertyDetailsForm)" [ngClass]="{'submitted': isSubmitted}">
    . . .
</form>

Finally, add the style in the page stylesheet. By default, invalid input field will be assigned ng-invalid class by Angular.
.submitted input.ng-invalid {
    border: 1px solid #f00;
}

Monday, 29 July 2019

ASP.NET Core Client Server JWT Authentication

Recently, I was trying to play around with ASP.NET Core JWT Authentication with Web API as backend server and Angular as front end client. I used ASP.NET Core 2.1 version.

Web API server setup
1. Add these codes inside ConfigureService method on Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
 services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(options =>
  {
   options.TokenValidationParameters = new TokenValidationParameters
   {
    ValidateIssuer = true,
    ValidateAudience = true,
    ValidateLifetime = true,
    ValidateIssuerSigningKey = true,

    ValidIssuer = "ABC",
    ValidAudience = "DEF",
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("mySuperSecretKey"))
   };
  });

 services.AddCors(options =>
 {
  options.AddPolicy("EnableCORS", builder =>
  {
   builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().AllowCredentials().Build();
  });
 });
}
Notice that we also need to enable Cross Origin Resource Sharing (CORS) as our Angular client will sit on different domain.

2. Then inside Configure method, add:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
 if (env.IsDevelopment())
 {
  app.UseDeveloperExceptionPage();
 }
 else
 {
  app.UseHsts();
 }

 app.UseAuthentication();

 app.UseCors("EnableCORS");

 app.UseHttpsRedirection();
 app.UseMvc();
}
Make sure it is before app.UseMvc(); line otherwise you will keep getting ‘401 Unauthorized’ message with no details. Also we need to add CORS setting that we have done.

3. On the Web API login method on controller:
[EnableCors("EnableCORS")]
[HttpPost, Route("login")]
public IActionResult Login([FromBody]LoginModel user)
{
 if (user == null)
 {
  return BadRequest("Invalid client request");
 }

 if (user.UserName == "user" && user.Password == "password")
 {
  var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("mySuperSecretKey "));
  var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);

  var tokeOptions = new JwtSecurityToken(
   issuer: "ABC",
   audience: "DEF",
   claims: new List<Claim>(),
   expires: DateTime.Now.AddMinutes(5),
   signingCredentials: signinCredentials
  );

  var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
  return Ok(new { Token = tokenString });
 }
 else
 {
  return Unauthorized();
 }
}
Make sure [EnableCors("EnableCORS")] is added either to the method or at controller level.

4. On our Angular client we will need to call login:
login(form: NgForm) {
    let credentials = JSON.stringify(form.value);
    this.http.post("http://localhost:5000/api/auth/login", credentials, {
      headers: new HttpHeaders({
        "Content-Type": "application/json"
      })
    }).subscribe(response => {
      let token = (<any>response).token;
      localStorage.setItem("jwt", token);
      this.invalidLogin = false;
      this.router.navigate(["/"]);
    }, err => {
      this.invalidLogin = true;
    });
}
Once authenticated, we use local storage to store the token with ‘jwt’ key.

5. Then on our secured resource controller:
[EnableCors("EnableCORS")]
[HttpGet, Authorize]
public IEnumerable<string> Get()
{
 return new string[] { "confidential one", " confidential two" };
}

6. To request that API, we simply use this on client side:
let token = localStorage.getItem("jwt");
this.http.get("http://localhost:5000/api/customer", {
  headers: new HttpHeaders({
 "Authorization": "Bearer " + token
  })
}).subscribe(response => console.log(response)); 

Note that we need to have "Authorization": "Bearer " + token in the request header so that the server can authorize it.  

Friday, 26 April 2019

Angular Directive Example to Format Value to Currency

The following is an example of an Angular 6 directive that format input value to currency when losing focus. It uses NgModel for value binding and CurrencyPipe for the formatter.
import { Directive } from '@angular/core';
import { NgModel } from '@angular/forms';
import { CurrencyPipe } from '@angular/common';

@Directive({
  selector: '[ngModel][my-directive]',
  providers: [NgModel, CurrencyPipe],
  host: {
    '(blur)': 'onInputChange($event)'
  } 
})
export class MyDirective {

  constructor(private model: NgModel, private currencyPipe: CurrencyPipe) { }

  onInputChange($event) {
    var value = $event.target.value;
    if (!value) return;

    var plainNumber: number;
    var formattedValue: string;


    var decimalSeparatorIndex = value.lastIndexOf('.'); 
    if (decimalSeparatorIndex > 0) {
      // if input has decimal part
      var wholeNumberPart = value.substring(0, decimalSeparatorIndex);
      var decimalPart = value.substr(decimalSeparatorIndex + 1);
      plainNumber = parseFloat(wholeNumberPart.replace(/[^\d]/g, '') + '.' + decimalPart)
    } else {
      // input does not have decimal part
      plainNumber = parseFloat(value.replace(/[^\d]/g, ''));
    }

    if (!plainNumber) {
      formattedValue = '';
    }
    else {
      formattedValue = this.currencyPipe.transform(plainNumber.toFixed(2), "USD", "symbol-narrow");
    }

    this.model.valueAccessor.writeValue(formattedValue);
  }
}

Then to use it on HTML part:
<input name="productPrice" [(ngModel)]="price" my-directive />

Friday, 5 April 2019

Angular Directive Example to Allow Certain Values

Below is an example of an Angular 6 Directive. This directive detects changes in NgModel as user enters in input value and only allows specific value to be entered while rejecting the others.
import { Directive } from '@angular/core';
import { NgModel } from '@angular/forms';

@Directive({
  selector: '[ngModel][my-directive]',
  providers: [NgModel],
  host: {
    '(ngModelChange)': 'onInputChange($event)'
  } 
})
export class MyDirective {

  constructor(private model: NgModel) { }

  onInputChange(value) {
    console.log(value);
    
    this.model.valueAccessor.writeValue(value.replace(/[^\d\.\,\s]+/g, ''));
  }
}

Then to use it on HTML part:
<input name="productPrice" [(ngModel)]="price" my-directive />

Friday, 22 March 2019

Angular Form, ngModel and Detecting Changes

Angular (at the moment of writing is version 8) has two flavours when coming down to forms. We can use Template Driven or Reactive Form. Template Driven form is useful for simple form that does not need much customisation. It is similar to the form in AngularJS. Angular will create form model representation and data binding in the background with some default structure and names. That is why it is called Template Driven.

Reactive Form, on the other hand is much more flexible however requires more effort in setting up. This form uses more code in component and less in HTML. Reactive form is best to handle complex scenarios and allow easier unit testing.

We are going to see an example of Template Driven form with ngModel binding.
<form name="personNameForm" novalidate="" ng-submit="submitForm()">
 <div>
  <input type="text" name="firstName" class="textinput"
                 [(ngModel)]="person.firstName"
                 (ngModelChange)="combineNames()"
                 required />
  <input type="text" name="lastName" class="textinput"
                 [(ngModel)]="person.lastName"
                 (ngModelChange)="combineNames()"
                 required />
 </div>
</form>
Note that we need to put form's name attribute and for each ngModel field a name attribute is also required.
We use ngModelChange directive to detect changes for NgModel value. $watch and $observe are not required anymore.

Tuesday, 4 December 2018

Angular Material Table with Server Side Data

In this post we will build an Angular Material Table with paging and sorting from server side api. The codes are using Angular Material 6.

We will need to create a service and data source that can be consumed by Angular component to populate and refresh the Material table. We will start with basic codes without paging and sorting initially then add the features once the basic is already working.

1. Basic table with server side data
1.1. First, we create a service:
import { Injectable }   from '@angular/core';
import { HttpClient }   from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from './models/user.model';

@Injectable()
export class UserService {
  private serviceUrl = 'http://myserviceurl';
  
  constructor(private http: HttpClient) { }
  
  getUser(): Observable<User[]> {
    return this.http.get<User[]>(this.serviceUrl);
  }  
}

1.2. Then a data source that inherits from Angular DataSource class:
import { CollectionViewer, DataSource } from "@angular/cdk/collections";
import { Observable } from 'rxjs';
import { UserService } from "./user.service";
import { User } from './models/user.model';

export class UserDataSource extends DataSource<any> {
  constructor(private userService: UserService) {
    super();
  }
  connect(): Observable<User[]> {
    return this.userService.getUser();
  }
  disconnect() { }
}

1.3. Then the Angular component that will consume the data source:
import { Component, ViewChild, AfterViewInit, OnInit} from '@angular/core';
import { MatPaginator, MatTableDataSource } from '@angular/material';
import { MatSort } from '@angular/material';

import { UserService } from './user.service';
import { UserDataSource } from './user.datasource';
import { User } from './models/user.model';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit, OnInit{
  displayedColumns: string[] = ['studentId', 'studentNumber', 'firstName', 'lastName'];
  user: User;
  dataSource: UserDataSource;
   

  constructor(private userService:UserService) {
  }

  ngAfterViewInit() {
  }

  ngOnInit() {
    this.dataSource = new UserDataSource(this.userService);
  }
}

1.4. Finally the HTML template:
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
   <ng-container matColumnDef="studentId">
     <th mat-header-cell *matHeaderCellDef> Student Id </th>
     <td mat-cell *matCellDef="let user"> {{user.studentId}} </td>
   </ng-container>
   <ng-container matColumnDef="studentNumber">
     <th mat-header-cell *matHeaderCellDef> Student Number </th>
     <td mat-cell *matCellDef="let user"> {{user.studentNumber}} </td>
   </ng-container>
   <ng-container matColumnDef="firstName"> 
     <th mat-header-cell *matHeaderCellDef> First Name </th>
     <td mat-cell *matCellDef="let user"> {{user.firstName}} </td>
   </ng-container>
     <ng-container matColumnDef="lastName">
     <th mat-header-cell *matHeaderCellDef> Last Name </th>
     <td mat-cell *matCellDef="let user"> {{user.lastName}} </td>
   </ng-container>

   <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
   <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

Once this is working, we can add the paging feature.

2. Add paging feature
2.1. Import paging module to the app module
import { MatPaginatorModule } from '@angular/material/paginator';
@NgModule({
  . . .,
  imports: [
    . . .
    MatPaginatorModule
  ],
 . . .
})

2.2. Add paginator element on HTML template:
<mat-paginator [pageSizeOptions]="[100, 500]"></mat-paginator> 

2.3. Modify getUser() method in the service class:
getUser(pageIndex : number =1, pageSize : number): Observable<User[]> {

   return this.http.get<User[]>(this.serviceUrl, {
      params: new HttpParams()
        .set('pageIndex', pageIndex.toString())
        .set('pageSize', pageSize.toString())
   });
}  

2.3. Modify the data source class:
export class UserDataSource implements DataSource<User> {
   // add variables to hold the data and number of total records retrieved asynchronously
   // BehaviourSubject type is used for this purpose
   private usersSubject = new BehaviorSubject<User[]>([]);

   // to show the total number of records
   private countSubject = new BehaviorSubject<number>(0);
   public counter$ = this.countSubject.asObservable();

   constructor(private userService: UserService) {
  
   }
  
   loadUsers(pageIndex: number, pageSize: number) {
  
      // use pipe operator to chain functions with Observable type
      this.userService.getUser(pageIndex, pageSize)
      .pipe(
         catchError(() => of([])),
         finalize()
      )
      // subscribe method to receive Observable type data when it is ready
      .subscribe((result : any) => {
         this.usersSubject.next(result.data);
         this.countSubject.next(result.total);
        }
      );
   }
  
   connect(collectionViewer: CollectionViewer): Observable<User[]> {
      console.log("Connecting data source");
      return this.usersSubject.asObservable();
   }

   disconnect(collectionViewer: CollectionViewer): void {
      this.usersSubject.complete();
      this.countSubject.complete();
   }
}

2.4. Modify component class:
// import ViewChild, MatPaginator and MatTableDataSource
import { ViewChild } from '@angular/core';
import { MatPaginator, MatTableDataSource } from '@angular/material';

export class AppComponent implements AfterViewInit, OnInit{

   . . .

   @ViewChild(MatPaginator) paginator: MatPaginator;

   ngAfterViewInit() {

      this.dataSource.counter$
      .pipe(
         tap((count) => {
            this.paginator.length = count;
         })
      )
      .subscribe();

      // when paginator event is invoked, retrieve the related data
      this.paginator.page
      .pipe(
         tap(() => this.dataSource.loadUsers(this.paginator.pageIndex, this.paginator.pageSize))
      )
      .subscribe();
   }  

   ngOnInit() { 
      // set paginator page size
      this.paginator.pageSize = 100;

      this.dataSource = new UserDataSource(this.userService);
      this.dataSource.loadUsers(this.paginator.pageIndex, this.paginator.pageSize);  
   }
}

For the server side api, we need to use a class that can hold data and records count like:
public class PagingResult<T>
{
   public IEnumerable<T> Data { get; set; }

   public int Total { get; set; }
}

Then the codes to retrieve data will look something like:
return new PagingResult<Student>()
{
   Data = query.Skip(pageIndex * pageSize).Take(pageSize).ToList(),
   Total = query.Count()
};

Once the paging works, we can move to the sorting functionality.

3. Add sorting functionality
3.1. Import sorting module to the app module
import { MatSortModule } from '@angular/material/sort;
@NgModule({
   . . .,
   imports: [
      . . .
      MatSortModule
   ],
   . . .
})

3.2. Add sorting element to the table and headers on HTML template:
<table mat-table [dataSource]="dataSource" matSort matSortDisableClear>

. . .

<th mat-header-cell *matHeaderCellDef mat-sort-header> Student Id </th>

<th mat-header-cell *matHeaderCellDef mat-sort-header> First Name </th>

3.3 Modify the component class:
import { MatSort } from '@angular/material';

. . .

export class AppComponent implements AfterViewInit, OnInit{
   . . .
  
   @ViewChild(MatSort) sort: MatSort;
   ngAfterViewInit() {
      . . .

      merge(this.paginator.page, this.sort.sortChange)
      .pipe(
         tap(() => this.dataSource.loadUsers(this.paginator.pageIndex, this.paginator.pageSize, this.sort.active, this.sort.direction))
      )
      .subscribe();
   }

   . . .

   ngOnInit() {
      this.paginator.pageSize = 100;
      this.sort.active = 'firstName';
      this.sort.direction = 'asc';

      this.dataSource = new UserDataSource(this.userService);
      this.dataSource.loadUsers(this.paginator.pageIndex, this.paginator.pageSize, 'firstName', 'asc');
   }
}

Reference:
Angular Material Data Table: A Complete Example (Server Pagination, Filtering, Sorting)

Wednesday, 31 October 2018

Adding Sorting Functionality to Angular Material Table

Below are the steps of how to add sorting feature of pre-fetched data to Angular Material 6 Table:

- on html page, add matSort to the table and mat-sort-header to each column that we would like to enable:
<table mat-table . . . matSort>
    <ng-container matColumnDef="name">
      <th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
      <td mat-cell *matCellDef="let element"> {{element.name}} </td>
    </ng-container>
    <ng-container matColumnDef="weight">
      <th mat-header-cell *matHeaderCellDef mat-sort-header> Weight </th>
      <td mat-cell *matCellDef="let element"> {{element.weight}} </td>
    </ng-container>
    <ng-container matColumnDef="symbol">
      <th mat-header-cell *matHeaderCellDef mat-sort-header> Symbol </th>
      <td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
    </ng-container>
. . .
</table>

- on module page (e.g., module.ts):
import { MatSortModule } from '@angular/material/sort;
@NgModule({
  . . .,
  imports: [
    . . .
    MatSortModule
  ],
. . .
})

- on component page (e.g., component.ts):
import { MatSort } from '@angular/material';
export class AppComponent implements AfterViewInit, OnInit{
  . . .
 
  @ViewChild(MatSort) sort: MatSort;
 
 
  ngAfterViewInit() {
    . . .
    this.dataSource.sort = this.sort;
  }
 
  . . .
}

I will create a post of how to do paging and sorting with server-side data interaction soon.

Friday, 21 September 2018

Adding Paginator to Angular Material Table

To add paging feature of pre-fetched data to Angular Material 6 Table:
- on html page add mat-paginator to the bottom (or top) of the table:
<mat-paginator [pageSizeOptions]="[2, 5, 10]"></mat-paginator>

- on module page (e.g., module.ts):
//import MatPaginatorModule
import { MatPaginatorModule } from '@angular/material/paginator';
@NgModule({
  . . .,
  imports: [
    . . .
    MatPaginatorModule
  ],
. . .
})

- on component page (e.g., component.ts):
// import ViewChild, MatPaginator and MatTableDataSource
import { ViewChild } from '@angular/core';
import { MatPaginator, MatTableDataSource } from '@angular/material';

export class AppComponent implements AfterViewInit, OnInit{

  . . .

  dataSource : MatTableDataSource<MyDataModel>;

  @ViewChild(MatPaginator) paginator: MatPaginator;

  ngAfterViewInit() {
    // set datasource paginator
    this.dataSource.paginator = this.paginator;
  }

  ngOnInit() {
    // initialise the datasource
    this.dataSource = new MatTableDataSource<MyDataModel>(MY_DATA);
  }
}

I will create a post of how to do paging and sorting with server-side data interaction soon.


Tuesday, 4 September 2018

How to Add Angular Material Table

Quick reference of how to add Angular Material Table (Angular Material 6):
- on module page (e.g., module.ts):
// import the module
import { MatTableModule } from '@angular/material';
@NgModule({
  . . .,
  imports: [
    . . .
    MatTableModule
  ],
 . . .
})

- on component page (e.g., component.ts):
export class AppComponent implements AfterViewInit, OnInit{
  . . .
// specify columns to be displayed
  displayedColumns : string[] = ['columnone', 'columntwo'];

// specify data to be displayed
  dataSource = [{columnone: 1, columntwo: ‘A’},{columnone: 2, columntwo: ‘B’},…]
  . . .
}

- on html page:
<table mat-table [dataSource]="dataSource">
  <ng-container matColumnDef="columnone">
    <th mat-header-cell *matHeaderCellDef> ColumnOne </th>
    <td mat-cell *matCellDef="let elem"> {{elem.columnone}} </td>
  </ng-container>

  <ng-container matColumnDef="columntwo">
    <th mat-header-cell *matHeaderCellDef> ColumnTwo </th>
    <td mat-cell *matCellDef="let elem"> {{elem.columntwo}} </td>
  </ng-container>

  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

We will see how to add Angular Material Paginator on the next post.

Tuesday, 28 August 2018

Installing Angular Material 6

At the time of writing, Angular Material version is 6.4.6. It needs Angular version 6, so we need to make sure we have the recent version. Below are NPM command lines to install latest version of Angular, Angular Material and packages that it requires:
// make sure to remove older version of Angular first
npm uninstall -g @angular/cli
npm uninstall -g angular-cli

// clean the cache and verify
npm cache clean
npm cache verify

// installing the packages
npm install -g @angular/cli
// Yarn package manager is required by Angular CLI
npm install -g yarn

// check the version
ng –version

// create a new Angular application
ng new angularmaterial

cd angularmaterial

// serve the application and show in browser 
ng serve

// install Angular Material and required Angular CDK
npm install --save @angular/material @angular/cdk

The application shown on the browser should automatically refresh every time you make changes to the codes. If it is not working try to put some polling interval, like:
ng serve --poll=2000