It's time to embrace the renaissance of Angular. Since v17 has launched many things have been changed for Angular. This buffy release has offered tons of new features and shift in the ideology of Angular development. The enhancement is continued any version beyond v17. In this article, we will adapt to this new way of development and will touch base some of the groundbreaking features.
Signals, Hydrations, Deferable Views, Control Flows, Enhanced SSR, Dev Tool and tons of performance improvements which has been witnessed post the launch of v17 of Angular, a detailed blog post has been provided from the Angular team here - Introducing Angular v17. Let us explore some of these concepts with our Angular Journey, since it's an obvious and most relevant thing to do as to embrace the upcoming future of Angular.
This article will mark the end to our Angular Journey (feeling emotional), we will adapt to the new way of Angular development and explore the feature along with our Notesy App. Though it's not an end to the learning instead a beginning for further explorations. In this reading, we will pick from where we left from our previous article Angular Journey: Part 5 - RxJS Touch.
We will head in this direction:
For this we need to get started with a fresh ng app, to setup in a newer Angular way. We call this version of our Notesy App as "ng-6-renaissance" (you can find the same in the repo). Take a clean directory to get started with and run below command:
ng new ng-6-renaissance
Install the necessary deps:
npm i bootstrap bootstrap-icons bootswatch uuid
npm i @types/uuid -D
Here, we are adding the Bootstrap for styling and UUID for the unique note id, these we have already covered in the first article.
Generate environment files using below command:
ng g environments
Update the environment files to have the JSON server API URL:
export const environment = {
apiUrl: 'http://localhost:3000/',
};
Update the style.scss to have the Bootstrap theme enabled:
@import "../node_modules/bootswatch/dist/litera/bootstrap.min.css";
@import "../node_modules/bootstrap-icons/font/bootstrap-icons.min.css";
Update title in index.html:
<title>Notesy</title>
Add data.json and server command for JSON server API:
{
"scripts": {
"server": "json-server data.json --delay 2000"
},
}
Post v17, we get one app.config file, which holds up the configuration for our app. This file was not there with the module-based approach. Post v17, we are having standalone components by default, if one wants to use the module driven approach, we need to explicitly mention the "--standalone=false" in the new command; the default is true. With the new app we kept it to default, and since we are not using the module-driven approach we get this app.config file:
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
],
};
Here, the ApplicationConfig has a property called providers in which we need to register the configuration for entire app. By default, we observe 2 methods added to the providers array, in our Notesy App we will also be needing the HttpClient provider as well to make the API calls. We need to add them as below:
import { provideHttpClient } from '@angular/common/http';
provideHttpClient(),
For this step we can simply move the components, constants, http, models, pages, and services folder from the "ng-5-rxjs" to our new "ng-6-renaissance" app. Also, make sure to remove "standalone: false" from all components.
With the standalone approach each component is now standing alone on its own without any module dependency. So, when a component using any component as a child, then it must need to be added to the parent component imports array in the component decorator. We can add component or modules to the imports array. For example, we are adding the item component to the list component:
import { CommonModule } from '@angular/common';
@Component({
imports: [ItemComponent],
})
export class ListComponent {...}
This way follows below list of imports and add them to their respective component mentioned:
app => imports: [RouterOutlet, HeaderComponent, NavbarComponent, FooterComponent, LoaderComponent]
footer, loader => imports: [CommonModule]
list => imports: [CommonModule, ItemComponent]
form => imports: [FormsModule]
navbar => imports: [RouterModule]
For the dev improvement we are now having control flows like @if, @for, @empty etc. They have slight performance benefit as compared to the ngIf and ngFor directives and dev useability. You can further read from here - Control Flow in Components.
Use the if condition in the loader component:
@if (loader) {
<h1 class="loader">
<i class="bi bi-snow2 text-success"></i>
</h1>
}
Add @if and @for into the list component:
@if ((notes$ | async); as notes) {
@for (note of notes; track note.id) {
<app-item [note]="note" (onRemove)="onRemove($event)"></app-item>
}
}
Here, in the @for we are using the track by method as well. The track by helps to keep track of the listed items. Notice we are not calling the reverse method, because this won't work with the control flows. Control flows doesn't consider any on fly changes of the array at template level. For this we will take help of a pipe.
Create reverse pipe, for this first create a folder called pipes and run below command:
ng g pipe reverse
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'reverse',
standalone: true,
})
export class ReversePipe implements PipeTransform {
transform<T>(value: T[]): T[] {
return value ? [...value].reverse() : [];
}
}
Use the reverse pipe in the @for control flow:
@if ((notes$ | async); as notes) {
@for (note of notes | reverse; track note.id) {
<app-item [note]="note" (onRemove)="onRemove($event)"></app-item>
}
}
The Angular signals is most exciting feature, which helps to write reactive states in a convenient way. The Signal concept is a big concept for which a dedicated article is worth to consider, will explore in future. But for now let us use the Signals for our loader and notes states instead of service base variable and BehaviorSubject.
Update the Loader Service - Convert the loader variable to signal:
loaders$ = signal<string[]>([]);
Remove the getter, as it won't be needed since we made the loaders$ signal variable public.
Update the show and hide methods:
show(value: string) {
this.loaders$.set([...this.loaders$(), value]);
}
hide(value: string) {
this.loaders$.set(this.loaders$().filter((loader) => loader !== value));
}
Here, to access the current value of the loaders$ we use the parenthesis.
Update the getter in the loader component:
get loaders$() {
return this.loaderService.loaders$;
}
Update the @if condition in the loader component template:
@if (loaders$().length > 0) {
<h1 class="loader">
<i class="bi bi-snow2 text-success"></i>
</h1>
}
In the Notes Service we have notes$ BehaviorSubject, let us convert it to Signal:
// before
notes$ = new BehaviorSubject<Note[]>([]);
// after
notes$ = signal<Note[]>([]);
Update the places where it is used in the Note Service:
tap((notes) => this.notes$.set(notes)),
tap((newNote) => this.notes$.set([...this.notes$(), newNote])),
tap(() => this.notes$.set(this.notes$().filter((note) => note.id !== id))),
To set the value of Signal we use the set method.
Update list component template where the notes$ is consumed:
@if ((notes$()); as notes) {
@for (note of notes | reverse; track note.id) {
<app-item [note]="note" (onRemove)="onRemove($event)"></app-item>
}
}
Update empty component template:
<div class="text-center p-3 text-muted">
<h1 class="display-4 text-secondary">
<i class="bi bi-egg me-2"></i>
</h1>
No notes found
</div>
Remove code logic from empty component:
@Component({
selector: 'app-empty',
templateUrl: './empty.component.html',
styleUrl: './empty.component.scss'
})
export class EmptyComponent {}
Using @for and @empty control flow in list component:
@if ((notes$()); as notes) {
@for (note of notes | reverse; track note.id) {
<app-item [note]="note" (onRemove)="onRemove($event)"></app-item>
} @empty {
<app-empty></app-empty>
}
}
Add the empty component to imports:
imports: [CommonModule, ItemComponent, EmptyComponent, ReversePipe],
Here, we are now using the @empty control flow instead of making the empty component to display on its own. Remove the empty component from the home component.
Run the Notesy App and perform all the get, add and remove operations.
December 31, 2020
October 19, 2020
March 02, 2022