This post was created for ng-newsletter and originally published on www.newline.co
Angular has some edge cases that can sometimes waste your time if you're not familiar with the details.
I've been working with Angular for the last 3,5+ years and here are some interesting things that I've learned that will save your time. We're going to talk about:
How HTTPInterceptors work with feature modules
Query params are lost on angular router redirect
::ng-deep span - avoid encapsulation for specific elements (on an example of search words highlighting)
Http with no subscribe
Reload component on same URL navigation
Let's dig in.
You can view the runnable demo here in Stackblitz and review the code in this Github repo.
If you want to get notified about my future video-course with advanced techniques of mastering Angular - Subcribe! (And watch some free videos here)
#1. How HTTPInterceptors work with feature modules
Once upon a time, I had a mentoring session on codementor.io where we integrated some separate Angular application to work as a feature module of the main project app. Code was copied to the project features folder it was added a lazy-loaded module and added to the main app routing config.
The main app.routing.ts
looked like this:
const routes: Routes = [
{path: '', redirectTo: 'home', pathMatch: 'full'},
{path: 'home', component: HomeComponent},
{ path: 'feature',
loadChildren: './some-feature/some-feature.module#SomeFeatureModule'}
];
And feature.module
:
@NgModule({
declarations: [
SomeComponentComponent
],
imports: [
CommonModule,
SomeFeatureRoutingModule,
HttpClientModule
],
bootstrap: [
SomeComponentComponent
]
})
export class SomeFeatureModule { }
At the end directory structure looks very similar to the code in our demo project:
From a first glance - it should work like a charm. And actually it does.
Main app component, as well as feature module component, perform some HTTP calls with standard Angular HttpClientModule
and specific token-interceptor appends Authorization header to all outgoing network requests.
But somehow feature module network requests were not handled by a token-interceptor of the main app.
Well, I will not torment you with intrigue. The problem was that during independent development of feature - individual HTTPClientModule
was added to imports array of feature.module.ts
. And it made Angular create another instance of HttpModule
with an empty custom interceptors list.
How to avoid it? Just comment out HTTPClientModule
in all other Angular modules except the main app module. (App.module.ts
)
Now, the feature module looks like this:
@NgModule({
declarations: [
SomeComponentComponent
],
imports: [
CommonModule,
SomeFeatureRoutingModule,
// HttpClientModule <-- commented out
],
bootstrap: [
SomeComponentComponent
]
})
export class SomeFeatureModule { }
Knowing that peculiarity allows you to implement specific HTTPInterceptors
for particular feature modules. All you have to do is to add HTTPClientModule
to specific Angular feature module and define HTTP_INTERCEPTORS
provider in the same module as well. Now in your feature module only its own interceptors will be used while all other application modules will use the root app module interceptor.
@NgModule({
declarations: [
SomeComponentComponent
],
imports: [
CommonModule,
SomeFeatureRoutingModule,
HttpClientModule // separate feture module HTTPClientModule
],
bootstrap: [
SomeComponentComponent
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: FeatureInterceptor, // Hey, we have specific module interceptor now
multi: true
}
],
})
export class SomeFeatureModule { }
Let's check how it works:
You can play with code in a Stackblitz playground. Also, the full project code is uploaded to GitHub "Mastering Angular" repo.
#2. Query params are lost on angular router redirect
Sometimes we have to specify query params for our Angular SPA. In that case, our application URL may look like:
http://yourdomain.com/?serach=text&page=3
It should work well. But very often Angular routing config uses redirectTo directive to lead request to a specific home component:
const routes: Routes = [
{path: '', redirectTo: '/home', pathMatch: 'full'},
{path: 'home', component: HomeComponent}
]
And we expect that Angular will change URL to: http://yourdomain.com/home?search=text&page=3
But practically Angular changes it to: http://yourdomain.com/home.
Query parameters are lost. HomeComponent will not be able to get them
How to deal with it?
There are three ways:
- The simplest. Just use the direct HomeComponent route URL with all params:
http://yourdomain.com/home?search=text&page=3
Redirect is not happening in this case, so queryParams will not be lost.
- The hardest. Listen to all route events and in case of redirect just copy query params and restore/assign them for /home page.
import {Component, OnInit} from '@angular/core';
import {NavigationEnd, Router} from '@angular/router';
import {filter, take, tap} from 'rxjs/operators';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'Mastering-angular';
constructor(private route: Router) {
}
ngOnInit() {
this.route.events.pipe(
tap(() => console.log('tap')),
filter((event) => event instanceof NavigationEnd
&& event.urlAfterRedirects === '/home'
&& event.url.search(/\?/) > -1 // look for '?'
),
take(1) // run handler once
).subscribe((event: NavigationEnd) => {
console.log('Test')
// extract queryParams as value: pair object.
const queryParams = event.url.split('?')[1]
.split('&')
.reduce((acc, item) => ({...acc, [item.split('=')[0]]: item.split('=')[1]}), {});
this.route.navigate([], {
queryParams, // reload current route and add queryParams
queryParamsHandling: 'merge'
});
});
}
}
- And one more nice solution - query params are not lost if we just remove '/' sign in route path, like this:
const routes: Routes = [
{path: '', redirectTo: 'home', pathMatch: 'full'},
{path: 'home', component: HomeComponent}
]
You can play with different methods here in a playground and check the code in an article repo.
Which method is best for you? Leave your choice in comments.
#3. ::ng-deep span - avoid encapsulation for specific elements (on an example of search words highlighting)
Ok, say you have a task make search text to be highlighted in specific paragraph.
How would you do that? The easiest way - just save the original text. And on each search just replace text we are looking for with <span class="highlight">textToSearch</span>
getHighlightedText(searchText) {
const regexp = new RegExp(searchText, 'gi');
const highlightedText = originalText.replace(regexp, '<span class="highlight">$&</span>');
return highlightedText;
}
And to highlight it in a component.scss
file we put the rule:
.highlight { background-color: yellow;}
You run it but...it doesn't work:
What is your gut feeling why it doesn't work? My first thought was that somehow our CSS rule was not included in a final bundle. So I decided to search for it and this is what I've found:
.highlight[_ngcontent-tls-c1] {
background-color: yellow;
}
Angular applies CSS encapsulation for all component css rules. That's why our element is not highlighted. How to fix it? Just use ::ng-deep prefix to tell Angular that this specific rule should not be modified:
::ng-deep .highlight {
background-color: yellow;
}
Now if we check our search with highlighting - it will work good:
You can play with code on a Stackblitz page or check it in GitHub repo. You can read more about ::ng-deep and styles encapsulation here.
#4. Http with no subscribe
If you monitoring the Angular community on Twitter then you may notice that template-driven development is becoming a new trend.
Good examples of that approach are:
- Async-pipeline from @NikPoltoratsky - a set of interesting pipes that apply different RxJS operators in a template, for example:
// app.component.ts
@Component({
template: <code>
{{ title$ | debounceTime:1000 | async }}
</code>,
})
export class AppComponent {
title$: Observable<string> = of('Hello, Async Pipeline!');
}
- ngx-template-streams by Dominic Elm - allows to assign domain events to specific properties in component class in a reactive way:
//html
<button (*click)="clicks$">Click Me (Stream)</button>
//ts
import { Component, OnInit } from '@angular/core';
import { ObservableEvent } from '@typebytes/ngx-template-streams';
import { Observable } from 'rxjs';
@Component({...})
export class AppComponent implements OnInit {
@ObservableEvent()
clicks$: Observable<any>;
ngOnInit() {
// we can either manually subscribe or use the async pipe
this.clicks$.subscribe(console.log);
}
}
- ngxf/platform by ReactiveFox with a very good doc - a set of structured directives to reduce angular code boilerplate, for example:
<ng-container *route="let route">
{{ route }} // is ActivatedRoute instance
</ng-container>
- And interesting "RFC: Component proposal" issue thread from Michael Hladky on how to make Angular even more reactive.
Let's apply this approach to quite a routine task when our Angular component does some network request inside ngOnInit hook. Very often code for that looks like this:
export class SomeComponentComponent implements OnInit {
data = {};
constructor(private http: HttpClient) { }
ngOnInit() {
this.http.get('https://jsonplaceholder.typicode.com/todos/2')
.subscribe((data) => {
this.data = data;
});
}
}
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title"><b>User id:</b> {{data.userId}} </h5>
<p class="card-text"><b>Title:</b> {{data.title}}</p>
</div>
</div>
<div class="wrapper">Raw user data: {{data | json}}</div>
But, as Michael Hladky mentions in his article "How to Avoid Observables in Angular" - it is not reactive programming:). I would say - it is not a template-driven approach.
Can we do better?
Yes, we can omit the intermediary variable/property and use Angular async pipe. Now our code will look like this:
export class HttpCallComponent {
data$ = this.http.get('https://jsonplaceholder.typicode.com/todos/2');
constructor(private http: HttpClient, private snackBar: MatSnackBar) { }
}
<ng-container *ngIf="data$ | async as data">
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title"><b>User id:</b> {{data.userId}} </h5>
<p class="card-text"><b>Title:</b> {{data.title}}</p>
</div>
</div>
<div class="wrapper">Raw user data: {{data | json}}</div>
</ng-container>
But what about error handling in case of network 404 response? Let's apply catchError operator to handle network request failure - we will show an error message from its callback to notify the user of what happened.
export class HttpCallComponent {
data$ = this.http.get('https://jsonplaceholder.typicode.com/todos/2').pipe(
catchError((err) => {
this.snackBar.open('Network request is failed', 'Failed', {
duration: 1500,
});
return of();
})
);
constructor(private http: HttpClient, private snackBar: MatSnackBar) { }
}
Time to check how it works:
LGTM! You can play with this example in a Stackblitz playground and check code in the article GitHub repo.
#5. Reload component on same URL navigation
OK, so you got a legacy code where the main component logic is set in ngOninit() lifecycle method. This means that if time is short - if we need to reset component values to initial - it is easier to re-create the whole component from scratch. But the problem is hidden here: Angular doesn't re-instantiate component when we try to navigate to the same URL that is already activated.
OK, this is a problem. First what I did to find a solution - I read a nice article "Angular: Refetch data on same URL navigation". But it doesn't provide solution with total component instance re-initialization - just methods to catch an event that activated route was clicked again. This is not what we want.
What if it is possible to load some other route and then return to the current active one? But it may cause some visual artifacts on a page so a user can observe it. Can we somehow do that without any visual artifacts?
The answer is "Yes" and I found it here. To be short - solution code is simple and nice:
this.router.navigateByUrl('/', {skipLocationChange: true})
.then(()=>this.router.navigate([<route>]));
To apply it we should change our routeLInk
to function call in app.component.html
:
<div class="col-12 text-center">
<a [routerLink]="['/home']" >Home</a>
<a [routerLink]="['/feature']" >Feature</a>
<a [routerLink]="['/search']" >Search</a>
<a [routerLink]="['/httpOnInit']" >HttpOnInit</a>
<!-- <a [routerLink]="['/sameRoute']" >SameRouteReload</a>-->
<a class="route_link" (click)="goToRandomNUmberPage($event)" >SameRouteReload</a>
</div>
And apply this code in app.component.ts
:
goToRandomNumberPage(event) {
event.preventDefault();
this.router.navigateByUrl('/', {skipLocationChange: true})
.then(() => this.router.navigate(['/sameRoute']));
}
Now it should work as we want:
You can check it in Stackblitz codepen and review the code in article github repo.
Summary
Angular-city streets are a safe place for everyone whether you are a beginner or experienced developer, but in dark corners, there are entities that wait for their turn to waste your time. I hope this article will help you to eliminate time-waste and deliver maintainable clean code quickly and in high quality.
If you liked this article - join me on Twitter and Buy me a coffee ☕️🤓.