Advanced Angular
workshop
slides: bit.ly/3FSDwvf
We expect cooperation from all participants to help ensure a safe environment for everybody.
We treat everyone with respect, we refrain from using offensive language and imagery, and we encourage to report any derogatory or offensive behavior to a member of the JSLeague community.
We provide a fantastic environment for everyone to learn and share skills regardless of gender, gender identity and expression, age, sexual orientation, disability, physical appearance, body size, race, ethnicity, religion (or lack thereof), or technology choices.
We value your attendance and your participation in the JSLeague community and expect everyone to accord to the community Code of Conduct at all JSLeague workshops and other events.
Code of conduct
Whoami
Andrei Antal
@andrei_antal
- frontend engineer, since i can remember
- currently doing consulting and training @JSLeague
- web & JS technologies enthusiast
- UX and accessibility passionate
- perpetual learner
Frontend Developer, Training Manager @ JsLeague
organizer for ngBucharest
@ngBucharest
groups/angularjs.bucharest
YOU?
- brief intro
- experience with course topics
- expectations from the course
Overview
Advanced Angular
Advanced Angular
Agenda for the course
- Intro
- Migrating to Standalone
- Signals and new Control flow
- Pipes and Directives
- Advanced forms scenarios
- Advanced Rxjs
- Authentication, Routing, Http
- State management with Ngrx & Signal store
- (other topics)
Advanced Angular
Schedule
- Session 10:00 - 11:30
- Break 11:30 - 11:45
- Session 11:45 - 13:00
- Lunch Break 13:00 - 14:00
- Session 14:00 - 15:30
- Break 15:30 - 15:45
- Session 15:45 - 17:00
Advanced Angular
Angular updates
Angular "renaissance"
-
Angular v14
- Standalone components (dev preview)
- Typed Forms
- inject()
- ngOptimizedImage directive
- deprecation of ComponentFactory API
-
Angular v15
- Standalone stable (http+router apis for moduless apps)
- directive composition API
- functional guards, resolvers, and interceptors (will be deprecated)
- self-closing tabs
Angular "renaissance"
-
Angular v16
- EsBuild + Vite (dev preview)
- Signals (dev preview)
- Hydration for SSR
- routing data as component inputs
- takeUntilDestoryed
- inputs for ngComponentOutlet
-
Angular v17
- EsBuild + Vite (stable)
- Signals (stable)
- New control flow
- deferred loading
- signal inputs, outputs, view querries
Angular "renaissance"
-
Angular v18
- zoneless change detection (experimental)
- ng content fallback
- Forms api improvements - events stream
- partial hydration support
- Stabilize APIs
- new docs website (angular.dev)
-
Angular v19
- @let
- standalone default in the CLI
- Faster builds
- New signals resources
Standalone APIs
NgModules, why?
@NgModule({
imports: [ CommonModule, Module1, Module2...],
declarations: [ AppComponent, Component1, Directive 1, ...],
...
})
export Class SomeModule {
}
- group parts of the app that belong together
- provide a compilation context for the angular compiler (essential for the View Engine renderer)
- components in Ivy rendering engine have their own compilation context
SCAM
Single Component Angular Module
@Component(...)
export Class AutocompleteComponent {
...
}
@NgModule({
imports: [
CommonModule,
...
],
declarations: [
AutocompleteComponent
],
exports: [AutocompleteComponent],
})
export Class AutocompleteModule {
...
}
Standalone components
ng generate component standalone --standalone (default true)
@Component({
selector: 'app-standalone',
standalone: true,
// all imports (modules and components) goes here
imports: [CommonModule, RouterOutlet, Component1],
templateUrl: './standalone.component.html',
styleUrls: ['./standalone.component.scss']
})
export class StandaloneComponent implements OnInit {
...
}
- The component can be defined without NgModule
Using a S.A. component
@NgModule({
imports: [
...,
// import library NgModule
WeatherWidgetModule,
...
]
})
export class DashboardComponent { }
@NgModule({
imports: [
...,
// import component directly
WeatherWidgetComponent,
SomeStandaloneDirective,
SomeStandalonePipe,
...
]
})
export class DashboardComponent { }
- We can import components and modules that export components
Standalone directives and pipes
@Pipe ({
standalone: true,
name: 'countWords',
pure: true // the default
})
export class CountWordsPipe implements PipeTransform {
transform (value: string, format: string): string {[…]}
}
@Directive ({
standalone: true,
selector: 'img[errorFallback]',
providers: […]
})
export class ErrorFallbackDirective {
...
}
Application bootstrap
- old way -
// app.module.ts
@NgModule({
declarations: [
AppComponent,
...
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
BrowserAnimationsModule,
...
],
providers: [...],
bootstrap: [AppComponent],
})
export class AppModule { }
// main.ts
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
Application bootstrap
// main.ts
bootstrapApplication(
AppComponent, // main component entry point
{
providers: [
importProvidersFrom([
SomeModule,
SomeOtherModule.forRoot(...)
]),
provideRouter(APP_ROUTES),
provideHttpClient(withInterceptors([...])),
provideAnimations(),
]
}
)
More granular imports
import {
// All standalone
AsyncPipe,
JsonPipe,
NgForOf,
NgIf
} from "@angular/common";
[...]
@Component({
standalone: true,
imports: [
// CommonModule,
NgIf,
NgForOf,
AsyncPipe,
JsonPipe,
...
],
})
export class AppComponent {
[...]
}
Routing
RouterModule.forRoot([
{
path: '',
component: HomeComponent,
},
{
path: 'movies',
loadChildren: () =>
import('./movies/movies.module').then((m) => m.MoviesModule),
},
]),
- Old way
bootstrapApplication(
AppComponent,
{
providers: [
provideRouter(APP_ROUTES),
]
}
)
- New way
Routing
// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
export const APP_ROUTES: Routes = [
{
path: '',
pathMatch: 'full',
redirectTo: 'home'
},
{
path: 'home',
component: HomeComponent
},
// Option 1: Lazy Loading another Routing Config
{
path: 'movies',
loadChildren: () => import('./movies/movies.routes').then(m => m.MOVIES_ROUTES)
},
// Option 2: Directly Lazy Loading a Standalone Component
{
path: 'movies-list',
loadComponent: () => import('./movies/movies-list.component').then(m => m.MoviesListComponent)
},
[...]
];
- Lazy load routing with standalone components
Routing
export const MOVIE_ROUTES: Route[] = [
{
path: '',
loadComponent: () =>
import('./components/movie-list/movie-list.component').then(
(c) => c.MovieListComponent
),
},
{
path: 'new',
loadComponent: () =>
import('./components/movie-detail/movie-detail.component').then(
(c) => c.MovieDetailComponent
),
},
{
path: ':id',
loadComponent: () =>
import('./components/movie-detail/movie-detail.component').then(
(c) => c.MovieDetailComponent
),
},
];
- Defining routes
Routing - functional guards
export const MOVIE_ROUTES: Route[] = [
{
path: '',
loadComponent: () =>
import('./components/movie-list/movie-list.component').then(
(c) => c.MovieListComponent
),
},
{
path: 'new',
loadComponent: () =>
import('./components/movie-detail/movie-detail.component').then(
(c) => c.MovieDetailComponent
),
},
{
path: ':id',
loadComponent: () =>
import('./components/movie-detail/movie-detail.component').then(
(c) => c.MovieDetailComponent
),
},
];
- Defining routes
Http
import { provideHttpClient, withInterceptors } from "@angular/common/http";
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([authInterceptor]),
),
]
});
- Functional interceptors
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authReq = req.clone({
headers: req.headers.set(
'Authorization',
`Bearer ${inject(AuthService).token()}`
),
});
return next(authReq);
};
Automatic migration
ng generate @angular/core:standalone
? Choose the type of migration: (Use arrow keys)
❯ Convert all components, directives and pipes to standalone
Remove unnecessary NgModule classes
Bootstrap the application using standalone APIs
New Control Flow
@if
@if (ifCondition) {
<div>If template</div>
} @else if (elseIfCondition) {
<div>Else-if Template</div>
} @else {
<div>Else Template</div>
}
@if(movie$ | async; as movie) {
// movie available as block variable
}
<div *ngIf="ifCondition; else elseTemplate">
If template
</div>
<ng-template #elseTemplate>
<div>Else template</div>
</ng-template>
- old vs new syntax
- usage with async pipe
@for
@for (movie of movies; track movie.id) {
} @empty {
<div> No movies in list </div>
}
// $count, $first, $last, $even, $odd.
@for (movie of movies; track movie.id) {
} @empty {
<div> No movies in list </div>
}
- old vs new syntax
- internal variables
*ngFor="let movie of movies; trackBy: trackByFn"
@switch
<div [ngSwitch]="streamingService">
<div *ngSwitchCase="'AppleTV'">Ted Lasso</div>
<div *ngSwitchCase="'Disney+'">Mandalorian</div>
<div *ngSwitchDefault>Peaky Blinders</div>
</div>
@switch(streamingService) {
@case ('Disney+') {
<div>'Mandalorian'</div>
} @case ('AppleTV') {
<div>'Ted Lasso'</div>
} @default {
<div>'Peaky Blinders'</div>
}
}
- old vs new syntax
Automatic migration
ng generate @angular/core:control-flow
? Which path in your project should be migrated?
? Should the migration reformat your templates?
Signals
Angular change detection
- Default change detection
event
Angular change detection
- Optimized change detection with OnPush
event
ngZone
- An execution context for asynchronous operations
- Zones are able to track the context of asynchronous actions by monkey-patching them
function addEventListener(eventName, callback) {
// call the real addEventListener
callRealAddEventListener(eventName, function() {
// first call the original callback
callback(...);
// and then run Angular-specific functionality
var changed = angular2.runChangeDetection();
if (changed) {
angular2.reRenderUIPart();
}
});
}
Signals
- signals reactive graph
Signals
let x = 5;
let y = 3;
let z = x + y;
console.log(z); // 8
x = 10;
console.log(z); // 8
- why? - more reactive code
const x = signal(5);
const y = signal(3);
const z = computed(() => x() + y());
console.log(z()); // 8
x.set(10);
console.log(z()); // 13
Signals
quantity = signal(1);
qtyAvailable = signal([1, 2, 3, 4, 5, 6]);
selectedVehicle = signal<Vehicle>({
id: 1,
name: 'AT-AT',
price: 19416.13
});
vehicles = signal<Vehicle[]>([]);
- create signals
quantity();
<div *ngFor="let q of qtyAvailable()">{{ q }}</div>
<div>Vehicle: {{ selectedVehicle().name }}</div>
<div>Price: {{ selectedVehicle().price }}</div>
- read signals
this.quantity.set(qty);
// Update value based on current value
this.quantity.update(qty => qty * 2);
- update signals
quantity.set(5);
quantity.set(42);
quantity.set(100); // final value
Signals
selectedVehicle = signal<Vehicle>({
id: 1,
name: 'AT-AT',
price: 19416.13
});
totalPrice = computed(() => this.selectedVehicle().price * this.quantity());
color = computed(() => this.totalPrice() > 50000 ? 'green' : 'blue');
- computed signals
Extended price: {{ totalPrice() }} // calculated
Total price: {{ totalPrice() }} // reused value
Amount due: {{ totalPrice() }} // reused value
Signals
constructor() {
effect(
() => console.log(this.selectedVehicle())
);
}
- effects
ngOnInit(): void {
// Effects are not allowed here.
effect(() => {
console.log('vehicle:', this.selectedVehicle());
});
}
- effects need an injection context - they use
inject
to get hold of the currentDestroyRef
injector = inject(Injector);
ngOnInit(): void {
runInInjectionContext(this.injector, () => {
effect(() => {
console.log('route:', this.flightRoute());
});
});
}
Signals
effect(() => {
// Writing into signals is not allowed here:
this.to.set(this.from());
});
- writing signals in effects
effect(() => {
this.to.set(this.from());
}, { allowSignalWrites: true })
- dependency tracking in effects
effect(() => {
if(false) {
this.to.set(this.from()); // won't trigger effect when changed
}
});
Signals
object = signal(
{
id: 1,
title: "Angular For Beginners",
},
{
equal: (a, b) => {
return a.id === b.id && a.title == b.title;
},
}
);
- signal equality check
title = computed(() => {
console.log(`Calling computed() function...`)
const course = this.object();
return course.title;
})
updateObject() {
this.object.set({
id: 1,
title: "Angular For Beginners"
});
}
Rx interop
counterObservable = interval(1000);
// Get a `Signal` representing the `counterObservable`'s value.
counter = toSignal(this.counterObservable, {initialValue: 0});
- observable -> signal
query: Signal<string> = inject(QueryService).query;
query$ = toObservable(this.query);
results$ = this.query$.pipe(
switchMap(query => this.http.get('/search?q=' + query ))
);
- signal -> observable
Signal inputs/outputs
// optional
firstName = input<string>(); // InputSignal<string|undefined>
age = input(0); // InputSignal<number>
// required
lastName = input.required<string>(); // InputSignal<string>
- input
changeMovie = output<string>()
changeMovie.emit(123)
- output
ngAdvanced
By Andrei Antal
ngAdvanced
- 1,068