Skip to main content

Angular

Promo talk

Angular includes:

  • A component-based framework for building scalable web applications
  • A collection of well-integrated libraries that cover a wide variety of features, including routing, forms management, client-server communication, and more
  • A suite of developer tools to help you develop, build, test, and update your code

With Angular, you're taking advantage of a platform that can scale from single-developer projects to enterprise-level applications.

Popularity

2024, stack overflow

  • Popularity: React 39.5%, Angular 17.1%, Vue.js 15.4%, AngularJS 6.8%, Svelte 6.5%, Solid.js 1.2%
  • Desired: React 33.4%, Vue.js 16.3%, Angular 13.9%, Svelte 11.5%, AngularJS 4.2%, Solid.js 3.6%
  • Admired: Svelte 72.8%, Solid.js 67%, React 62.2%, Vue.js 60.2%, Angular 53.4%, AngularJS 23.1%

According to the 2023 Survey, React is the most used framework, followed by Vue, with Angular trailing behind. However, Angular's comprehensive nature and features make it a preferred choice for complex, large-scale applications.

  • A comparison between Angular Vs React market ranking indicates that React is leading the way. 4.6% of all the websites use React, which is a JavaScript library market share of 5.7%. In contrast, 0.3% of all websites use Angular, making its JS library market share 0.3%.

State of JS 2024 survey

Install angular cli (and node)

npm install -g @angular/cli


# check install
ng v
     _                      _                 ____ _     ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/


Angular CLI: 19.2.11
Node: 20.15.0
Package Manager: npm 10.7.0
OS: darwin arm64

Angular:
...

Package Version
------------------------------------------------------
@angular-devkit/architect 0.1902.11 (cli-only)
@angular-devkit/core 19.2.11 (cli-only)
@angular-devkit/schematics 19.2.11 (cli-only)
@schematics/angular 19.2.11 (cli-only)

Install in VS code - Angular Language Service

Create new project

ng new angulartest01 --inline-style --inline-template --skip-tests
cd angulartest01
code .
## start up
ng dev
  • src/app - main component, routes and config
  • index.html, styles.css and main.ts - angular bootsrapper

Angular components

Three parts

  • template - html
  • code in TS
  • styles (local, encapsulated)
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
selector: 'app-root',
imports: [RouterOutlet],
template: `
<h1>Welcome to {{title}}!</h1>

<router-outlet />
`,
styles: [],
})
export class AppComponent {
title = 'angulartest01';
}

If you would like separate css and html files - do not use --inline-style --inline-template while creating new angular app.

For extrenal template add templateUrl with the relative address of this component's HTML template.

templateUrl: './counter-button.component.html',

First component

ng generate component components/header
import { Component } from '@angular/core';

@Component({
selector: 'app-header',
imports: [],
template: `
<p>
header works!
</p>
`,
styles: ``
})
export class HeaderComponent {

}

Use your component...

Add it to imports: [], and reference it in template with selector <app-header/> defined in component.

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { HeaderComponent } from './components/header/header.component';

@Component({
selector: 'app-root',
imports: [RouterOutlet, HeaderComponent],
template: `
<app-header />
<h1>Welcome to {{title}}!</h1>

<router-outlet />
`,
styles: [],
})
export class AppComponent {
title = 'angulartest01';
}

String interpolation - {{}}

import { Component } from '@angular/core';

@Component({
selector: 'app-header',
imports: [],
template: `
<p>
header works! - {{title}}
</p>
`,
styles: ``
})
export class HeaderComponent {
title ='First App';
}

Signals API - for dynamic data. Best Practice.

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

@Component({
selector: 'app-header',
imports: [],
template: `
<p>
header works! - {{title()}}
</p>
`,
styles: ``
})
export class HeaderComponent {
title = signal<string>('First App - signal');
}

Bootstrap

ng add @ng-bootstrap/ng-bootstrap

Restart dev server afterwards.

ng-bootstrap

or look into tailwindcss... tailwind

Component inputs and outputs

counter-button component.

import { Component, input, output } from '@angular/core';

@Component({
selector: 'app-counter-button',
imports: [],
template: `
<p>
<button (click)="clickHandler($event)">{{label()}} {{value()}}</button>
</p>
`,
styles: ``
})
export class CounterButtonComponent {
label = input('Add +1: ');
value = input(0);
btnClicked = output();

clickHandler(e: Event) {
console.log("Button clicked", e);
this.btnClicked.emit();
}
}

Using it in app.

import { Component, input } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { HeaderComponent } from './components/header/header.component';
import { CounterButtonComponent } from "./components/counter-button/counter-button.component";

@Component({
selector: 'app-root',
imports: [RouterOutlet, HeaderComponent, CounterButtonComponent],
template: `
<app-header />
<h1>Welcome to {{title}}!</h1>


<app-counter-button label="Kala maja" (btnClicked)="btnClicked()" [value]="value"></app-counter-button>

<router-outlet />

`,
styles: [],
})
export class AppComponent {
title = 'angulartest01';

value = 10;

btnClicked() {
console.log("Button clicked in app component");
this.value++;
}
}

Template syntax

{someValue} or {someFunc()}- string interpolation. someValue/someFunc get evaluated and result converted to string. (click)="someMethod()" - template statements.

[someproperty] = "expression" - binding.

<button type="button" [disabled]="isUnchanged">Save</button>

Different ways to bind:

  • one way, from data source to target
    {{expression}} or [target]="expression"
  • one way, from view target to data source
    (target)="statement"
  • two-way (bananas go inside the box...) [(target)]="expression"

Templates documentation

Services

Create with command

ng generate service services/counter

counter.service.ts

import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class CounterServiceService {

constructor() { }
}

Service is a broad category encompassing any value, function, or feature that an application needs. A service is typically a class with a narrow, well-defined purpose. It should do something specific and do it well.

Injectable registers it with DI container.

When Angular creates a new instance of a component class, it determines which services or other dependencies that component needs by looking at the constructor parameter types.

When Angular discovers that a component depends on a service, it first checks if the injector has any existing instances of that service. If a requested service instance doesn't yet exist, the injector makes one using the registered provider and adds it to the injector before returning the service to Angular.

Service:

import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class CounterService {

counter = 0;
constructor() { }
increment() {
this.counter++;
}

getCounter() {
return this.counter;
}

reset() {
this.counter = 0;
}
}

Button:

import { Component, input, output } from '@angular/core';
import { CounterService } from '../../services/counter.service';

@Component({

selector: 'app-counter-button',
imports: [],
template: `
<p>
<button (click)="clickHandler($event)">{{label()}} {{counterService.getCounter()}}</button>
</p>
`,
styles: ``
})
export class CounterButtonComponent {

constructor(public counterService: CounterService) { }

label = input('Add +1: ');

clickHandler(e: Event) {
this.counterService.increment();
}
}

App:

import { Component, input } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { HeaderComponent } from './components/header/header.component';
import { CounterButtonComponent } from "./components/counter-button/counter-button.component";
import { CounterService } from './services/counter.service';

@Component({
selector: 'app-root',
imports: [RouterOutlet, HeaderComponent, CounterButtonComponent],
template: `
<app-header />
<h1>Welcome to {{title}}!</h1>


<app-counter-button label="Kala maja" (btnClicked)="btnClicked()"></app-counter-button>

Current counter: {{counterService.getCounter()}}

<router-outlet />

`,
styles: [],
})
export class AppComponent {


constructor(public counterService: CounterService) { }

title = 'angulartest01';


btnClicked() {
console.log("Button clicked in app component");
}
}

Forms

Two approaches - reactive and template

Reactive:

import {Component} from '@angular/core';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
@Component({
selector: 'app-reactive-favorite-color',
template: `
Favorite Color: <input type="text" [formControl]="favoriteColorControl">
`,
imports: [ReactiveFormsModule],
})
export class FavoriteColorReactiveComponent {
favoriteColorControl = new FormControl('');
}

Reactive forms

Template:

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
imports: [FormsModule],
template: `
<main>
<h2>Hello {{ name }}!</h2>
<input type="text" [(ngModel)]="name" />
</main>
`
})
export class AppComponent {
name = 'Andres Käver';
}

Template forms

Reactive forms

Provide direct, explicit access to the underlying form's object model. Compared to template-driven forms, they are more robust: they're more scalable, reusable, and testable. If forms are a key part of your application, or you're already using reactive patterns for building your application, use reactive forms.
Data model - Structured and immutable

Template-driven forms
Rely on directives in the template to create and manipulate the underlying object model. They are useful for adding a simple form to an app, such as an email list signup form. They're straightforward to add to an app, but they don't scale as well as reactive forms. If you have very basic form requirements and logic that can be managed solely in the template, template-driven forms could be a good fit.
Data model - Unstructured and mutable

Directives - attribute

  • NgClass - Adds and removes a set of CSS classes.
  • NgStyle - Adds and removes a set of HTML styles.
  • NgModel - Adds two-way data binding to an HTML form element.
<!-- toggle the "special" class on/off with a property -->
<div [ngClass]="isSpecial ? 'special' : ''">This div is special</div>

Directives - structural

  • NgIf - Conditionally creates or disposes of subviews from the template.
  • NgFor - Repeat a node for each item in a list.
  • NgSwitch - A set of directives that switch among alternative views.

When NgIf is false, Angular removes an element and its descendants from the DOM. Angular then disposes of their components, which frees up memory and resources.

<app-item-detail *ngIf="isActive" [item]="item"></app-item-detail>

Use the NgFor directive to present a list of items.

<div *ngFor="let item of items">{{ item.name }}</div>
<div *ngFor="let item of items; let i=index">{{ i + 1 }} - {{ item.name }}</div>

The Angular <ng-container> is a grouping element that doesn't interfere with styles or layout because Angular doesn't put it in the DOM.
Use ng-container when there's no single element to host the directive.

Like the JavaScript switch statement, NgSwitch displays one element from among several possible elements, based on a switch condition. Angular puts only the selected element into the DOM.

<div [ngSwitch]="currentItem.feature">
<app-stout-item *ngSwitchCase="'stout'" [item]="currentItem"></app-stout-item>
<app-device-item *ngSwitchCase="'slim'" [item]="currentItem"></app-device-item>
<app-lost-item *ngSwitchCase="'vintage'" [item]="currentItem"></app-lost-item>
<app-best-item *ngSwitchCase="'bright'" [item]="currentItem"></app-best-item>
...
<app-unknown-item *ngSwitchDefault [item]="currentItem"></app-unknown-item>
</div>

Control flow (instead of structural directives)

@if (a > b) {
{{a}} is greater than {{b}}
} @else if (b > a) {
{{a}} is less than {{b}}
} @else {
{{a}} is equal to {{b}}
}
@for (item of items; track item.id) {
{{ item.name }}
}

@for block has

  • $count - Number of items in a collection iterated over
  • $index - Index of the current row
  • $first - Whether the current row is the first row
  • $last - Whether the current row is the last row
  • $even - Whether the current row index is even
  • $odd - Whether the current row index is odd

Use @empty block when for is empty

@for (item of items; track item.name) {
<li> {{ item.name }}</li>
} @empty {
<li aria-hidden="true"> There are no items. </li>
}

@switch

@switch (userPermissions) {
@case ('admin') {
<app-admin-dashboard />
}
@case ('reviewer') {
<app-reviewer-dashboard />
}
@case ('editor') {
<app-editor-dashboard />
}
@default {
<app-viewer-dashboard />
}
}

@switch does not have a fallthrough - no break statement;

Slot

<ng-content> is a special element that accepts markup or a template fragment and controls how components render content.

@Component({
selector: 'button[baseButton]',
template: `
<ng-content />
`,
})

Http requests

Service:

@Injectable({providedIn: 'root'})
export class UserService {
private http = inject(HttpClient);
getUser(id: string): Observable<User> {
return this.http.get<User>(`/api/user/${id}`);
}
}
import { AsyncPipe } from '@angular/common';
@Component({
imports: [AsyncPipe],
template: `
@if (user$ | async; as user) {
<p>Name: {{ user.name }}</p>
<p>Biography: {{ user.biography }}</p>
}
`,
})
export class UserProfileComponent {
@Input() userId!: string;
user$!: Observable<User>;
private userService = inject(UserService);
constructor(): void {
this.user$ = this.userService.getUser(this.userId);
}
}

Http guide

Pipes

Pipes are a special operator in Angular template expressions that allows you to transform data declaratively in your template. Pipes let you declare a transformation function once and then use that transformation across multiple templates. Angular pipes use the vertical bar character ( ), inspired by the Unix pipe.

Built in pipes

  • AsyncPipe - Read the value from a Promise or an RxJS Observable.
  • CurrencyPipe - Transforms a number to a currency string, formatted according to locale rules.
  • DatePipe - Formats a Date value according to locale rules.
  • DecimalPipe - Transforms a number into a string with a decimal point, formatted according to locale rules.
  • I18nPluralPipe - Maps a value to a string that pluralizes the value according to locale rules.
  • I18nSelectPipe - Maps a key to a custom selector that returns a desired value.
  • JsonPipe - Transforms an object to a string representation via JSON.stringify, intended for debugging.
  • KeyValuePipe - Transforms Object or Map into an array of key value pairs.
  • LowerCasePipe - Transforms text to all lower case.
  • PercentPipe - Transforms a number to a percentage string, formatted according to locale rules.
  • SlicePipe - Creates a new Array or String containing a subset (slice) of the elements.
  • TitleCasePipe - Transforms text to title case.
  • UpperCasePipe - Transforms text to all upper case.

Routing

Define routes

import {Routes} from '@angular/router';
import {HomeComponent} from './home/home.component';
export const routes: Routes = [
{
path: '',
title: 'App Home Page',
component: HomeComponent,
},
];

Use routerLink

import { RouterLink, RouterOutlet } from '@angular/router';
@Component({
...
template: `
...
<a routerLink="/">Home</a>
<a routerLink="/user">User</a>
...
`,
imports: [RouterLink, RouterOutlet],
})

Routing

Observable pattern (rxjs) - Reactive programming

The pattern’s aim is to define a one-to-many relationship such that when one object changes state, the others are notified and updated automatically.

observer

  • Subject: It is considered as the keeper of information, of data or of business logic.
  • Register/Attach: Observers register themselves to the subject because they want to be notified when there is a change.
  • Event: Events act as a trigger in the subject such that all the observers are notified.
  • Notify: Depending on the implementation, the subject may “push” information to the observers, or, the observers may “pull” if they need information from the subject.
  • Update: Observers update their state independently from other observers however their state might change depending on the triggered event.
const node = document.querySelector('input');
const p = document.querySelector('p');

function Observable(subscribe) {
this.subscribe = subscribe;
}

Observable.fromEvent = (element, name) => {
return new Observable(observer => {
const callback = event => observer.next(event);
element.addEventListener(name, callback, false);
return () => element.removeEventListener(name, callback, false);
});
};

const input$ = Observable.fromEvent(node, 'input');

const unsubscribe = input$.subscribe({
next: event => {
p.innerHTML = event.target.value;
},
});

// automatically unsub after 5s
setTimeout(unsubscribe, 5000);

Account service

@Injectable({
providedIn: 'root'
})
export class AccountService {

private apiUrl = 'http://localhost:5171/api/v1/account/';

constructor(private httpClient: HttpClient) {

}

// returns observable

login(email: string, password: string): Observable<IJwtResponse> {
return this.httpClient.post<IJwtResponse>(this.apiUrl + 'login', {email, password})
}

setJwt(jwt: IJwtResponse) {
localStorage.setItem('jwt', JSON.stringify(jwt));
}

getJwt(): string | null{
let jwt = localStorage.getItem('jwt');
if (jwt) {
let jwtData : IJwtResponse = JSON.parse(jwt);
return jwtData.jwt;
}
return null;
}

isAuthenticated(): boolean{
let jwt = localStorage.getItem('jwt');
return jwt ? true : false;
}

logout(){
localStorage.removeItem('jwt');
}

}
export interface IJwtResponse {
jwt: string,
refreshToken: string,
}

Routing guard

auth.guard.ts

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AccountService } from '../services/account.service';

export const authGuard: CanActivateFn = (route, state) => {
const authToken = inject(AccountService).isAuthenticated();
const router = inject(Router);

if (authToken) return true;

return router.navigate(['login'], { });
};

Routes

import { Routes } from '@angular/router';
import { HomeComponent } from './components/home/home.component';
import { LoginComponent } from './components/login/login.component';
import { DataComponent } from './components/data/data.component';
import { authGuard } from './guards/auth.guard';

export const routes: Routes = [
{
path: '',
title: 'App Home Page',
component: HomeComponent,
},
{
path: 'login',
title: 'Login',
component: LoginComponent,
},
{
path: 'data',
title: 'Data',
component: DataComponent,
canActivate: [authGuard]
},
];

Http interceptor

jwt.intercepter.ts

import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AccountService } from '../services/account.service';

export const jwtInterceptor: HttpInterceptorFn = (req, next) => {
const authToken = inject(AccountService).getJwt();

if (authToken) {
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${authToken}`),
});
return next(authReq);
}

return next(req);
};

app.config.ts

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { jwtInterceptor } from './interceptors/jwt.interceptor';

export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([jwtInterceptor])),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes)
]
};

Login component

import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AccountService } from '../../services/account.service';
import { Observable } from 'rxjs';
import { IJwtResponse } from '../../models/ijwt-response';
import { Router } from '@angular/router';

@Component({
import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AccountService } from '../../services/account.service';
import { Observable } from 'rxjs';
import { IJwtResponse } from '../../models/ijwt-response';
import { Router } from '@angular/router';

@Component({
selector: 'app-login',
imports: [FormsModule, AsyncPipe],
template: `
<h1>Login</h1>
<form (submit)="login()">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input [(ngModel)]="username" name="username" type="text" class="form-control" id="username" placeholder="Enter username">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input [(ngModel)]="password" name="password" type="password" class="form-control" id="password" placeholder="Enter password">
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>

@if (data$ | async; as data) {
<ul>
<li>jwt - {{data.jwt}}</li>
<li>rt - {{data.refreshToken}}</li>
</ul>
}
`,
styles: ``
})

export class LoginComponent {
username: string = 'user@taltech.ee';
password: string = 'Foo.Bar.2';

constructor(private accountService: AccountService, private router: Router) { }

data$!: Observable<IJwtResponse>;

login() {
// Implement login logic here
console.log('submitted', this.username, this.password);
if (this.username && this.password) {
this.data$ = this.accountService.login(this.username, this.password);
this.data$.subscribe(jwt => {
console.log("got jwt", jwt);
this.accountService.setJwt(jwt);
this.router.navigate(['/data'])
});
}
}
}