Skip to main content

12 posts tagged with "Angular"

View All Tags

Service Provided in a Lazy Loaded Module

· One min read

Open in Notion

https://angular.io/guide/ngmodule-faq#why-is-a-service-provided-in-a-lazy-loaded-module-visible-only-to-that-module

Unlike providers of the modules loaded at launch, providers of lazy-loaded modules are module-scoped.

When the Angular router lazy-loads a module, it creates a new execution context. That context has its own injector, which is a direct child of the application injector.

The router adds the lazy module's providers and the providers of its imported NgModules to this child injector.

These providers are insulated from changes to application providers with the same lookup token. When the router creates a component within the lazy-loaded context, Angular prefers service instances created from these providers to the service instances of the application root injector.

Global Error Handling

· One min read

Open in Notion

global-error-handler.ts

import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHandler, Injectable, NgZone } from '@angular/core';

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
constructor() {}

public handleError(error: any): void {
// Check if it's an error from an HTTP response
const isServerError = error instanceof HttpErrorResponse;

if (!isServerError) {
// TODO
}

console.error('Error from Global Error Handler', error);
}
}

app.module.ts

@NgModule({
declarations: [AppComponent],
imports: [
CommonModule,
HttpClientModule,
],
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
],
bootstrap: [AppComponent],
})
export class AppModule {}

Preloading

· 2 min read

Open in Notion

https://angular.io/guide/lazy-loading-ngmodules#preloading-modules

import { PreloadAllModules } from '@angular/router';
RouterModule.forRoot(
appRoutes,
{
preloadingStrategy: PreloadAllModules
}
)

Preloading component data

import { Resolve } from '@angular/router';

...

/* An interface that represents your data model */
export interface Crisis {
id: number;
name: string;
}

export class CrisisDetailResolverService implements Resolve {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
// your logic goes here
}
}
import { CrisisDetailResolverService } from './crisis-detail-resolver.service';
{
path: '/your-path',
component: YourComponent,
resolve: {
crisis: CrisisDetailResolverService
}
}
import { ActivatedRoute } from '@angular/router';

@Component({ ... })
class YourComponent {
constructor(private route: ActivatedRoute) {}

ngOnInit() {
this.route.data
.subscribe(data => {
const crisis: Crisis = data.crisis;
// ...
});
}
}

Preloading Strategies

Available Preloading strategies

  • Build-in preloading strategies — NoPreloading (default) or PreloadAllModules.
  • Custom preloading strategies — Preload after some time, preload based on network quality, load required modules first, frequently used second, and others lazy load/last.

Preloading all the modules (PreloadAllModules)

Custom preloading strategies

app-routing.module.ts

import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {CustomPreloadingStrategyService} from './custom-preloading-strategy.service';
const routes: Routes = [
{path: 'about', data: {preload: true}, loadChildren: () => import('./about/about.module').then(m => m.AboutModule)},
{path: 'users', loadChildren: () => import('./users/users.module').then(m => m.UsersModule)},
{path: '', redirectTo: '', pathMatch: 'full'}
];
@NgModule({
imports: [RouterModule.forRoot(routes, {preloadingStrategy: CustomPreloadingStrategyService})],
exports: [RouterModule]
})
export class AppRoutingModule {
}

custom-preloading-strategy.service.ts

import {Injectable} from '@angular/core';
import {PreloadingStrategy, Route} from '@angular/router';
import {Observable, of} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class CustomPreloadingStrategyService implements PreloadingStrategy {
preload(route: Route, fn: () => Observable<any>): Observable<any> {
if (route.data && route.data.preload) {
return fn(); // Proceeds with preloading
}
return of(null); // Proceeds without preloading
}
}

Unit Test

· One min read

Open in Notion

import {HttpTestingController} from '@angular/common/http/testing';

describe('DataService', () => {
let service: DataService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
providers: [DataService]
});
service = TestBed.get(DataService);
httpMock = TestBed.get(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});

it('be able to retrieve posts from the API bia GET', () => {
const dummyPosts: Post[] = [{
userId: '1',
id: 1,
body: 'Hello World',
title: 'testing Angular'
}, {
userId: '2',
id: 2,
body: 'Hello World2',
title: 'testing Angular2'
}];
service.getPost().subscribe(posts => {
expect(posts.length).toBe(2);
expect(posts).toEqual(dummyPosts);
});
const request = httpMock.expectOne( `${service.ROOT_URl}/posts`);
expect(request.request.method).toBe('GET');
request.flush(dummyPosts);
});
});

Cache for HttpClient

· One min read

Open in Notion

@Injectable()
class CacheInterceptor implements HttpInterceptor {
private cache: Map<HttpRequest, HttpResponse> = new Map()
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>{
if(req.method !== "GET") {
return next.handle(req)
}
if(req.headers.get("reset")) {
this.cache.delete(req)
}
const cachedResponse: HttpResponse = this.cache.get(req)
if(cachedResponse) {
return of(cachedResponse.clone())
}else {
return next.handle(req).pipe(
do(stateEvent => {
if(stateEvent instanceof HttpResponse) {
this.cache.set(req, stateEvent.clone())
}
})
).share()
}
}
}

According to above code, if you do not want to get the data from cache. you just pass a head parameter as below:

public fetchDogs(reset: boolean = false) {
return this.httpClient.get("api/dogs", new HttpHeaders({reset}))
}

And lastly, you must add the interceptor to module.

@NgModule({
...
providers: {
provide: HTTP_INTERCEPTORS,
useClass: CacheInterceptor,
multi: true
}
})
...

Fix Circular Dependency issue

· One min read

Open in Notion

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

constructor(
private injector: Injector,
// public status: StatusService, // do not do it if this leads to circular dependency issue
) { }

// use below instead
getStatusSerice() {
return this.injector.get<StatusSerice>(StatusSerice);
}
}

CLI

· One min read

Open in Notion

ng generate component|directive|pipe|service|class|guard|interface|enum|module --module=app
ng g directive ./src/app/directives/your-directive --module=shared


ng test --include path/to/your-spec-file
ng test --include src/**/*.spec.ts
ng test your-module --code-coverage

Install Error

internal/modules/cjs/loader.js:638
throw err;
^

Error: Cannot find module '/usr/lib/node_modules/@angular/cli/bin/postinstall/script.js'
sudo npm install -g @angular/cli --unsafe-perm=true --allow-root

Events of Component

· One min read

Open in Notion

Outside Click

import { Directive, Input, Output, EventEmitter, ElementRef, HostListener } from '@angular/core';

@Directive({
selector: '[clickOutside]',
})
export class ClickOutsideDirective {

@Output() clickOutside = new EventEmitter<void>();

constructor(private elementRef: ElementRef) { }

@HostListener('document:click', ['$event.target'])
public onClick(target) {
const clickedInside = this.elementRef.nativeElement.contains(target);
if (!clickedInside) {
this.clickOutside.emit();
}
}
}
<div (clickOutside)="someHandler()"></div>

Keydown of Component

@HostListener('document: keydown', ['$event'])
public onEnter(event: KeyboardEvent): void {
if (this.elementRef.nativeElement.contains(event.target)) {
if (event.code === 'Enter') {
// TODO:
}
}
}

‼️Tips

If the element contains *ngIf, this.overlay.nativeElement.contains(event.target); always return false. Because of the angular directive *ngIf

<div #overlay>
<div *ngIf="show" class="item">
Click here
</div>
</div>
@ViewChild('overlay', { static: false }) overlay: ElementRef;

//...
this.overlay.nativeElement.contains(event.target); // always return false

How to solve it?

You can check the event.target

Object.keys(event.target.classList).includes('item');

Or use [hidden] instead.

<div [hidden]="!show" class="item">
Click here
</div>