Angular

Basics of front-end Angular development for web applications for Java developers.

What is Angular?

How Angular Differs from Traditional Websites

Traditional Websites Angular
Traditional Websites have a browser request an HTML page from a website from a server Angular applications are designed to send the entire site upon request, so there are no further requests made to the server.
The has the advantage of being super fast and responsive
Traditional Websites would process a template (eg. JSP) and Data to create the HTML page Angular is already going to have that pages deigned even if the Data appears later
The data is requested from the server using an architecture such as REST

Setting Up a Project

Angular Architecture

Component File Structure

A Code Snippet

    import {Component, EventEmitter, Output} from '@angular/core';

    @Component({
      selector: 'app-header',
      templateUrl: './header.component.html',
      styleUrls: ['./header.component.css']
    })
    export class HeaderComponent {

      constructor() { }

      @Output()
      pageRequested = new EventEmitter<number>();

      onChangePage(page: number): void {
        console.log("page " + page +  "clicked");
        this.pageRequested.emit(page);
      }
    }

Not a lot to discuss here since the class above is very similar to java. Just a couple key differences:

A tag for this component in HTML would look like: <app-header></app-header>

Template Variables

<app-page1 [hidden]="currentPage !==1"></app-page1> <app-page3 *ngIf="headerComponent.pageRequested === 3"></app-page3>

Component Interaction

Services and Dependency Injection

The Observer Design Pattern

  ngOnInit(): void {
  	// This will automatically update if the books array is changes.
    // Because it is a referenced variable
    this.books = this.dataService.books;
    // This will not update automatically if the books array changes.
    // It is a local variable.
    this.numBooksByMe = this.books.filter( it => it.author === 'Me' ).length;
  }
    this.dataService.bookAddedEvent.subscribe(
      // first var: the incoming data
      (newBook) => {
        if (newBook.author === 'Me') {
          this.numBooksByMe++;
        }
      },
      // second var: error handling
      (error) => {
        console.log('An error occurred:', error);
      },
      // Optional complete event always takes no parameters
      () => {
      	// complete event
      }
    );
@NgModule({
  declarations: [
    AppComponent,
    Page1Component,
    HeaderComponent,
    Page2Component,
    Page3Component,
    FooterComponent
  ],
  imports: [
    BrowserModule
  ],
  // When provided with <InterfaceName> use <ServiceName>
  providers: [ {provide : 'DataServiceInterface' , useClass : DataService} ],
  bootstrap: [AppComponent]
})
export class AppModule { }

A Note on .gitignore

By default, Github will exclude /node_modules in the .gitignore file. This is good because this folder can be very large, and the contents can be easily obtained when opening an Angular project. However, if using Continous Integration, the build will fail without these files. So including these files should be concidered if using a workflow which automatically deploys.

TypeScript

Intro to TypeScript

TS Data Types

(primative types are lower case)

  exploringArrays() {
    const myArray1 = new Array<number>();
    const myArray2: number[] = [1, 2, 3, 4];
  }

Take the below code for example

export class AppComponent {
  title = 'ExploringTypeScript';

  readonly myNumber: number;

  someMethod(): void {
    let anotherNumber;
    const aThirdNumber = 1;

    anotherNumber = 2;
  }
}

Working with Arrays

Loops

for (let i = 0; i < 10; i++) {
	console.log(i);
} 

for (let next of myArrray){
	console.log(next);
}

Classes and Objects

Because TypeScript is compiled to JavaScript classes don't actaully exist at runtime.

export class Book {
  title: string;
  author: string;
  price: number;
  readonly id: number = 1;

  constructor(author: string, title?: string) {
    this.author = author;
    if (title) {
      this.title = title;
    }
  }
}
enum SubjectArea {
  ART, HISTORY, SCIENCE, LITURATURE
}

printArrays(): void {
    for (const subject in SubjectArea) {
      if (true) {	// the if statement just surpresses a warning
        console.log(subject); 
      }
    } // output: 0, 1, 2, 3, ART, HISTORY, SCIENCE, LITURATURE

    const enumArray = Object.keys(SubjectArea);
    for (const value of enumArray.slice(enumArray.length / 2)) {
      console.log(value);
    } // output: ART, HISTORY, SCIENCE, LITURATURE
}

export enum SubjectArea {
  ART = 'Arts and Shit', HISTORY = 'History', SCIENCE = 'Science and Math', LITURATURE = 'English'
}

printArrays(): void {
	let label;
    for (const subject in SubjectArea) {
        console.log(subject);
        console.log(SubjectArea[subject]);
        if (SubjectArea[subject] === 'History') {
          label = subject;
        }
    }

    let label2 = Object.keys(SubjectArea).find( it => SubjectArea[it] === 'History' );
}

Methods and Functions

methodName(paramName: paramType): ReturnType

export class User {
  id: number;
  name: string;

  getRole(): string {
    return 'standard';
  }

  static fromHttp(user: User): User {
    const newUser = new User();
    newUser.id = user.id;
    newUser.name = user.name;
    return newUser;
  }
}
    this.dataService.getUser(13).subscribe(
      next => {
        console.log(next);
        console.log(typeof next);
        let user: User = next;
        console.log(user)
        console.log(typeof user)
        let user2: User = next as User;
        console.log(user2)
        console.log(typeof user2)
        let user3: User = User.fromHttp(next);
        console.log(user3);
        console.log(typeof user3)
		// this will only work for users created from the created method
        console.log(user3.getRole()) 
      });

Output:

{ id: 13, name: "matt" }
object
{ id: 13, name: "matt" }
object
{ id: 13, name: "matt" }
object
{ id: 13, name: "matt" }
object
standard

Reminder: JavaScript SUCKS

JavaScript is not an 'opinionated' langauge, meaning there are a million ways to do the same fucking thing with no reccomendation on how to optimize. Ex:

console.log(`To buy this book it will cost: ${myBook.priceWithTax(.2)} dollars`);
console.log('To buy this book it will cost: ' + myBook.priceWithTax(.2) + ' dollars');
console.log('To buy this book it will cost: ', myBook.priceWithTax(.2), ' dollars');

All do the same thing. Notice the backticks instead of quotes in the first.

    const oddNumbers = numbers.filter(
      num => {
        return num % 2 === 1;
      }
    );
	
    const evenNumbers = numbers.filter( num => num % 2 === 0 );

But by far the most mind-bogglingly stupid feature of JS is eqaulity.       == Abstract eqaulity       === Strict eqaulity The short story is always use Strict eqaulity. As abstract eqaulity will attempt to cast parameters to the same type before comparision.

Debugging and Testing

Note for full Intellij debug compatabillity you must use Google Chrome. It's also sort of a hassle to configure linux for tests the first time; ensure CHROME_BIN env variable is set to chromium, or add a firefox config to Karma.js

Debugging (In Intellij)

Go to 'Edit Configurations...', then find JavaScript Debgger, there is a default Angular config

Testing

// Set up testing eviornment
fdescribe('DataService', () => {
  let service: DataService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(DataService);
  });

  // tests begin with it
  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  // focused test
  fit('addBook increases size of books', () => {
    const book = new Book();
    const before = service.books.length;
    service.addBook(book);
    expect(service.books.length).toBeGreaterThan(before);
  });
});

Angular Environments

Angular environment variables are store in a folder under src/ called environments in files environment.ts and environment.prod.ts for testing and production. It contains key value pairs. We can import the environment for access to all these variables.

NEVER import environment.prod.ts. Import environment.ts

To run Angular with production enviornment: ng serve --prod which is a shortcut for ng serve -c production (Although in actual deployment we would not use the Angular server) Angular.json tells the app where to get production and regular environment variables, and can even replace files when certian environments are activated It's a good idea to have at least 3 enviornments; One for Production, for development or localhost, and a local version that has data stored internally.

Building a Site

Bootstrap

			...
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.css",
              "node_modules/bootstrap/dist/css/bootstrap.min.css"
            ],
            "scripts": [
              "node_modules/jquery/dist/jquery.min.js",
              "node_modules/popper.js/dist/umd/popper.min.js",
              "node_modules/bootstrap/dist/js/bootstrap.min.js"
            ]
          },
		  ...

This would apply the css in styles.css to and node modules every page in the application

Routing Basics

const routes: Routes = [
  // The extension of the URL, and what component or action should be loaded
  { path : 'admin/users', component : UsersComponent }
];

@NgModule({
  declarations: [
    AppComponent,
    MenuComponent,
    CalendarComponent,
    RoomsComponent,
    UsersComponent
  ],
  imports: [
    BrowserModule,
	// It is nessecary to add this module with the routes
	// It never changes, cerimonial code.
    RouterModule.forRoot(routes)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Then in app.component.html:

<div class="container">
  <app-menu></app-menu>
  <router-outlet></router-outlet>
</div>

router-outlet will be replaced with whatever component it finds in the router module with the given URL

// Note: It's usaully production standard to put these in a seperate file
// called app.routing.module.ts
const routes = [
  { path : '', component : `HomeComponent },
  { path : '404', component : PageNotFoundComponent },
  { path : "**", redirectTo : '/404' }
];

The wild card must always come at the end

  ...
  <a class="dropdown-item" (click)="navigateToRoomsAdmin()">Rooms</a>
  ...

In the menu.component.ts class:

@Component({
  selector: 'app-menu',
  templateUrl: './menu.component.html',
  styleUrls: ['./menu.component.css']
})
export class MenuComponent implements OnInit {

  constructor(private router: Router) { }

  ngOnInit(): void {
  }

  navigateToRoomsAdmin() {
    // navigate to /admin/rooms
    this.router.navigate(['admin','rooms']);
  }

}

Routing for Sub-Components

  rooms: Array<Room>;
  selectedRoom: Room;

  constructor(private dataService: DataService, 
  private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.rooms = this.dataService.rooms;
    // inspect the URL to see if there is a parameter on the path
    this.route.queryParams.subscribe((params) => {
      const id = params['id'];
      if (id) {
        // cast a variable to a number using +
        this.selectedRoom = this.rooms.find( room => room.id === +id);
      }
    })
  }

Models and Views

  private rooms: Array<Room>;
  private users: Array<User>;

  getRooms(): Observable<Array<Room>> {
    return of(this.rooms);
  }

  getUsers(): Observable<Array<User>> {
    return of(this.users);
  }

Then in the ngInit function we subscribe to that event

  ngOnInit(): void {
    this.dataService.getRooms().subscribe(next => this.rooms = next );
	}

Pipes

Pipes allow us to change the way something is displayed, such as a date

selectedDate = new Date();

<p>The selected date is {{selectedDate | date:'yyyy-MM-dd'}}</p>

Check out some other pipe usages in the documentation

The alternative to the above would be

ngOnInit(): void {
  const date: string = formatDate(this.selectedDate, 'yyyy-MM-dd', 'en-US');
}

You could also specify the locale this way. But pipes are a pretty cool feature of Angular.

Forms

So far we've only looked at static website design and reading data. Forms allow users to input data. There are two different ways to do forms in Angular Template-driven and Reactive.

Also here's a cheatsheet on types of databinding in Angular, which i will go into more detail on later:

Template-Driven Forms

Template Form Validation

// When the input box is invalid 
// and has been touched change border to red
input.ng-invalid.ng-touched {
  border: 1px solid #f00;
} 
<form #userForm="ngForm">
...
<button type="submit" class="btn btn-primary" (click)="onSubmit()" [disabled]="userForm.invalid">Save</button>

Reactive Forms

  roomForm = new FormGroup({
    roomName : new FormControl('roomName')
  });
<form [formGroup]="roomForm"> 
  <div class="form-group">
    <label for="name">Name</label>
        <!-- Notice below the formControlName doesn't need to be bound with [] bc the formGroup has already been applied -->
    <input type="text" class="form-control" id="name" placeholder="room name" [formControlName]="roomName">
    <div class="alert alert-danger"></div>
  </div>
  <button type="button" class="btn btn-primary" (click)="onSubmit()">Save</button>
</form>
  ngOnInit(): void {
    this.roomForm.patchValue({
      roomName : this.room.name,
      location : this.room.location
    });
  }
  
  onSubmit(): void {
    this.room.name = this.roomForm.controls['roomName'].value;
    this.room.location = this.roomForm.value['location'];
    // TODO: Call a method in the dataService to save the room
  }
  constructor(private formBuilder: FormBuilder) {
 }

 ngOnInit(): void {
   this.roomForm = this.formBuilder.group({
     roomName : this.room.name,
     location : this.room.location
   });

   for (const layout of this.layouts) {
     const layoutCapacity = this.room.capacities.find( (lc) => lc.layout === Layout[layout]);
     const initialCapacity = layoutCapacity == null ? 0: layoutCapacity.capacity;
     this.roomForm.addControl(`layout${layout}`, this.formBuilder.control(initialCapacity));
   }
 }
  <div class="form-group" *ngFor="let layout of layouts">
   <label for="layout{{layout}}">{{ layoutEnum[layout] }}</label>
     <input type="number" class="form-control" id="layout{{layout}}" formControlName="layout{{layout}}">
 </div>
    this.roomForm = this.formBuilder.group({
     roomName : [this.room.name , Validators.required],
     location : [this.room.location, [Validators.required, Validators.minLength(2)]]
   });

Connecting to a REST Endpoint

Choosing a Backend

HTTP VERB URL Action
GET /api/rooms/ Get All Rooms
GET /api/rooms/123 Get the room with id 123
POST /apt/rooms Add a Room
PUT /api/rooms Update a Room

One important thing before we get into accessing REST api from angular: Never store plaintext passwords in Angular. Since all Angular code is run through the browser it wouldn't be hard to crack.

Connecting Angular

  constructor(private http: HttpClient) {
    console.log(environment.restUrl);
  }

  getUser(id: number): Observable<User> {
    return this.http.get<User>(environment.restUrl + '/api/users/' + id);
  }

The get method above accepts a generic type as a hint to the compiler. getUser is actually returning a JS Object with fields that match the user class, but it's not actaully an instance of a User.

Configuring CORS

@Configuration
public class CORSConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedOrigins("http://localhost:4200"); 
    }
}

Pre-Processing REST Data

You could put all the preprocessing code in the dataService class subscription within the ngInit method, but that can lead to sloppy code. Instead use the pipe method within the dataService class function which is called for the same effect.

In the below example we create a JSON type user from the User object recieved from the api

export class User {
  id: number;
  name: string;

  static fromHttp(user: User): User {
    const newUser = new User();
    newUser.id = user.id;
    newUser.name = user.name;
    return newUser;
  }
}
	// some.component.ts
  ngOninit {
  	 this.dataService.getUser(13).subscribe(
      next => {
        console.log(next);
        console.log(typeof next);
        console.log(next.getRole())
    });
  }  
//data.service.ts
  getUser(id: number): Observable<User> {
    return this.http.get<User>(environment.restUrl + '/api/users/' + id)
      .pipe(
        map( data => {
          return User.fromHttp(data);
        })
      );

Dealing with Slow and Unresponsive Connections

Sometimes the data will not be available right away. In this case it would be nice to display something like "Loading, please wait."

To do this we create a boolean in the typescript file for the component loading the data

	message = "Loading... Please wait."
  loadingData = true;

  ngOnInit(): void {
    this.dataService.getRooms().subscribe(next => {
      this.rooms = next;
      this.loadingData = false;
    });
	}

And then we can query that parameter from the HTML

<div *ngIf="loadingData">{{message}}</div>

But what if the entire backend is down?

We can add an additional parameter to the data service to catch errors.

  message = "Please wait... getting the list of rooms"

  ngOnInit(): void {
    this.dataService.getRooms().subscribe(next => {
      this.rooms = next;
      this.loadingData = false;
    },
      (error) => {
        console.log('error', error);
        if (error.status === 402) {
          this.message = "Sorry - payment is required to use this application."
        } else {
          this.message = "Sorry, something went wrong. Please try again later.";         
        }
      });

For helpful error messages, make sure you properly set the return status code from the back end. Then you can customize the response based on the status code. By default a nonresponsive server returns status code 0 to the browser.

Retrying on error

The best way to do this is by creating a counter and recursively calling a load data function.

  reloadAttempts = 0;
  
  loadData() {
    this.dataService.getRooms().subscribe(next => {
        this.rooms = next;
        this.loadingData = false;
      },
      (error) => {
        console.log('error', error);
        if (error.status === 402) {
          this.message = 'Sorry - payment is required to use this application.'
        } else {
          this.reloadAttempts++;
          if (this.reloadAttempts <= 10) {
            this.message = 'Sorry, something went wrong. Trying again...';
            this.loadData()
          } else {
            this.message = 'Sorry, something went wrong. Please contact support.'
          }
        }
      });
  }

A Common Bug

Say you save some data and it redirects to the view page. The data might not be immedatly available if there is a delay on the backend, so the console will get an error like 'Object is null'.

To prevent this we can use the null check op in TypeScript:

  <table>
    <tr>
      <td>id</td><td>{{ room?.id }}</td>
    </tr>
    <tr>
      <td>name</td><td>{{ room?.name }}</td>
    </tr>
    <tr>
      <td>location</td><td>{{ room?.location }}</td>
    </tr>
  </table>
  <h4>This room can accomodate:</h4>
  <table *ngIf="room.capacities">
    <tr *ngFor="let layoutCapacity of room.capacities"> <!-- repeat the tr for each capacity -->
      <td>{{ layoutCapacity.layout }}</td><td>{{ layoutCapacity.capacity }}</td>
    </tr>
  </table>

So it will only display if the variable is not null. Note that this is one of the few situations to use this, it's generally not good practice.

Confirming Action

To confirm a delete, update, etc. It is very simple to accomplish in Angular

  deleteBooking(id: number): void {
    const result = confirm('Are you sure you wish to delete this booking?');
    if (result) {
      this.message = 'Deleting data...';
      this.dataService.deleteBooking(id).subscribe(
	  ...

Pre-Fetching Data and Using Resolvers

Concider the following component code for a calander app:

  ngOnInit(): void {
    this.dataService.getRooms().subscribe(
      next => this.rooms = next
    );
    this.dataService.getUsers().subscribe(
      next => this.users = next
    );

    const id = this.route.snapshot.queryParams['id']
    if (id) {
      this.dataService.getBooking(+id).subscribe(next => {
        this.booking = next;
        this.dataLoaded = true;
        this.message = '';
      });
    } else {
      this.booking = new Booking();
      this.dataLoaded = true;
      this.message = '';
    }

  }

We have 3 different data services making calls asyncrounsly. This could lead to problems when the page is loading, as the users/rooms might be unavailable when the booking is loaded. To solve this we could nest each data service call within the next block or increment a counter, but this makes the code syncrounous and slows down the user experience.

The solution is to create a resolver. A resolver is an object which resolves an observable. The resolver does the subscribing and then waits for the data to become available. In the above case we extract the subscription component for users and rooms from the calander component and add them to two new services: prefetch-rooms.service.ts:

import { Injectable } from '@angular/core';
import {Observable} from "rxjs";
import {Room} from "./model/Room";
import {Resolve} from "@angular/router";
import {DataService} from "./data.service";

@Injectable({
  providedIn: 'root'
})
export class PrefetchRoomsService implements Resolve<Observable<Array<Room>>>{

  constructor(private dataService: DataService) { }

  resolve() {
    return this.dataService.getRooms();
  }
}

prefetch-users.service.ts

...
Resolve<Observable<Array<User>>>{

  constructor(private dataService: DataService) { }

  resolve() {
    return this.dataService.getUsers();
  }
}

Then in app.module.ts we add the following to the routes:

const routes: Routes = [
  { path : 'editBooking', component : CalendarEditComponent, resolve : {rooms : PrefetchRoomsService, users : PrefetchUsersService}},
  { path : 'addBooking', component : CalendarEditComponent, resolve : {rooms : PrefetchRoomsService, users : PrefetchUsersService}},
  ...

So now when we navigate to addBooking or editBooking the resolvers are going to be set up so our data is available within the route, although it is hidden. Now we can extract the data into the edit-calander.component.ts as follows:


  constructor(private dataService: DataService,
              private route: ActivatedRoute,
              private router: Router) { }

  ngOnInit(): void {
    this.rooms = this.route.snapshot.data['rooms']
    this.users = this.route.snapshot.data['users']

    const id = this.route.snapshot.queryParams['id']
    if (id) {
      this.dataService.getBooking(+id)
        .pipe(
          map (booking => {
            booking.room = this.rooms.find(room => room.id === booking.room.id);
            booking.user = this.users.find(user => user.id === booking.user.id);
            return booking;
          })
        )
        .subscribe(next => {
        this.booking = next;
        this.dataLoaded = true;
        this.message = '';
      });
    } else {
      this.booking = new Booking();
      this.dataLoaded = true;
      this.message = '';
    }

  }

Securing an Angular Application

Security is constantly evolving. You'll need to do your own research on the specific vulnerabilities of your application. However, the following explains fundamental security services provided by Angular.

Before we move on understand the difference between the folowing two terms:

Authentication: Validiating a users credentials to access a system or part of a system

Authorization: Checking if a user has access to a restricted system.

Json Web Tokens

JWT Tokens are used to store an active session after a user has been authenticated.

Using JWTs there is no need to store the username and password directoly in memory. Everything needed for the server to authenticate and authorize exists in memory, and every token can expire after a set amount of time.

A typical JWT is a long string seperated into 3 parts seperated by the '.' symbol.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The Header contains 2 parameters: The algorithm used to decode the token and the token type, which in our case should always be "JWT"

{
  "alg": "HS256",
  "typ": "JWT"
}

Next is the Payload of the token, it is encoded in Base64. We can put anything we'd like into here. Never put sensitive information like passwords in here, as it can easily be decoded.

{
  "sub": "1234567890",
  "name": "John Doe",
  "role": admin
}

Finally the Signature, which is generated from the payload and header. Note it is encoded first using the header-defined algorithm, then Base64, so decoding it must be done in reverse order.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  { my-256-bit-secret }
) 

A private key stored on the server side is used to sign the token so we can be sure only our server issued the token.

Server Side JWT Usage

The first step is choosing your method of authentication. Whatever method of authentication you use on the backend is up to you. A few different types of authentication include:

Basic Auth: Comparing plaintext username/passwords directly on the server. Straightforward but remember it is bad practice to hard code any passwords into the code. If using this method I would recommend storing the hashed password in a relational database.

LDAP: Lightwieght Directory Acess Protocol is used for directory services authentication. Connect to an external server which holds credentials. Pretty complicated to set up but very secure when done right.

OAuth : OAuth/OAuth2 is an open standard for securing access to APIs. Connect to a existing third party login provider such as OKTA, Google, Facebook, etc. Some configuration is required and there is a bit of a learning curve, some api documentations are better than others.

On the backend implement the dependecies for whichever auth service you choose, and also any JWT dependencies that are needed. The next steps are as followed:

  1. Write a method to generate a Token, choose what fields you want in the payload.
  2. Choose method of authentication. RSA encrytption requires a public and private key. It is ok to have a new Key generated every startup

The below is written in Java and I'm not going into specifics on the classes used here because there are already a lot of good guides out there. I into more detail on RSA encryption and signing in my Ktor tutorial

	private RSAPrivateKey privateKey;
	private RSAPublicKey publicKey;
	
	@PostConstruct initKeys() throws NoSuchAlgorithmException {
		KeyPairGenerator generator = KeyParGenerator.getInstance("RSA");
		generator.initialize(2048);
		KeyPair keypair = generator.generateKeyPair();
		privateKey = (RSAPRivateKey) keypair.getPrivate();
		publicKey = (RSAPublicKey) keypair.getPublic();
	}
	
	public String generateToken(String name, String role) {
	...

Securely Storing the JWT Token

Returning the JWT Token as a HTTP respsonse puts the token in local javascript memory. This puts the application at risk of Cross-Site-Scripting. The production standard way of storing tokens are SSL-encrypted HTTP-only cookies.