Angular Notes
Angular Notes
Learn Angular
Angular Setup & Config
1. The first version of Angular is called AngularJS. After that, it is referred to
simply as Angular.
2. Typescript provides errors and warnings during compile time that are easier to
understand. Typescript is a superset of Javascript.
3. Install Angular with npm → npm install -g @angular/cli@latest
4. Creating app → ng new my-app
5. Create app in existing directory → ng new existingDir --directory ./
6. Run app → ng serve
Lifecycle Method
1. OnChanges(): Called after a bound input property changes. 'Bound' means
properties decorated with @Input(). It receives arguments and is also invoked
when a component is initialized.
2. OnInit(): Called once the component is initialized and will run after the
constructor. It used to perform a custom change detection & responding to the
changes in a component.
3. DoCheck(): Called during every change detection run and will be executed
frequently.
4. AfterContentInit(): Called after content (ng-content) has been projected into
view.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
2
Angular Basics
Component
1. It is best practice to create all components in the 'src' directory, and each
component should have its own dedicated directory.
2. If we create a component manually, we need to create three different files.
(e.g. xyz.component.ts, xyz.component.html, xyz.component.scss)
3. General Structure of a Component(demo.component.ts) →
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
3
[Note: We cannot use `template` and `templateUrl` simultaneously; the same rule
applies to `styles` and `styleUrls`.]
Data Binding
1. String interpolation → {{data}}
<p>{{ 'Server' }} with ID {{ severId }} is {{ getServerStatus() }}</p>
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
4
[Note: Add parentheses at the end of EventEmitter to call its constructor and create a
new EventEmitter object, which is now stored in the serverCreated and
blueprintCreated variables.]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
5
2. We can directly access a local reference variable in the TypeScript file using
@ViewChild().
export class DemoComponent {
@ViewChild('serverNameInput') nameInput: ElementRef;
onAddServer() {
const name = this.nameInput.nativeElement.value;
}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
6
onCLick() {
console.log(this.control);
}
}
--------------------- control.component.html --------------------
<label>{{ label }}</label>
<ng-content select="input, textarea" />
--------------------- parent.component.html ---------------------
<form (ngSubmit)="onSubmit()" #form>
<app-control label="Title">
<input name="title" id="title" #input />
</app-control>
<app-control label="Request">
<textarea name="request" id="request" rows="3" #textInput #input></textarea>
</app-control>
</form>
Directive
1. Directives are instructions for the DOM. To create a directive, use the command
→ ng g d directive_name
2. Angular has two types of directives.
a) Structural directive
<p *ngIf="server; else noServer">Server is available!</p>
<ng-template #noServer>
<p>No Server!</p>
</ng-template>
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
7
b) Attribute directive.
In an HTML tag, we can use only one structural directive, while multiple
attribute directives can be applied. It's important to note that attribute directives
do not add or remove elements.
[Note: After creating a custom attribute directive, it is necessary to add the
directive to the declarations array in the app.module.ts or specific module.ts file.]
3. Dynamically styling and binding property into a directive →
<p [ngStyle]="{backgroundColor: getColor()}"
[ngClass]="{online: onlineStatus === 'online'}">
Hello
</p>
ngOnInit() {
this.elementRef.nativeElement.style.backgroundColor = 'blue';
}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
8
ngOnInit() {
this.renderer.setStyle(this.elRef.nativeElement, 'background-color', 'blue');
}
}
10. Custom property binding in a directive involves using the @Input() variable
within the directive component, allowing us to pass a value through property
binding.
----------------- better-highlights.directive.ts -----------------
export class BetterHighlightsDirective implements OnInit {
@Input() defaultColor: string = '';
@Input() highlightColor: string = '';
@HostBinding('style.backgroundColor') bgColor: string;
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
9
ngOnInit() {
this.bgColor = this.defaultColor;
}
@HostListener('mouseenter') mouseover(eventData: Event) {
this.bgColor = this.highlightColor;
}
}
---------------- HTML file where we use the directive ----------------
<p
appBetterHightlights
[defaultColor]="'mouseenter'" [highlightColor]="'mouseenter'"
>
</p>
12.Understanding ngSwitch →
<div [ngSwitch]="value">
<p *ngSwitchCase="5">Value is 5</p>
<p *ngSwitchDefault>Value is default</p>
</div>
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
10
Decorator
Styling
1. If we want to share a component’s CSS/SCSS styles with all other components,
we need to set the View Encapsulation value to ‘None’ on the corresponding
TypeScript file.
@Component({
selector: 'app-xyz',
templateUrl: './xyz.component.html',
styleUrls: ['./xyz.component.scss'],
Encapsulation: ViewEncapsulation.None -OR- ViewEncapsulation.ShadowDom
})
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
11
[Note: If we want to inject one or more services into another service, we need to add
the @Injectable() decorator at the top of the service file. This is necessary to make
the service injectable.]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
12
Angular Tokens
Tokens are useful when there are multiple dependencies in the application.
There are three types of tokens.
1. Type token
2. String token
3. Injection token object
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
13
Injecting Token →
It acts as a key to uniquely identify a provider, which is important because there
can be multiple dependencies within an application. Tokens are therefore very useful
for uniquely identifying these dependencies.
------------------------- log-message1.service.ts -------------------------
@Injectable()
export class LogMessage1Service {
log() { console.log('This is Service 1'); }
}
------------------------- log-message2.service.ts -------------------------
@Injectable()
export class LogMessage2Service {
log() { console.log('This is Service 2'); }
}
---------------------------- app.component.ts -----------------------------
export class AppComponent {
constructor(private logger: LogMessage1Service) {
this.logger.log();
}
}
------------------------------ app.module.ts ------------------------------
@NgModule({
declarations: [],
imports: [BrowserModule, AppComponent],
providers: [{
provide: LogMessage1Service, useClass: LogMessage1Service,
provide: LogMessage1Service, useClass: LogMessage2Service,
// We use the same token for two different classes. In this case, the
output is displaying the second service file message.
}],
bootstrap: [AppComponent],
})
[Note: Here, we use a type token to inject the instance of the logMessage1 service. The
provide property holds the token, which acts as a key, allowing the dependency injection
system to locate the provider associated with the token. The useClass property tells
Angular’s dependency injection system to instantiate the provided class when the
dependency is injected.]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
14
String Token →
String-based injection tokens are another way to register a provider.
-------------------------- app.component.ts --------------------------
export class AppComponent {
constructor(
@Inject('LOG_MSG1')
private logger: LogMessage1Service
) {
this.logger.log();
}
}
---------------------------- app.module.ts ---------------------------
providers: [{
provide: 'LOG_MSG1', useClass: LogMessage1Service,
}],
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
15
Provider
Value Provider(useValue) → It provides a specific value to be used as a dependency.
Instead of creating an instance of a class, it allows values to be directly assigned when
the dependency is injected.
------------------------- injection-token.ts -------------------------
import { InjectionToken } from '@angular/router';
export const STR_MSG = new InjectionToken<string>('Greeting');
-------------------------- app.component.ts --------------------------
export class AppComponent {
constructor(@Inject('STR_MSG') public msg: string) {
console.log(this.msg); // Output - This is the string message
}
}
---------------------------- app.module.ts ---------------------------
providers: [{
provide: 'STR_MSG', useValue: 'This is the string message',
}],
[Note: This technique is mostly used when we want runtime configuration constants such
as website base API addresses, etc.]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
16
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
17
[Note: The deps property is used to specify the dependencies used with the factory.]
Routing
We can create a routing module by running the command → ng g module
app-routing --routing --flat.
In this command, --routing creates a dedicated file for routing, and --flat places the
module file in the root directory of our project.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
18
@NgModule({
imports: [ RouterModule.forRoot(appRoutes) ],
exports: [RouterModule]
})
export class AppRoutingModule { }
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
19
Navigation
1. In Angular, it is recommended to use routerLink="/" instead of href="/" to
navigate between pages within an app. routerLink is an attribute directive that
allows us to add behavior to an existing element by modifying its attributes. We
can navigate using router Links as follows →
<div class="container">
<ul class="nav nav-tabs">
<li class="active"><a routerLink="/">Home</a></li>
<li><a routerLink="/servers">Servers</a></li>
<li><a [routerLink]="['/users']">Users</a></li>
</ul>
</div>
2. We already understand navigation paths, but there are two different ways to
navigate using router links.
a) Relative path: If we don’t use a slash(/) or use relative paths(./, ../, ../../, etc.)
before the route, the path will be appended to the current one. There are three
distinct ways to utilize relative path navigation.
<li><a routerLink="servers">Servers</a></li>
—---OR—---
<li><a routerLink="./servers">Servers</a></li>
—---OR—---
<li><a routerLink="../servers">Servers</a></li>
3. Programmatically Navigation →
constructor(private router: Router, private route: ActivatedRoute) {}
// Absolute Routing
this.router.navigate(['/servers']);
// Relative Routing
this.router.navigate(['servers'], {relativeTo: this.route});
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
20
In this code, 'active' is a class name that can be customized to our preference.
Additionally, we use routerLinkActiveOptions to specify options for matching
the exact router link.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
21
getData() {
this.user = {
id: this.route.snapshot.params['id'],
name: this.route.snapshot.params['name'];
};
}
Fetch Data Reactively
-------------------- user-details.component.ts -------------------
paramsSubscriptions: Subscription;
ngOnInit() {
this.paramsSubscriptions = this.route.params.subscribe(
(params: Params) => {
this.user.id = params['id'];
this.user.name = params['name'];
});
}
ngOnDestroy() { this.paramsSubscription.unsubscribe(); }
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
22
getData() {
const isEdit = this.activeRoute.snapshot.queryParams['allowEdit'];
const fragment = this.activeRoute.snapshot.fragment;
}
Fetch Data Reactively
ngOnInit() {
this.activeRoute.queryParams.subscribe(
(params: Params) => { // Our own code });
this.activeRoute.fragment.subscribe();
)};
}
[Note: We don’t need to unsubscribe this because angular automatically handles it.]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
23
6. If we want to retain the current query parameters when navigating to the next
route, we need to use queryParamsHandling while initiating the navigation to a
new route.
this.router.navigate(
['edit'],
{ relativeTo: this.route, queryParamsHandling: 'preserve' }
);
[Note: When 'merge' is used instead of 'preserve' in queryParamsHandling, it will
combine the existing query parameters with the new ones during navigation.]
9. To pass data dynamically to a route, we can use Resolver Guard. Please refer to
the Guard section for more information.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
24
Nested Route
1. The general form of nested route →
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users/:id/:name', component: UserDetailsComponent },
{ path: 'servers', component: ServersComponent,
children: [
{ path: ':id', component: ServerComponent },
{ path: ':id/edit', component: ServerEditComponent }
]
}
];
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
25
<div class="container">
<ul class="nav nav-tabs">
<li class="active"><a routerLink="/">Servers</a></li>
<li><a routerLink="/:id">Server</a></li>
<li><a [routerLink]="/:id/edit">Edit</a></li>
</ul>
<div> <router-outlet></router-outlet> </div>
</div>
Guard
We can create a route guard by running the command → ng g guard NAME.
1. To implement the auth guard feature, we will create a new file called
auth-guard.service.ts in the root(src) directory. This service will contain all the
guard-related code, and It is important that Angular executes this code before a
route is loaded. Typically, Angular guards are implemented using a service that
adheres to the CanActivate or other guard interfaces.
--------------------- auth-guard.service.ts ---------------------
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
26
[Note: Must add both services into the providers' array of the app.module.ts file.]
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean
{
// Logical Code that we added above }
canActivateChild( route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean
{
return this.canActivate(route, state);
}
}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
27
[Note: Here, the parent and child routes are protected. If we remove canActivate:
[AuthGuard], only the child routes will be protected.]
@Injectable()
export class ServerResolver implements Resolve<Server> {
constructor(private serverService: ServerService) { }
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
28
Must Know
1. Execute Route Resolver when QueryParams change →
----------------------- tasks.component.ts ----------------------
export const resolveUserTasks: ResolveFn<Task[]> = (activatedRoute:
ActivatedRouteSnapshot, routerState: RouterStateSnapshot) => {
const order = activatedRoute.queryParams['order'];
const tasksService = inject(TasksService);
const tasks = tasksService.allTasks().filter(
(task: any) => task.userId === activatedRoute.paramMap.get('userId')
);
if (order && order === 'asc') tasks.sort((a, b) => (a.id > b.id ? 1 : -1));
else tasks.sort((a: any, b: any) => (a.id > b.id ? -1 : 1));
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
29
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
30
4. Control accidentally clicks the back button or navigates back which is called
Deactivate Route →
-------------------- edit-server.component.ts -------------------
import { CanComponentDeactivate } from './can-deactivate-guard.service';
export class EditServerComponent implements CanComponentDeactivate {
changesSaved: boolean = false;
onUpdateServer() {
this.serverService.updateServer(this.server.id);
this.changesSaved = true;
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
31
@NgModule({
imports: [
RouterModule.forRoot(appRoutes, {useHash: true})
],
exports: [RouterModule]
})
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
32
Observable
1. What is an Observable? → An Observable is a data source in an Angular
project, implemented using the observable pattern from the RxJS library. It
allows us to handle multiple emitted events asynchronously. Paired with an
Observer, the Observable can emit data in response to certain triggers, enabling
efficient event handling.
2. Install RxJS & RxJS Compat →
npm install --save rxjs
npm install --save rxjs-compat
3. Angular Observable: In routing, we often subscribe to route parameters, which
are essentially observables. We use the “subscribe” function because, when the
observable value changes, the updated value is provided through the subscribe
callback. It's important to note that Angular has many built-in observables,
which we will explore later.
Note: Angular's built-in observables don't require manual unsubscription, as
Angular manages this automatically.
4. Create an observable by using the Interval method of RxJS →
import { interval, Subscription } from 'rxjs';
export class DemoComponent implements OnInit, OnDestroy {
private demoObservable: Subscription;
ngOnInit() {
this.demoObservable = interval(1000).subscribe(next => {
// Our code which will trigger after 1 second interval
});
}
ngOnDestroy() {
this.demoObservable.unsubscribe();
}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
33
ngOnDestroy() { this.demoObservable.unsubscribe(); }
}
6. If an Observable throws an error, it terminates and will not emit any more
values, so there’s no need to unsubscribe. Similarly, when an Observable
completes, it stops emitting values, and no unsubscription is necessary.
7. Operators → Operators are a powerful feature of the RxJS library. When we
have an observable and an observer, we typically receive data by subscribing to
the observable. However, there are times when we want to transform, filter, or
manipulate the data before handling it. This can be done either within the
subscription or by passing a function to it.
Rather than manually setting up transformations inside the subscription, we can
use built-in RxJS operators. These operators process the data before we
subscribe, allowing the data to flow through the operators for manipulation.
RxJS provides a wide range of operators. Below are examples of two common
operators: map and filter, demonstrating how to use them.
import { Observable } from 'rxjs';
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
34
ngOnInit() {
const customObservable = Observable.create(observer => {
let count = 0;
setInterval(() => {
observer.next(count);
count++;
}, 1000);
});
ngOnDestroy() {
this.demoObservable.unsubscribe();
}
}
[Note: Every observable has a pipe method.]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
35
onActivate() {
// this.userService.activatedEmitter.emit(true); // Old way
this.userService.activatedEmitter.next(true);
}
}
ngOnInit() {
this.activateSub = this.userService.activatedEmitter
.subscribe(didActivate => {
this.userActivated = didActivate;
});
}
ngOnDestroy(): void {
this.activateSub.unsubscribe();
}
}
[Note: We should ensure to unsubscribe from our subjects when they are no
longer needed to prevent memory leaks.]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
36
Angular Forms
Angular offers us two approaches to handling forms.
a) Template Driven → Angular infers the Form Object from the DOM.
b) Reactive → Form is created programmatically and synchronized with the DOM.
To enable the use of form features, first ensure that the FormsModule is imported into
the imports array of the app.module.ts file. This will include a variety of forms-related
functionality in our application. This provides various forms-related functionalities
within the application.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
37
2. Submit Form →
---------------------- demo.component.html ----------------------
<form class="form-container" (ngSubmit)="onSubmit(f)" #f="ngForm"> </form>
----------------------- demo.component.ts -----------------------
onSubmit(form: NgForm) {
console.log(form); // Now, we can access all the properties of the form
that we will use to control the form
form.form.reset();
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
38
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
39
13.Reset Form →
@ViewChild('f') signUpForm: NgForm;
onSubmit() {
console.log(this.signUpForm.value.userData.username);
this.signUpForm.reset();
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
40
Reactive Approach
1. Angular provides numerous tools for efficiently creating reactive forms. To utilize
these reactive form features, it's essential to include ReactiveFormsModule in
the imports array of the app.module.ts file or a specific module file. Now, let's
explore how to set up a reactive form by creating one →
----------------------- demo.component.ts –----------------------
export class DemoComponent implements OnInit {
signupForm: FormGroup;
ngOnInit() {
this.signupForm = new FormGroup({
'username': new FormControl(null),
'email': new FormControl(null),
'gender': new FormControl('male')
});
}
}
[Note: We should initialize the form before initializing the template.]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
41
3. Add Validation →
ngOnInit() {
this.signupForm = new FormGroup({
'username': new FormControl(null, Validators.required),
'email': new FormControl(null, [Validators.required, Validators.email]),
'gender': new FormControl('male')
});
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
42
<div formArrayName="hobbies">
<h4>Your Hobby</h4>
<button type="button" (click)="onAddHobby()">Add Hobby</button>
<div class="form-group" *ngFor="let hobby of getControls(); let i = index">
<input type="text" [formControlName]="i">
</div>
</div>
<button class="btn form-btn" type="submit">Submit</button>
</form>
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
43
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
44
[Note: We bind `this` when setting the forbiddenNames function as a validator. This
is essential because, when Angular checks the validity and calls the forbiddenNames
function, it needs to know the context of the caller. Since the function is not called
from the class, `this` would not refer to our class. Therefore, binding `this` ensures
the correct context during the validation process.]
8. Handling Error Codes from Validators: When working with custom validators
or built-in validators in Angular, it's important to handle the error codes they
generate. Each validator can return an object containing error codes that indicate
what went wrong. →
---------------------- demo.component.html ----------------------
<form class="form-container" [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<div formGroupName="userData">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" formControlName="username">
<span *ngIf="signupForm.get('userData.username').errors['nameIsForbidden']">
Please enter a valid Name.</span>
</div>
</div>
<button class="btn form-btn" type="submit">Submit</button>
</form>
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
45
—----- A N O T H E R
E X A M P L E —------
[Note: For some websites, we need to check immediately whether an
email or username is valid or not. This is a best example to use
async validation.]
this.signupForm.valueChanges.subscribe((value) => {
console.log(value);
});
this.signupForm.statusChanges.subscribe((status) => {
console.log(status);
});
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
46
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
47
14.Reset form →
this.signupForm.reset();
3. Number pipe →
<span>{{ 110 | number:'3.1-2' }}</span> // Output: 110.0
<span>{{ 110 | number:'3.2-3' }}</span> // Output: 110.00
<span>{{ 110 | number:'3.3-4' }}</span> // Output: 110.000
<span>{{ 110.66 | number:'3.1-1' }}</span> // Output: 110.7
<span>{{ pi | number:'1.2-2' }}</span> // Output: 3.14
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
48
6. Creating a Custom Pipe: After creating a custom pipe, it's crucial to add the
pipe to the declarations array in the app.module.ts file, just like we do with
components and directives. This ensures that the custom pipe is recognized and
can be used throughout the application.
------------------------ shorten.pipe.ts ------------------------
import { PipeTransform, Pipe } from '@angular/core';
@Pipe({
name: 'shorten'
})
export class ShortenPipe implements PipeTransform {
transform(value: any) {
if (value.length > 10)
return value.substr(0, 10) + '...';
return value;
}
}
----------------------- demo.component.ts -----------------------
<div class="server-info">
<strong>{{ server.name | shorten }}</strong>
<span>{{ server.type | uppercase }}-{{ server.start | date }}</span>
</div>
[Note: The transform method is pipe built-in functional method. ]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
49
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
50
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
51
5. DELETE request →
onClearPosts() {
this.http.delete(API_URL)
.subscribe(responseData => { this.allPosts = []; });
}
ngOnInit() {
this.errorSub = this.PostService.error
.subscribe(errorMessage => {
this.error = errorMessage;
});
}
ngOnDestroy() {
this.errorSub.unsubscribe();
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
52
8. Set Header →
onFetchPost() {
this.http.get(API_URL, {
headers: new HttpHeaders({ 'Custom-Header': 'hello' })
}).subscribe(
responseData => { console.log(responseData); },
error => { this.error.next(error.message);
});
}
this.http.get(API_URL, {
headers: new HttpHeaders({ 'Custom-Header': 'hello' }), params: searchParams
}).subscribe(
responseData => { console.log(responseData); },
error => { this.error.next(error.message);
});
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
53
// Handle event
this.http.delete(API_URL, { observe: 'events' })
.pipe(tap(event => {
if (event.type === HttpEventType.Sent) { }
if (event.type === HttpEventType.Response) { console.log(event.body); }
}))
.subscribe(responseData => { this.allPosts = []; });
Interceptors
1. What are interceptors? → Interceptors are a powerful feature in Angular
that allow us to modify or handle HTTP requests and responses globally before
they are sent or after they are received. For example, if we want to append a
custom header (such as an authentication token) to every outgoing HTTP
request, we can use an interceptor to do this automatically.
Here’s a general overview of interceptors:
● Global Modification: Interceptors provide a way to globally modify HTTP
requests (e.g., adding headers) or responses (e.g., logging or error
handling) without changing the logic for each individual request.
● Middleware-Like Functionality: They act like middleware, processing
HTTP requests and responses before the request reaches the server and
before the response reaches the client.
● Multiple Interceptors: You can chain multiple interceptors to handle
different tasks like logging, authentication, caching, etc.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
54
return next.handle(modifiedHeader);
}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
55
5. Response Interceptors →
intercept(req: HttpRequest<any>, next: HttpHandler) {
return next.handle(req)
.pipe(
tap(event => {
if (event.type === HttpEventType.Response) {
console.log('Response arrived, body data: ' + event.body);
}
})
);
}
6. Multiple Interceptors →
----------------- logging-interceptor.service.ts -----------------
export class LoggingInterceptorService implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler) {
return next.handle(req)
.pipe(tap(event => {
if (event.type === HttpEventType.Response) {
console.log('Response arrived ' + event.body);
}
})
);
}
}
--------------------- app-routing.module.ts ---------------------
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptorService,
multi: true,
},
{
provide: HTTP_INTERCEPTORS,
useClass: LoggingInterceptorService,
multi: true,
}
]
})
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
56
Dynamic Components
1. What is the dynamic component? → Dynamic component is a component
created dynamically at runtime. For example, we might want to display an alert,
modal, or overlay that only loads in response to certain actions. Dynamic
component creation and behavior are not built-in Angular features but are
implemented based on the application's requirements.
2. Create an Alert Modal component manually → github-gist
3. Prepare Alert Modal programmatically → While it is possible to
manually instantiate a component in TypeScript (e.g. const alertCmp = new
AlertComponent()), this approach isn’t valid in Angular. Angular provides us a
more structured way to create components dynamically at runtime using a
ComponentFactory. This tool allows us to generate and insert components
programmatically. To illustrate this concept, we have provided an example code
in the link below - github-gist
4. entryComponents → It was traditionally an array in the module file where
components, directives, or pipes were listed when they were created
programmatically, such as in a modal. Angular would automatically check the
declarations array for components used as HTML selectors or routing
components. However, for components instantiated dynamically (e.g., modals),
the component had to be included in the entryComponents array. Starting
with Angular 9, this is no longer required, as Angular now automatically detects
components that need to be created programmatically without the
entryComponents array.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
57
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
58
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
59
@NgModule({
imports: [RouterModule.forChild(routes)], exports: [RouterModule]
})
----------------------- recipes.module.ts -----------------------
@NgModule({ imports: [ RecipesRoutingModule ] })
6. Core Module → A Core Module is created to house services that are intended
to be provided throughout the application, rather than declaring them in the App
Module. This approach helps in organizing global services, ensuring that they
are available application-wide but are only instantiated once.
After creating the Core Module (commonly named core.module.ts), it is
imported into the app.module.ts file. This structure makes it easier to manage
and separate core functionality, improving the overall maintainability of the app.
Lazy Loading
What is lazy-loading? → Lazy-loading refers to loading specific modules
or components of an application only when they are required, rather than loading
everything upfront. For example, in an Angular app with routes like /, /products, and
/admin, each route may have its own module containing components, services, and
more. When lazy-loading is implemented, only the module related to the visited route
is loaded. If the user accesses the /products route, only the components and features
for that route are loaded, improving performance and reducing initial load time.
By loading modules on demand, the application has a smaller initial bundle size,
leading to faster load times and better user experience.
1. Enable Lazy-loading with Routing →
--------------------- recipes-routing.module.ts ---------------------
const routes: Routes = [
{
path: '/',
loadComponent: () => import('./recipes/recipes.component).then(m => m.RecipesComponent),
canActivate: [AuthGurad], children: [
{ path: '', component: RecipeStartComponent },
{ path: ':id', component: RecipeDetailsComponent,
resolve: [RecipesResolverService]
}
]
}
];
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
60
@NgModule({
imports: [
RouterModule.forRoot(appRoutes,
{ preloadingStrategy: PreloadAllModules }
)
]
})
[Note: The advantage of lazy-loading is that it keeps the initial download bundle
small, as only the necessary code is loaded. This results in a faster initial loading
phase and quicker subsequent loads, improving overall performance and user
experience.]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
61
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
62
Deployment
1. Ensure all necessary environment variables are suitably set for the environment.
2. Build Angular apps for deployment by running → ng build
3. For Firebase deployment, execute → npm install -g firebase-tool.
Afterward, log in to Firebase with → firebase login.
Next, initialize Firebase for project → firebase init and choose Hosting.
Set the public directory as dist/project_directory_name.
Finally, deploy with → firebase deploy.
4. To address broken routes after deployment, refer to → Fix Broken Route.
Standalone Components
Angular 14 introduced Standalone Components, which became stable from
Angular 15 onwards. This feature simplifies Angular development by reducing the
boilerplate code required when setting up a new project or refactoring existing ones.
Traditionally, Angular developers had to declare components, directives, and pipes in
modules and manage imports and exports between them, which could be cumbersome
in larger apps.
Standalone components allow us to build Angular apps without the need for
NgModules. These components can now operate independently, with services,
directives, and pipes being injected directly into them without the need to declare them
in a module.
Key Benefits: Reduced Boilerplate Code, Simplified Structure, Backward Compatibility,
Improved Developer Experience.
1. Create a Standalone Component →
import { Component } from '@angular/core';
@Component({
standalone: true,
selector: 'app-xyz',
templateUrl: './xyz.component.html',
styleUrls: ['./xyz.component.scss']
})
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
63
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
64
if (environment.production) { enableProdMode(); }
bootstrapApplication(AppComponent);
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
65
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
66
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
67
client-side single-page application once downloaded from the server. However, the
key difference is that the very first page loaded by the user is pre-rendered on the
server. This means the fully-rendered page is served to the user right away, so we
don’t have to wait for the JavaScript code to download and execute before seeing
content on the screen.
It's important to note that this optimization applies only to the initial page load.
After that, subsequent navigations within the app are still handled by Angular on the
client side, maintaining the single-page application experience with the added benefit
of faster first-page load times.
1. With Angular 17, "Angular Universal" was essentially renamed to "Angular
SSR" (SSR stands for Server-side Rendering). However, the core concept
remains the same in both cases: the Angular app is pre-rendered on the server,
and the finished HTML is sent to the client. Afterward, the app transitions back
into a client-side SPA (Single Page Application).
2. After running the Angular Universal or SSR enable command, 4 new files will
be created automatically and also updating the existing app-routing module file.
Created files are → main.server.ts, app.server.module.ts, tsconfig.server.json,
server.ts.
3. Creating Angular SSR Apps with Angular 17+, by running
→ ng new <project-name> --ssr
Note: Using Angular 17, running an SSR-enabled project also is easier than
doing so with Angular 16 or lower.
Deployment
In the Angular Universal app, we could create a REST API into the server.ts file,
allowing us to build a full-stack app with a backend REST API that resides within the
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
68
Angular Signals
Angular 16 introduced a very important new feature called Signals, which is
stable in Angular 17.
What’s the purpose of Signals? → Signals provide a new way of handling data
changes and updating the UI. Traditionally, Angular uses the zone.js library, which
automatically detects changes and updates the UI accordingly. However, this approach
can negatively impact app performance and increase bundle size.
That’s why Signals come in. They offer an alternative method for managing and
detecting changes, allowing Angular to eliminate the need of zone.js library. This
change let's Angular stop monitoring the entire application, which can improve
performance and reduce bundle size. With Signals, there is no automatic change
detection. Instead, we explicitly tell Angular when data changes and where that data is
being used.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
69
increment() {
this.counter.update((oldCounter) => oldCounter + 1);
this.counter.set(5);
this.counter.set( this.counter() + 5);
this.counter.mutate((oldActions) => oldActions.push('Increment'));
}
--------------------- signals.component.html --------------------
<div class="container">
<h2> Signals </h2>
<li *ngFor="let action of actions()"></li>
</div>
[Note: Both set and update assign a new value to a Signal. The mutate allows us to
modify existing values, effectively assigning a new value by editing the current one.]
3. Computed Values →
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
70
4. Effects →
import { Component, signal, effect } from '@angular/core';
export class SignalsComponent {
constructor() {
effect(() => console.log(this.counter()));
}
}
[Note: The effect is a function that could be executed in the constructor and we can
use it in other places. The effect allows us to define code that depends on signals and
Angular will automatically re-execute this function that’s being passed to effect,
whenever any signal is being used inside of the function.
Angular understands that this code uses the counter signal and whenever the
counter changes, only then angular will go ahead and re-execute this code.]
Angular Animations
With the release of Angular 4, the general syntax for Angular Animations remained
unchanged.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
71
3. Now, we can import trigger, state, style, and other animation-related functions
from the @angular/animations.
Implementation
1. Uses of Animations Trigger and State → To use animations in component
templates, we first need to set them up in the app component decorator.
----------------------- demo.component.ts -----------------------
import {
Component, trigger,
state, transition, animate
} from '@angular/core';
@Component({
selector: 'app-demo',
templateUrl: './demo.component.html',
styleUrls: ['./demo.component.scss'],
animations: [
// We could write multiple trigger here separating by `,`
trigger('divState', [
state('normal', style({
'background-color': 'red', 'transform': 'translateX(0)'
})),
state('highlighted', style({
'background-color': 'blue', 'transform': 'translateX(100px)'
})),
transition('normal => highlighted', animate(300)),
transition('highlighted => normal', animate(800))
// transition('normal <=> highlighted', animate(300))
// This works in both(left-right, right-left) ways.
])
]
})
export class DemoComponent {
state = 'normal';
// Switching between states
onAnimate() {
this.state = this.state === 'normal' ? 'highlighted' : 'normal';
}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
72
2. Advance Transitions →
----------------------- demo.component.ts -----------------------
@Component({
selector: 'app-demo',
templateUrl: './demo.component.html',
animations: [
trigger('wildState', [
state('normal', style({
'background-color': 'red',
'transform': 'translateX(0) scale(1)'
})),
state('highlighted', style({
'background-color': 'blue',
'transform': 'translateX(100px) scale(1)'
})),
state('shrunken', style({
'background-color': 'green',
'transform': 'translateX(0px) scale(0.5)'
})),
transition('normal => highlighted', animate(300)),
transition('highlighted => normal', animate(800))
transition('shrunken <=> *', animate(500)) // Use * means any
state, 'shrunken' to any state and any state to 'shrunken'
])
]
})
export class DemoComponent {
wildState = 'normal';
onAnimate() {
this.wildState = this.wildState === 'normal' ? 'highlighted' : 'normal';
}
onShrink() { this.wildState = 'shrunken'; }
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
73
3. Transition Phases → We can write style properties into the animate method.
transition('shrunken <=> *', animate(50, style({ 'borderRadius': '50px' })))
[Note: The ”void” is a reserve state name. We can’t use it. We shouldn’t override it
based on our requirement.]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
74
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
75
2. Caching assets for Offline Use → To cache and load data for offline use, we
need to ensure that key assets, such as the index.html file (which sits at the
root of the server folder), are cached. In the ngsw-config.json file, we find
two asset group arrays. These asset groups define which static assets should be
cached and how they should be cached. Dynamic assets, such as data from an
API, are different—they change frequently, so they are not considered static.
Within the assetGroups array, you'll see installMode and updateMode
properties, where you can set values like "prefetch" or "lazy" to control
how assets are loaded. There is also a resources object, which contains an
array specifying the files you want to cache.
If you want to cache assets like fonts via a link, you can add a urls array inside
the resources object, alongside the files array, and include the link in the
urls array.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
76
3. Caching Dynamic assets and urls → To cache dynamic assets and urls, we need
to create a dataGroups object array just like assetGroups.
"dataGroups": [
{
"names": "posts",
"urls": [ "https://xyz.com/posts" ],
"cacheConfig": {
"maxSize": "5",
"maxAge": "6h",
"timeout": "10s",
"strategy": "freshness", // Or "performance",
}
}
]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
77
Command
1. To test our app run → ng test
2. Run test without hot loader → ng test --no-watch
describe("CalculatorService", () => {
it("should add two numbers", () => {
const calculator = new CalculatorService(new LoggerService());
const result = calculator.add(2, 2);
expect(result).toBe(4);
});
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
78
// spyOn(logger, "log");
const calculator = new CalculatorService(logger);
const result = calculator.add(2, 2);
expect(result).toBe(4);
expect(logger.log).toHaveBeenCalledTimes(1);
});
});
beforeEach
To streamline our tests and avoid repeating code and execute necessary logic
before each test, we should implement a beforeEach block. In this block, we include
all the setup logic required for the individual it() test blocks. The code in
beforeEach will execute before every it() block in the test suite.
---------------------- calculator.service.spec.ts ---------------------
describe("CalculatorService", () => {
let calculator: CalculatorService, loggerSpy: any;
beforeEach(() => {
loggerSpy = jasmine.createSpyObj("LoggerService", ["log"]);
calculator = new CalculatorService(loggerSpy);
});
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
79
afterEach
To avoid code duplication and execute necessary cleanup after each test, we
should implement an afterEach block. In this block, we include any logic that needs
to run after the individual it() blocks. The code in afterEach will execute after
every it() block in the test suite.
----------------------- courses.service.spec.ts ----------------------
describe("CoursesService", () => {
afterEach(() => {
httpTestingController.verify();
});
});
[Note: The verify() method of the HttpTestingController ensures that only the HTTP
requests specified within the it() block, using the expected APIs from the testing controller,
are made by the service being tested.]
toBeTruthy()
This is used after the expect() block to ensure that a variable holds a truthy
value. If the variable exists and has a value, toBeTruthy() will validate it.
----------------------- courses.service.spec.ts -----------------------
coursesService.findLessons(12)
.subscribe((lessons) => {
expect(lessons).toBeTruthy();
});
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
80
TestBed
It allows us to configure services without directly assigning a service instance to
a variable. It enables us to provide dependencies to services using Angular's
dependency injection, eliminating the need to call constructors explicitly.
---------------------- calculator.service.spec.ts ---------------------
import { TestBed } from "@angular/core/testing";
beforeEach(() => {
loggerSpy = jasmine.createSpyObj("LoggerService", ["log"]);
TestBed.configureTestingModule({
providers: [
CalculatorService,
{ provide: LoggerService, useValue: loggerSpy },
],
});
calculator = TestBed.inject(CalculatorService);
});
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [CoursesService]
});
coursesService = TestBed.inject(CoursesService);
httpTestController = TestBed.inject(HttpTestingController);
});
it('Should retrieve all courses', () => {});
})
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
81
2. req.flush() - It triggers the mock HTTP call and simulates a response with mock
data that resembles the original response.
-------------------- courses.service.spec.ts --------------------
describe('CoursesService', () => {
it("should save the course data", () => {
const changes: Partial<Course> = {
titles: { description: "Testing Course" },
};
coursesService.saveCourse(12, changes)
.subscribe((course) => { expect(course.id).toBe(12); });
expect(req.request.body.titles.description)
.toEqual(changes.titles.description);
Must know
1. To skip a test suite or a specific test block, we use xdescribe and xit.
Conversely, to focus on a specific test suite or test block, we can use
fdescribe and fit.
RxJS
RxJS stands for Reactive Extensions for JavaScript. It is a Third-party powerfull
library used in Angular for handling asynchronous operations and managing events
through observables and provides a wide range of JavaScript functions and classes,
including Observable, map, filter, merge, concat, catchError, interval, etc.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
82
Observable
It is a class interface provided by the RxJS library. It is used to manage
asynchronous data streams, allowing developers to handle events, asynchronous
operations, and data changes efficiently.
export class AppComponent {
count$ = new Observable((observer) => {
observer.next('Hello');
observer.error('An error occurred');
observer.complete();
}).subscribe({
next(value) { console.log(value); },
error(err) { console.log(err); }
});
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
83
ngOnInit() {
this.customInterval$.subscribe(() => {
next: (val: any) => console.log(val);
complete: () => console.log('Completed!!');
error: () => console.log('Error!!');
})
}
}
Operator
1. of() → It allows us to create an Observable that emits a sequence of values.
—------------------------- demo.service.ts —-------------------------
export class DemoService {
constructor(private http: HttpClient) { }
loadAllCourses(): Observable<Course[]> {
return this.http.get<Course[]>(url)
.pipe(shareReplay());
}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
84
userPlaces: any = []
loadUserPlaces() {
this.httpClient.get<{places: Place[]}>(API_URL)
.pipe(map((resData: any) => resData.places),
catchError((error) => throwError(() => new Error(error))
))
.pipe(tap({ next: (userPlaces) => this.userPlaces = userPlaces; }));
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
85
emitted value among multiple subscribers, effectively caching the result and
replaying it to any new subscribers.
—------------------------- demo.service.ts —-------------------------
export class DemoService {
constructor(private http: HttpClient) { }
loadAllCourses(): Observable<Course[]> {
return this.http.get<Course[]>(url).pipe(shareReplay());
}
}
—------------------------ demo.component.ts —------------------------
allCourses: Observable<Course[]>;
ngOnInit() {
const allCourses = this.demoService.loadAllCourses();
allCourses.subscribe(val => console.log(var));
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
86
Standalone Component
--------------------------- main.ts ---------------------------
import { provideStore } from '@ngrx/store';
bootstrapApplication(AppComponent, {
providers: [ provideStore() ]
});
4. Create a store directory into the app directory where all the store-related files
we will create.
Implementation
1. Add a First Reducer & Store Setup →
---------------------- counter.reducer.ts ---------------------
import { createReducer } from '@ngrx/store';
[Note: If we have multiple reducers, we can add them here by using key-value
structure.]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
87
this.value = this.count$.subscribe();
// Must unsubscribe this inside the ngOnDestroy method. Additionally,
we could use the async pipe to get value instead of subscribing to the
observable variable.
}
}
[Note: It's common practice, though not required, to use a dollar sign ($) at the end of
property names (e.g., count$) that store observables. This is a convention followed
in many high-quality projects to indicate that the variable holds an observable.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
88
const initialState = 0;
export const counterReducer = createReducer(
initialState,
on(increment, (state) => state + 1)
);
5. Dispatch Actions →
---------------- counter-controls.component.ts ----------------
import { Store } from '@ngrx/store';
import { increment } from './counter.actions';
increment() { this.store.dispatch(increment()) }
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
89
const initialState = 0;
export const counterReducer = createReducer(
initialState,
on(increment, (state, action) => state + action.x)
// if we have multiple actions then we can write another or more on
block here For example-
on(decrement, (state, action) => state - action.x)
);
---------------- counter-controls.component.ts ----------------
import { Store } from '@ngrx/store';
import { increment } from './counter.actions';
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
90
const initialState = 0;
export function counterReducer(state = initialState, action: CounterActions | Action) {
if (action.type == INCREMENT)
return state + (action as IncrementAction).x;
return state;
}
Selectors
1. We’ve seen how the select() method retrieves values from the store using a
selector (e.g., select('counter')). However, a more recommended approach
is to create custom selectors. Selectors can be triggered for specific actions and
provide a more structured way to access the store's state.
-------------------- counter.selectors.ts ---------------------
import { createSelector } from '@ngrx/store';
export const selectCount = (state: {x: number}) => state.x;
// We can write multiple selectors here and we can combine selectors here
with the help of createSelector of the NgRx library.
export const selectDoubleCount = createSelector(
selectCount,
(state: number) => state * 2 // Here, we will get value from selectCount
);
----------------- counter-output.component.ts -----------------
import { selectCount, selectDoubleCount } from './counter.selectors';
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
91
Effects
1. An effect is triggered by specific actions and is typically used for handling side
effects, rather than directly updating the UI. To use effects in our project, first
need to install the NgRx effects package by running: ng add @ngrx/effects
After installation, the EffectsModule will automatically be added to the imports
array in the app.module.ts file.
Note: For the Standalone components, the EffectsModule will be added to
the providers' array in the main.ts file.
2. Define a Effect →
--------------------- counter.effects.ts ----------------------
import { tap } from 'rxjs';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { decrement, increment } from './counter.actions;
@Injectable()
export class CounterEffects {
constructor(private actions: Actions) { }
saveCount = createEffect(() =>
this.actions.pipe(
ofType(increment, decrement),
tap((action) => { localStorage.setItem('count', action.value.toString()); })
),
{ dispatch: false }
);
}
- - - - - - - OLD WAY - - - - - - -
import { tap } from 'rxjs';
import { Actions, Effect, ofType } from '@ngrx/effects';
export class CounterEffects {
constructor(private actions: Actions) { }
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
92
3. Register an Effects →
Normal Component
------------------------ app.module.ts ------------------------
@NgModule({
imports: [
storeModule.forRoot({counter: counterReducer, // auth: authReducer}),
EffectsModule.forRoot([CounterEffects])
]
})
Standalone Component
--------------------------- main.ts ---------------------------
bootstrapApplication(AppComponent, {
providers: [
provideStore({ counter: counterReducer, // auth: authReducer}),
provideEffects([CounterEffects])
]
});
@Injectable()
export class CounterEffects {
constructor(private actions: Actions, private store: Store) { }
saveCount = createEffect(() =>
this.actions.pipe(
ofType(increment, decrement),
withLatestFrom(this.store.select(selectCount)),
tap(([action, counter]) => {
console.log(action);
localStorage.setItem('count', counter.toString());
})
),
{ dispatch: false }
);
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
93
const initialState = 0;
export const counterReducer = createReducer(
initialState,
on(increment, (state, action) => state + action.x)
on(set, (state, action) => action.value)
);
--------------------- counter.effects.ts ----------------------
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of, tap, withLatestFrom, switchMap } from 'rxjs';
import { store } from '@ngrx/store';
import { decrement, increment, init, set } from './counter.actions';
import { selectCount } from './counter.selectors';
@Injectable()
export class CounterEffects {
constructor(private actions: Actions, private store: Store) { }
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
94
// Here we should not add the dispatch: false configuration object to create
an effect. That was only required for where we did indeed not yield a new
action object.
Angular 17
1. Server-Side Rendering project → ng new new_project --ssr
2. New syntax →
<section>
@if (user.loggedIn) { // Our code }
@else if (user.role === 'admin') { // Our code }
@else { // Our code }
</section>
---------------------------------------------------------------
<section>
@for (user of userList; track user) {
<app-card [data]="user" />
} @empty {
<p>No item available</p>
}
</section>
[Note: The track is now required. It will help our loop be faster and more efficient. We
can also specify a fallback when there are no items in the list by using @empty.]
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
95
---------------------------------------------------------------
<section>
@switch (membershipStatus) {
@case ('gold') { <p>Your discount is 20%</p> }
@case ('silver') { <p>Your discount is 10%</p> }
@default { <p>Keep earning rewards</p> }
}
</section>
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
96
// Use Condition
<button (click)="load = true"> Load Component </button>
@defer (when load == true) {
<recommended-movies />
}
// Prefetch
@defer (prefetch on immediate; prefetch when val === true) {
// @defer (on interaction(showContent); prefetch on idle)
<recommended-movies />
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
97
Lazy Loading
What is @defer? → It is an Angular template syntax that enables loading
parts of a template or components only when they are needed. It provides two levels
of control:
1. Prefetching: Controls when code or data should be fetched from the
backend and loaded into memory.
2. Rendering: Allows code or components to be rendered separately on the
page only when required.
@defer() { <large-component /> }
@defer() { <h1>Heading</h1> }
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
98
Release on 17.2
1. Two-way binding with Signal Inputs(Model Inputs) → Read Me, Read Me.
Important Things
1. To disable strict mode in a project, set strict: false in our tsconfig.json file.
2. To create a data model, first declare the data fields in a class and use the
constructor to initialize values for these fields.
export class Ingredient {
public name: string;
public amount: number;
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
99
Shortcut way →
export class Ingredient {
constructor(public name: string, public amount: number) {}
}
// Using $implicit
<ng-template #show let-person>
<p>My name is {{ person.name }}, I am {{ person.age }} years old.</p>
</ng-template>
<ng-container
*ngTemplateOutlet="show; context : {$implicit: showDetails}">
</ng-container>
const showDetails = {
name: 'Mr. X',
age: 45
};
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
100
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
101
Basics
Component
1. Structure of a Component(component.ts) →
----------------------- demo.component.ts -----------------------
import { Component } from '@angular/core';
@Component({
selector: 'app-demo',
standalone: true,
imports: [HeaderComponent], // Component list that use under this demo component
// template: '<p>Hello</p>' // We can write HTML code here
templateUrl: './demo.component.html',
styleUrls: ['./demo.component.scss'],
// styles: [`h3{color: white}`] // We can write CSS code here
})
export class DemoComponent {}
[Note: We cannot use `template` and `templateUrl` simultaneously, and the same
rule applies to `styles` and `styleUrls`.]
2. To use files (such as images or SVGs) from the assets directory in a component
file, ensure "src/assets" is included in the assets array of architect > build >
options object of the angular.json file. Without this, these images won’t load.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
102
3. Advance Projection →
---------------------- button.component.html --------------------
<span> <ng-content /> </span>
<span>
<ng-content select="icon" />
</span>
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
103
<app-control label="Request">
<textarea name="request" id="request" rows="3"></textarea>
</app-control>
<p>
<button appButton>
Logout
<span ngProjectAs="icon">→</span>
</button>
</p>
</form>
Host Element
1. Component Host Element →
--------------------- button.component.html ---------------------
<span> <ng-content /> </span>
<span> <ng-content select="icon" /> </span>
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
104
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
105
<app-control label="Request">
<textarea name="request" id="request" rows="3"></textarea>
</app-control>
</form>
onCLick() {}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
106
Binding
1. Class Binding →
<div [class.status] = "currentStatus === 'offline'"></div>
--------------------- Alternatively ---------------------
<div [class]="{
status: true,
'status-online': currentStatus === 'online',
'status-offline': currentStatus === 'offline',
'status-unknown': currentStatus === 'unknown'
}"> </div>
2. Style Binding →
<div [style.fontSize] = "'64px'"></div>
<div [style.height]="(dataPoint.value / maxTraffic) * 100 + '%'"> </div>
--------------------- Alternatively ---------------------
<div [style] ="{ fontSize: '64px' }"></div>
Lifecycle Methods
https://angular.dev/guide/components/lifecycle
ngOnInit() {
const interval = setInterval(() => {
if (Math.random() < 0.5) {
this.currentStatus = 'online';
}
}, 5000);
this.destroyRef.onDestroy(() => {
clearInterval(interval);
});
}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
107
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
108
onSubmit() {
this.form().nativeElement.reset();
// If we don't use required, then we should use optional after form().
}
}
onCLick() { console.log(this.control()); }
}
--------------------- shared.component.html ---------------------
<label>{{ label }}</label>
<ng-content select="input, textarea" />
---------------------- parent.component.ts ----------------------
<form (ngSubmit)="onSubmit()" #form>
<app-control label="Title">
<input name="title" id="title" #input />
</app-control>
<app-control label="Request">
<textarea name="request" id="request" rows="3" #textInput #input></textarea>
</app-control>
</form>
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
109
Signal
----------------------- demo.component.ts -----------------------
export class DemoComponent {
selectedUser = signal(Users[0]);
imagePath = computed(() => 'assets/users/' + this.selectedUser().avatar);
onSelectedUser() { this.selectedUser.set(Users[randomIndex]); }
avatar = input<string>();
name = input.required<string>();
select = output<string>();
Signal Effects
Use Case: we may want to trigger code within the TypeScript class whenever
a Signal changes, beyond just updating the template automatically. This is helpful
when we need specific logic to run in response to Signal updates, not directly tied to
the UI.
Solution: Modern angular provides us a special function called effect, which
we can execute in our constructor and effect takes a function as an argument. If we use
a Signal in that function that's passed to effect, Angular will set up a subscription. The
subscription will automatically clean up if that component should get removed from
the DOM.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
110
constructor() {
effect(() => { console.log(this.currentStatus()); });
effect(() => { console.log('1'); });
}
ngOnInit() {
const interval = setInterval(() => {
if (Math.random() < 0.5) {
this.currentStatus.set('online');
} else { this.currentStatus.set('unknown'); }
}, 3000);
this.destroyRef.onDestroy(() => { clearInterval(interval); });
}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
111
@if (selectedUser) {
<app-tasks [userId]="selectedUser.id" />
} @else {
<p id="fallback">Select a user to see their tasks</p>
}
</main>
Directive
In modern Angular, there are no built-in structural directive left.
1. Custom Structural directive →
-------------------- safe-link.directive.ts ---------------------
@Directive({
selector: 'a[appSafeLink]',
standalone: true,
host: { '(click)': 'onConfirm($event)' }
})
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
112
<ul>
<li>
<a href="https://angular.dev" appSafeLink="myapp-docs-link"></a>
</li>
<li>
<a href="https://angular.dev" appSafeLink="myapp-course-link"></a>
</li>
</ul>
2. Dependency Injection →
-------------------- safe-link.directive.ts ---------------------
export class SafeLinkDirective {
queryParam = input('myapp', { alias: 'appSafeLink' });
private hostElementRef = inject<ElementRef<HTMLAnchorElement>>(ElementRef);
onConfirm(event: MouseEvent) {
const address = (event.target as HTMLAnchorElement).href;
this.hostElementRef.nativeElement.href = address+'?from='+this.queryParam;
}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
113
3. Structural Directive →
-------------------- safe-link.directive.ts ---------------------
@Directive({
selector: '[appAuth]',
standalone: true
})
export class AuthDirective {
userType = input.required<'user' | 'guest' | 'default'>({ alias: 'appAuth' });
private authService = inject(AuthService);
private templateRef = inject(TemplateRef);
// When building a structural directive, it is super important
private viewContainerRef = inject(ViewContainerRef);
// This view container ref works as a reference to the place in the DOM
constructor() {
effect(() => {
if (this.authService.activePermission() === this.userType()) {
this.viewContainerRef.createEmbeddedView(this.templateRef);
} else {
this.viewContainerRef.clear();
}
});
}
}
---------------- HTML file where we use the directive ----------------
<ng-template appAuth="admin">
<p>Hello World!</p>
</ng-template>
4. Host Directive → Imagine we have a directive that contains a click event in the
host object which is linked to a method that logs the host element ref.
Typically, we use the directive with HTML tags where needed. However, there is
another way to use it that works effectively. Modern angular provides a method
called hostDirectives, which we can add to the decorator object.
------------------------ log.directive.ts -----------------------
@Directive({
selector: '[appLog]',
standalone: true,
host: { '(click)': 'onLog()' }
})
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
114
addTask(taskData: any) {
const newTask: Task = {
...taskData,
id: Math.random().toString(),
status: 'OPEN'
};
this.tasks.update((oldTasks) => [...oldTasks, newTask]);
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
115
bootstrapApplication(AppComponent, {
providers: [{ provide: TasksServiceToken, useClass: TasksService}]
}).catch((err) => console.error(err));
--------------------- account.component.ts ----------------------
export class AccountComponent {
private tasksService = inject(TasksServiceToken);
}
Change Detection
How does the Angular change detection Mechanism work by default?
Let’s imagine a component tree(where one component is nested within another),
and angular wraps this entire tree within a ‘zone’. This zone is a feature provided by
zone.js(a library maintained by the angular team). It informs Angular about potential
events happening on the pages that may need to be reflected on the screen.
For instance, if a user clicks a button, Angular is notified of this event, triggering a
change detection process. Angular then proceeds to visit every component in the
application, regardless of where the event occurred. It examines all templates and
template bindings, such as property bindings and string interpolations, checking if any
bindings produce a new value. If a new value is detected, Angular updates the real
DOM to reflect this change. This is how Angular's change detection works by default.
In development mode, Angular runs change detection twice by default. If code
within a block whose value changes after every change detection cycle, we get an error
called ExpressionChangedAfterItHasBeenCheckedError.
1. Avoiding Zone Pollution →
To prevent zone.js from monitoring specific sections of code, we can use the
runOutsideAngular method. By passing a function to this method, we can
wrap code that should execute outside of Angular’s change detection or
zone.js monitoring. This approach helps avoid unnecessary change detection
cycles.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
116
ngOnInit() {
this.zone.runOutsideAngular(() => {
setTimeout(() => {
console.log('Time expired');
}, 5000);
});
}
}
2. OnPush strategy →
To avoid expensive function calls, Angular offers a powerful strategy that
can significantly improve application performance: the OnPush change detection
strategy. This alternative change detection approach can be applied on a
per-component basis to ensure that change detection runs less frequently for
selected components.
When the OnPush strategy is enabled on a component, Angular will only
reevaluate that component and its child components if an event occurs within
that component or one of its child components, or if an input value changes.
However, if the OnPush strategy is enabled only on a child component, that
child component will not reevaluate when events occur in the parent
component.
This selective reevaluation process helps improve application performance by
reducing unnecessary change detection cycles.
---------------------- demo.component.ts ------------------------
@Component({
selector: 'app-demo',
standalone: true,
templateUrl: './demo.component.html',
styleUrl: './demo.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
117
get allMessages() {
return [...this.messages];
}
addMessage(message: string) {
this.messages = [...this.messages, message];
this.message$.next([...this.messages]);
}
}
***Subscribe Way***
------------------ message-display.component.ts ------------------
export class MessagesListComponent implements OnInit {
private messagesService = inject(MessagesService);
private cdRef = inject(ChangeDetectorRef);
private destroyRef = inject(DestroyRef);
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
118
***Async Way***
------------------ message-display.component.ts ------------------
@Component({
selector: 'display-message',
standalone: true,
imports: [AsyncPipe],
templateUrl: './display-message.component.html',
styleUrl: './display-message.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MessagesListComponent {
private messagesService = inject(MessagesService);
message$ = this.messagesService.messages$;
}
----------------- message-display.component.html -----------------
<ul>
@for (message of messages$ | async; track message) {
<li>{{ message }}</li>
}
</ul>
4. Zoneless App -
To make an Angular app zoneless, first, remove zone.js from the polyfills
array in the angular.json file. Then, add
provideExperimentalZonelessChangeDetection to the
bootstrapApplication function within the providers array.
Note: This is an experimental feature available in Angular 18. It works best
when Signals are used consistently throughout the project.
--------------------------- main.ts ----------------------------
import { bootstrapApplication } from '@angular/platform-browser';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [provideExperimentalZonelessChangeDetection()]
}).catch((err) => console.error(err));
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
119
RxJS observables
In the previous section, we explored RxJS. Now, we are working with Signals,
which offer similar features to RxJS in some respects.
RxJS also includes Subjects, which are a specific type of observable with unique
properties. In modern Angular, however, we have a new built-in concept called
Signals, which functions differently from observables. Now, let’s explore Signals,
focusing on the similarities and differences between Observables and Signals. We’ll
also cover how to convert a Signal to an Observable and vice versa.
Creating and Using an Observable -
—----------------------- messages-list.component.ts —----------------------
export class MessagesListComponent {
clickCount = signal(0);
constructor() {
effect(() => { console.log(this.clickCount()); });
}
onCLick() { this.clickCount.update(prevCount => prevCount + 1); }
}
[Note: For every count update, the console.log will print the value.]
Signals vs Observables →
When comparing Signals to Observables (especially Subjects), they may initially
seem quite similar. For instance, when using a Subject in a service to share state
between different components, we could use a Signal instead—this might even be a
better approach in modern Angular.
The key difference is that Observables provide a pipeline of values emitted over
time, while Signals act as value containers. Signals allow you to change values, and
any observers are notified immediately. A significant advantage of Signals is that you
can access the current value directly at any time without needing a subscription.
– Using RxJS –
ngOnInit() {
const subscription = interval(1000)
.pipe(map((val: number) => val * 2))
.subscribe(() => {
next: (val: number) => console.log(val); // 0 2 4 6 8
});
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
120
– Using Signal –
interval = signal(0);
doubleInterval = computed(() => this.interval() * 2);
constructor() {
effect(() => { console.log(this.doubleInterval()); });
}
ngOnInit() {
setInterval(() => {
this.interval.update(prevVal => prevVal + 1);
}, 1000)
}
constructor() {
// effect(() => { console.log(this.clickCount()); });
}
ngOnInit() {
// setInterval(() => { this.clickCount.update(prevVal => prevVal + 1); }, 1000);
const subscription = this.clickCount$.subscribe({
next: (val) => console.log(val)
});
this.destroyRef.onDestroy(() => { subscription.unsubscribe(); });
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
121
constructor() {
effect(() => { console.log(this.intervalSignal()); });
}
ngOnInit() {
// setInterval(() => { this.clickCount.update(prevVal => prevVal + 1); }, 1000);
}
}
Http
1. Handling HTTP Errors →
import { catchError } from 'rxjs';
export class AppComponent implements OnInit {
isFetching = signal<boolean>(false);
places = single<Place[]>();
ngOnInit() {
this.isFetching.set(true);
this.httpClient.get<{ places: Place[] }>(API_URL)
.pipe(map(
(resData: any) => resData.places),
catchError((error) => throwError(() => new Error(error))
)).subscribe({
next: (places) => { this.places.set(places); },
error: (error) => { console.log(error); },
complete: () => { this.isFetching.set(false); }
});
}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
122
@Injectable()
class LoggingInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<unknown>, handler: HttpHandler):
Observable<HttpEvent<any>> {
console.log('Outgoing ' + req.url);
return handler.handle(req).pipe(
tap({
next: (event: any) => {
if (event.type === HttpEventType.Response) {
console.log('Incoming Response.');
console.log(event.status, event.body);
}
}
})
);
}
}
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
123
Angular Forms
Template Driven Approach
1. Accessing form with signal viewChild() →
---------------------- demo.component.html ----------------------
<form #form="ngForm" (ngSubmit)="onSubmit(form)"> </form>
----------------------- demo.component.ts -----------------------
private form = viewChild.required<NgForm>('form');
private destroyRef = inject(DestroyRef);
constructor() {
afterNextRender(() => {
const subscription = this.form().valueChanges?.pipe(debounceTime(500))
.subscribe({
next: (value) => window.localStorage.setItem(
'search-value', JSON.stringify({ email: value.email })
),
});
Routing
Setting & Config
-------------------------app.routes.ts –------------------------
export const routes: Routes = [
{ path: 'tasks', component: TasksComponent }
];
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
124
Parameter
1. Extracting Dynamic Route Parameters via Inputs →
-------------------------app.routes.ts –------------------------
export const routes: Routes = [
{ path: 'tasks', component: TasksComponent },
{ path: 'user/:userId', component: UsersComponent,
children: [
{ path: 'tasks', component: TasksComponent },
{ path: 'tasks/new' component: NewTaskComponent }
],
resolve: {
userName: resolveUserName // Can add multiple resolver here for multiple field
}
}
];
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
125
onCompleteTask() {
this.tasksService.removeTask(this.tasks().id);
this.router.navigate(['./'], {
relativeTo: this.activatedRoute, onSameUrlNavigation: 'reload'
});
}
}
[Note: Ensure that we set runGuardsAndResolvers: 'always' for this page route.]
Nested Route
1. Accessing Parent Route Data from Inside Nested Routes →
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
126
-------------------------app.routes.ts –------------------------
export const routes: Routes = [
{ path: 'tasks', component: TasksComponent },
{ path: 'user/:userId', component: UsersComponent,
children: [
{ path: 'tasks', component: TasksComponent },
{ path: 'tasks/new' component: NewTaskComponent }
]
}
];
----------------------- user.component.ts –----------------------
export class UsersComponent {
userId = input.required<string>();
// Here we get userId from route parameter
}
Guard
1. CanMatch(Function based and Class based)→
------------------------- app.routes.ts -------------------------
// We can write this function anywhere in our project
const dummyCanMatchs: CanMatchFn = (route, segments) => {
const router = inject(Router);
const shouldGetAccess = Math.random();
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
127
2. CanDeactivateFn(Function base) →
----------------------- tasks.component.ts ----------------------
// We can write this function anywhere in our project
export const canLeaveEditPage: CanDeactivateFn<NewTaskComponent> = (component) => {
if (component.enteredTitle() || component.enteredSummary())
return window.confirm('Are you sure you want to leave?');
return true;
}
------------------------- app.routes.ts ------------------------
const routes: Routes = [
{ path: '', redirectTo: 'tasks', pathMatch: 'full' },
{ path: 'tasks', component: TasksComponent,
runGuardsAndResolvers: 'always', canMatch: [dummyCanMatchs],
},
{ path: 'tasks/new', component: NewTaskComponent,
canDeactivate: [canLeaveEditPage]
}
];
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
128
2. Lazy-Loading Services →
To enable lazy loading for a service, first, remove { providedIn: 'root' } from the
service file if it is present.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
129
Deferrable Views
// Now, the app-offer-preview component will load lazily
@defer { <app-offer-preview /> }
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
130
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
131
SPAs do not require a dynamic web server; a static host is sufficient to serve the built
application. For SPA deployment, Follow the link - deployment
Server-Side Rendering(SSR)
In this mechanism, an Angular app's routes are rendered on-demand by a
dynamic web server. When a user requests to visit a specific page, the server first
handles the request and pre-renders the requested page. This pre-rendered page is
then sent back to the user.
Advantages of SSR:
● Users receive a fully rendered HTML file with all the content, rather than an
empty or nearly empty HTML file.
Disadvantages of SSR:
● If rendering a component takes longer due to data fetching, the user may
experience delays in receiving a response.
● SSR introduces additional complexity to the deployment process.
Setting Up SSR →
1. To prepare our Angular application for server-side rendering, run the following
command: ng add @angular/ssr.
2. Alternatively, when creating a new Angular project, you can add the SSR flag:
ng new project-name --ssr
3. After executing the first command, you will be prompted with several questions
to complete the setup for server-side rendering. This process updates
TypeScript and Angular configuration files and adds various settings to support
SSR. It also creates a server.ts file, which contains the Node.js server code used
to serve your application.
4. Then run - npm run build. Angular will recognize that it should build the
project for server-side rendering. Upon completion, you will find both a
browser and a server directory within the dist/routing directory. Note
that for SPA builds, only the browser directory is generated.
For deployment - https://firebase.google.com/docs/app-hosting/get-started
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
132
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
133
user task page where user tasks are dependent on userId). To prerender the
specific page we can declare the route in a txt file and then, we must add the file
into the angular.json file.
------------------------- angular.json –------------------------
"options": {
"server": "src/main.server.ts",
"prerender": {
"routesFile": "user-routes.txt"
},
"ssr": { "entry": "server.ts" }
}
------------------------- user-routes.ts –-----------------------
/users/u1/tasks
/users/u2/tasks
Client Hydration
Client Hydration If you open the src/app/app.config.ts in our new SSR
application, we'll notice one interesting provider: provideClientHydration().
This provider is crucial for enabling hydration - a process that restores the server-side
rendered application on the client.
Hydration improves application performance by avoiding extra work to re-create
DOM nodes. Angular tries to match existing DOM elements to the application's
structure at runtime and reuses DOM nodes when possible. This results in a
performance improvement measured using Core Web Vitals statistics, such as reducing
the Interaction To Next Paint (INP), Largest Contentful Paint (LCP), and Cumulative
Layout Shift (CLS). Improving these metrics also positively impacts SEO performance.
Without hydration enabled, server-side rendered Angular applications will
destroy and re-render the application's DOM, which may result in visible UI flicker,
negatively impact Core Web Vitals like LCP, and cause layout shifts. Enabling hydration
allows the existing DOM to be reused and prevents flickering.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com
134
during the SSR phase and replays them once the application is hydrated. This ensures
that user actions, such as adding items to a cart, are not lost during the hydration
process, providing a seamless user experience even on slow networks.
To enable event replay, you can add withEventReplay to your
provideClientHydration call:
bootstrapApplication(App, {
providers: [provideClientHydration(withEventReplay())],
});
Constraints
However, hydration imposes a few constraints on your application that are not
present without hydration enabled. Your application must have the same generated
DOM structure on both the server and the client. The hydration process expects the
DOM tree to have the same structure in both environments, including whitespaces and
comment nodes produced during server-side rendering.
S M HEMEL
Senior Software Engineer
Email: smhemel.eu@gmail.com