Advanced Angular

workshop

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

The repo

bit.ly/3NDrERG

 
 

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 current DestroyRef
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
Made with Slides.com