What's new in Angular 14.2?

Angular 14.2.0 is here!

Angular logo

This is a minor release, but it is packed with interesting features: let’s dive in!

Typescript 4.8

TypeScript v4.8 has just been released, and Angular is already compatible \o/. Check out the Typescript v4.8 blog post to learn more about the new features.

NgOptimizedImage

The biggest new feature in Angular 14.2 is the new NgOptimizedImage directive. It is a standalone directive available as an experiment in the @angular/common package.

This directive helps you to optimize your images in your application. To enable it, add it to the imports of one of your modules or standalone components:

imports: [NgOptimizedImage]

Then to use it, you can simply replace the src of an image with rawSrc:

<img [rawSrc]="imageUrl" />

The directive then does its best to enforce best practices for this image. For example, did you know that it is recommended to set the width and height attributes on the img tag to prevent layout shifts? See this web.dev article for more information.

If you use the NgOptimizedImage directive, then you get an error if width and height are not properly set:

NG02954: The NgOptimizedImage directive (activated on an <img> element with the `rawSrc="/avatar.png"`) has detected that these required attributes are missing: "width", "height". Including "width" and "height" attributes will prevent image-related layout shifts. To fix this, include "width" and "height" attributes on the image tag.

This forces us to properly set the width and height attributes on the img tag. But note that you need to set a width/height ratio coherent with your image’s intrinsic size. For example, if the image is 800x600 pixels, then you need to set the width and height attributes to 800 and 600 respectively, or to values that respect the same ratio, like 400 and 300.

<img [rawSrc]="imageUrl" width="400" height="300" />

Otherwise, you get a warning letting you know that the image is distorted:

NG02952: The NgOptimizedImage directive (activated on an <img> element with the `rawSrc="/avatar.png"`) has detected that the aspect ratio of the image does not match the aspect ratio indicated by the width and height attributes. Intrinsic image size: 800w x 600h (aspect-ratio: 1.3333333333333333). Supplied width and height attributes: 300w x 300h (aspect-ratio: 1). To fix this, update the width and height attributes.

But the directive does more than just screaming at you 😉.

It automatically sets the fetchpriority attribute on the img tag. This attribute is used by modern browsers to determine how it should prioritize the fetching of the image (see the MDN docs). The directive will set the fetchpriority attribute to high if the image has the priority attribute (so the browser will fetch it right away), or to auto otherwise.

It also sets the loading attribute (see the MDN docs) to eager if the image has the priority attribute, or to lazy otherwise.

This means that by default, the browser will only load images when they’re about to be visible in the viewport.

It checks a few more things when running in dev mode (ng serve). If the image is treated by the browser as a Largest Contentful Paint (LCP) element (typically the case for above-the-fold images), then it checks that the image has the priority attribute. If that’s not the case you get a warning in the console (NG02955).

Last but not least, the directive comes with the concept of “loaders”. By default, the image is loaded from the src directory of your application, as usual. But you can specify another loader if you are using a service like Cloudflare Image Resizing, Cloudinary, ImageKit or Imgix. To do so, you can define one of the provided loaders in your providers:

providers: [
  provideCloudflareLoader("https://ninja-squad.com/"),
  // or `provideCloudinaryLoader`, `provideImageKitLoader`, `provideImgixLoader`
]

It is of course possible to create your own loader:

providers: [
  {
    provide: IMAGE_LOADER,
    useValue: (config: ImageLoaderConfig) => {
      return `https://example.com/${config.src}-${config.width}.jpg}`;
    }
  }
]

The directive also supports width or density descriptors, like 400w or 2x, with rawSrcset.

<img rawSrcset="avatar.png" rawSrcset="100w, 200w" />

This directive is probably only useful in some specific cases, but it enforces best practices that we don’t always know as web developers. It has been pushed by the Aurora team which is a collaboration between Chrome and open-source web frameworks. Give it a try (and keep in mind this is experimental). That’s why you can see similar work in other frameworks, like Nuxt Image for example.

The Aurora team wrote an in-depth article if you want to learn more.

Core

A new function createComponent() has been added to the framework to help create components dynamically. This is a replacement for the ComponentFactory that was usually used until it was deprecated in Angular v13.

const app = await bootstrapApplication(AppComponent);
const homeComponent = createComponent(HomeComponent, app.injector);
app.attachView(homeComponent.hostView);

Another new function called createApplication has been introduced to let developers start an application without bootstrapping a component (unlike bootstrapApplication). This can be useful if you want to render multiple root components in your application, or if you are using Angular Elements like in the following example:

const app = await createApplication();
const HomeNgElementCtor = createCustomElement(HomeComponent, { injector: app.injector });
customElements.define('app-home', HomeNgElementCtor);

A low-level utility function called reflectComponentType() has also been added to the framework to help get the component metadata from a component type.

const mirror = reflectComponentType(UserComponent)!;

mirror is a ComponentMirror object, which contains the metadata of the component:

  • selector, for example app-user
  • type, for example UserComponent
  • inputs, for example [{ propName: 'userModel', templateName: 'userModel' }]
  • outputs, for example [{ propName: 'userSaved', templateName: 'userSaved' }]
  • ngContentSelectors, for example ['*']
  • isStandalone, for example false

Similarly, there is now a provideRouterForTesting() function that can be used in tests instead of RouterTestingModule.

Forms

Angular v14 introduced a new form element called FormRecord. You can read more about it in our blog post about Strictly typed forms.

But there was no method to create a FormRecord with the FormBuilder. This is now fixed in Angular v14.2 (a small contribution from me 👉👈), and you can use fb.record({}):

this.form = this.fb.group({
  languages: this.fb.record({
    english: true,
    french: false
  })
});

Standalone components

Angular v14.1 introduced the common directives and pipes as standalone entities, v14.2 now introduces the router directives as standalone entities!

You can now import RouterLinkWithHref (for a routerLink), RouterLinkActive and RouterOutlet directly instead of importing the whole RouterModule:

@Component({
  standalone: true,
  templateUrl: './user.component.html',
  imports: [RouterLinkWithHref] // -> you can now use `routerLink` in the template
})
export class UserComponent {

Related to standalone components, the router is now usable without using RouterModule, thanks to the new provideRouter function.

So instead of using:

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(HttpClientModule),
    importProvidersFrom(RouterModule.forRoot(ROUTES, { preloadingStrategy: PreloadAllModules }))
  ]
});

you can now write:

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(HttpClientModule),
    provideRouter(ROUTES,
      withPreloading(PreloadAllModules)
    )
  ]
});

Other with... functions are available to enable router features:

  • withDebugTracing
  • withDisabledInitialNavigation
  • withEnabledBlockingInitialNavigation
  • withInMemoryScrolling
  • withRouterConfig

These changes allow tree-shaking parts of the router module that aren’t actually used, thus reducing the main bundle size.

Router

The router introduced the possibility of defining a page title on the route in Angular v14 (see our blog post). With this v14.2 release, it is now possible to retrieve the resolved title on the ActivatedRoute and ActivatedRouteSnapshot:

constructor(private route: ActivatedRoute) {
  this.title = route.snapshot.title;
}

It is now also possible to define guards and resolvers as simple functions. You can now write something like:

{
  path: '/user/:id/edit', 
  component: EditUserComponent,
  canDeactivate: [(component: EditUserComponent) => !component.hasUnsavedChanges]
}

The RouterLink directive received a tiny improvement that is noticeable: all its boolean inputs (preserveFragment, skipLocationChange and replaceUrl) now accept a string and coerce it to a boolean. This means you can now write:

<a [routerLink]="['/user', user.id, 'edit']" skipLocationChange='true'>Edit</a>
<!-- or even -->
<a [routerLink]="['/user', user.id, 'edit']" skipLocationChange>Edit</a>

instead of:

<a [routerLink]="['/user', user.id, 'edit']" [skipLocationChange]="true">Edit</a>

Angular CLI

The new CLI version does not have many features this time.

One notable addition is the ability for ng serve to serve service workers. It is enabled automatically if you have the option "serviceWorker": true in your builder configuration (which is the case by default when you add @angular/pwa to your application). This is handy as it allows us to use the usual ng serve to test the PWA behavior, whereas we previously had to build the application and serve it with another HTTP server to check it.

The work on the esbuild builder continues, and it is now faster to downlevel the JS code. In the esbuild builder as well, the Sass compilation now uses the “modern API” of Sass, which is faster than the legacy one. The classic webpack builder still uses the legacy Sass API, but should switch to the modern one soon as well.

That’s all for this release, stay tuned!

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


What's new in Angular 14.1?

Angular 14.1.0 is here!

Angular logo

This is a minor release, but it is packed with interesting features: let’s dive in!

Router new guard type: CanMatch

The router gained a new guard type in this release: CanMatch.

The existing CanActivate guard decides whether or not a navigation can go through. CanLoad guards decide if a module/component can be loaded. But there is no guard that allows matching a route depending on business logic: that’s what the CanMatch guard fixes.

It is now possible to define the same route several times, with different CanMatch guards, and to navigate to a specific one:

[
  { path: '', component: LoggedInHomeComponent, canMatch: [IsLoggedIn] },
  { path: '', component: HomeComponent }
]

Note that a CanMatch guard that returns false does not cancel the navigation: the route is skipped and the router simply continues matching other potential routes.

Here, navigating to / will render LoggedInHomeComponent if the user is logged in and will render HomeComponent otherwise. Note that the URL will remain / in both cases.

@Injectable({
  providedIn: 'root'
})
export class IsLoggedIn implements CanMatch {
  constructor(private userService: UserService) {}

  canMatch(route: Route, segments: Array<UrlSegment>): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
    return this.userService.isLoggedIn();
  }
}

A CanMatch guard can also redirect to another route like other guards do. To do so, you can return an UrlTree.

@Injectable({
  providedIn: 'root'
})
export class IsLoggedIn implements CanMatch {
  constructor(private userService: UserService, private router: Router) {}

  canMatch(route: Route, segments: Array<UrlSegment>): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
    return this.userService.isLoggedIn() || this.router.parseUrl('/');
  }
}

As the route is not even considered when the CanMatch guard returns false, it can be used to replace the CanLoad guard (and may even replace it in the future).

Router navigation events

The router now indicates why a navigation was canceled in a dedicated code field of the NavigationCancel event. Previously, you could use the reason field of the event to get the same information, but this was more a workaround than an intended feature. The code can now be used, and the reason field should only be used for debugging purposes. The code property can have the following values: NavigationCancellationCode.Redirect, NavigationCancellationCode.SupersededByNewNavigation, NavigationCancellationCode.NoDataFromResolver, or NavigationCancellationCode.GuardRejected.

The NavigationError also received a small improvement: the target of the navigation is now available in the event.

Standalone components

The built-in Angular directives and pipes offered by CommonModule (NgIf, NgFor, DatePipe, DecimalPipe, AsyncPipe, etc.) are now available as standalone!

You can now import them directly, without having to import CommonModule:

@Component({
  standalone: true,
  templateUrl: './user.component.html',
  imports: [NgIf, DecimalPipe] // -> you can now use `*ngIf` and `| number` in the template
})
export class UserComponent {

A new function called provideAnimations() is also available to add the animation providers to your application, instead of importing BrowserAnimationModule. Similarly, you can use provideNoopAnimations instead of importing the BrowserNoopAnimationsModule:

bootstrapApplication(AppComponent, {
  providers: [provideAnimations()]
});

inject function

The new inject function introduced in Angular v14.0 allows injecting dependencies (see our last blog post for more info), and a second parameter can be used to define the injection flag. In v14, the second parameter was a bit field: InjectFlags.Host, InjectFlags.Optional, InjectFlags.Self, or InjectFlags.SkipSelf. In v14.1, this signature has been deprecated in favor of a more ergonomic one with an object as the second parameter:

value = inject(TOKEN, { optional: false });

The cool thing is that it improved the type safety of the function. Previously, TypeScript had no idea of the flag signification, and the return type was always T | null, even if the injection was not optional. This is now working properly, and the above example has a return type T.

runInContext function

The inject function mentioned above only works in the constructor, or to initialize a field, or in a factory function.

So, how can you use it in a method/function that is not a constructor? You can use the EnvironmentInjector.runInContext function that has been introduced for this purpose in v14.1!

For example, this doesn’t work:

export class AppComponent implements OnInit {
  ngOnInit() {
    console.log('AppComponent initialized', inject(UserService));
  }
}

But this does, thanks to runInContext:

export class AppComponent {
  constructor(private injector: EnvironmentInjector) {}

  ngOnInit() {
    this.injector.runInContext(() => {
      console.log('AppComponent initialized', inject(UserService));
    });
  }
}

setInput

ComponentRef has a new method called setInput that can be called to set an input. Why is that interesting?

Currently when you are testing an OnPush component, it is not easy to test if the change of an input properly triggers what you want, because manually setting the input does not trigger the change detection.

This is now no longer a problem if you call setInput()! If your UserComponent component has an input called userModel, you can now write the following code in a test:

  const fixture = TestBed.createComponent(UserComponent);
  fixture.componentRef.setInput('userModel', newUser);

setInput() properly sets the input (even if it is aliased), calls the NgOnChanges lifecycle hook and triggers the change detection!

This feature is useful in tests, but also with any kind of dynamic component. It even opens the door for the router to set the inputs of a component dynamically based on route params (a feature that the Vue router has for example). Maybe we’ll see that in a future release!

ContentChild descendants

The ContentChild decorator now supports the descendants option, as ContentChildren does. The default behavior does not change, and if you don’t specify the option, then ContentChild looks for the query in the descendants. This behavior is the same as specifying @ContentChild({ descendants: true }). But you can now change it by specifying @ContentChild({ descendants: false }), in which case Angular will only do a “shallow” search and look for the direct descendants.

Extended template diagnostics

The team added a few more “extended diagnostics”.

The first one is missingControlFlowDirective, and it’s linked to the Standalone Components story.

With this check enabled, the compiler warns us when a ngIf, ngFor, or ngSwitch is used in the template of a standalone component, but the corresponding directive or the CommonModule is not imported:

Error: src/app/register/register.component.html:11:59 - error NG8103: 
The `*ngFor` directive was used in the template, 
but neither the `NgForOf` directive nor the `CommonModule` was imported.
Please make sure that either the `NgForOf` directive or the `CommonModule`
is included in the `@Component.imports` array of this component.

This is a nice addition, as it can be fairly easy to forget to import CommonModule or the directive itself as I pointed out in our guide to standalone components. The message even mentions the directive you need to import, which can be tricky to figure out for *ngFor.

The second extended diagnostics is textAttributeNotBinding. When enabled, the compiler warns us when a class, style, or attr binding does not have the [], or if the value is not interpolated. For example, a template with class.blue="true" yields the following:

Error: src/app/register/register.component.html:2:8 - error NG8104: 
Attribute, style, and class bindings should be  
enclosed with square braces. For example, '[class.blue]="true"'.

Slightly related, the third one is suffixNotSupported. When enabled, the compiler warns us when a suffix like px, % or em is used on attribute binding where it doesn’t work, unlike when used in a style binding:

Error: src/app/register/register.component.html:2:9 - error NG8106: 
The '.px', '.%', '.em' suffixes are only supported on style bindings.

The fourth one is missingNgForOfLet. when enabled, the compiler warns us when a *ngFor is used with the let keyword. For example, *ngFor="user of users" throws with:

Error: src/app/users/users.component.html:1:7 - error NG8105: 
Your ngFor is missing a value. Did you forget to add the `let` keyword?

The fifth and last one is optionalChainNotNullable, and it is slightly similar to the already existing nullishCoalescingNotNullable check. When enabled, the compiler warns us when an unnecessary optional check is used. For example, if user is not nullable, then using `` in a template yields:

Error: src/app/user/user.component.html:2:21 - error NG8107: 
The left side of this optional chain operation does not include 'null' or 'undefined' in its type, 
therefore the '?.' operator can be replaced with the '.' operator.

Zone.js

zone.js has also been released in version v0.11.7, and contains a new feature that improves the debugging of asynchronous tasks, by using an experimental feature of Chrome. You can leverage this new support by importing import 'zone.js/plugins/async-stack-tagging';. When this is enabled, you’ll have nicer stack traces in case of an error in an async task.

Angular CLI

As usual, you can check out our dedicated article about the new CLI version:

👉 Angular CLI v14.1

That’s all for this release, stay tuned!

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


What's new in Angular CLI 14.1?

Angular CLI 14.1.0 is out!✨

If you want to upgrade to 14.1.0 without pain (or to any other version, by the way), I have created a Github project to help: angular-cli-diff. Choose the version you’re currently using (12.2.0 for example), and the target version (14.1.0 for example), and it gives you a diff of all files created by the CLI: angular-cli-diff/compare/13.2.0…14.1.0. It can be a great help along with the official ng update @angular/core @angular/cli command. You have no excuse for staying behind anymore!

Let’s see what we’ve got in this release.

npm init / yarn create

It is now possible to create a new Angular CLI project by using:

npm init @angular
yarn create @angular

Using this avoids the need to install the Angular CLI package globally. The commands support the same options as ng new.

esbuild builder improvements

The new experimental builder that uses esbuild has been introduced and v14, but with some missing features.

Even if it is still not feature complete, the esbuild builder now supports service workers and Sass files.

It also allows declaring external dependencies with the new externalDependencies option. When dependencies are listed in this option, they are excluded from the generated bundle. Instead, the created bundle relies on these dependencies to be available during runtime.

So for example if you define externalDependencies: ['@angular/core'], then the @angular/core package will not be bundled, and you’ll need to include it on your page in another way.

This is useful if you want to run several Angular applications on the same page, and use import maps to load Angular itself just once (instead of loading it in every bundle).

You’ll find more interesting features in our article about the framework v14.1.0 release.

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


What's new in Angular 14?

Angular 14.0.0 is here!

Angular logo

This is a major release with really interesting features: let’s dive in!

Strictly typed forms

The most up-voted issue in the Angular repository is solved in Angular v14: we now have strictly typed forms!

As there is quite a bit to explain between the migration, the new API, and the addition of FormRecord and NonNullableFormBuilder, we wrote a dedicated blog post:

👉 Strictly typed forms

TL;DR: with some elbow grease, it’s now possible to have form values perfectly typed, and no longer of type any ✨.

Other forms improvements

It is now possible to use negative indices on FormArray methods, like the Array methods do in JavaScript. For example formArray.at(-1) is now allowed and returns the last control of the form array.

Standalone components (see ya NgModule!)

The other big feature of the release is the addition of the (experimental) standalone APIs. Same here: as there is a lot to cover, we wrote a dedicated blog post:

👉 A guide to standalone components

TL;DR: it’s now possible (but experimental) to get rid of NgModule in your applications, and use the new standalone components/directives and pipes ✨.

inject function

You can now use the inject() function from '@angular/core' (which already existed but has been improved) to inject a token programmatically.

For example you can use it in a component:

constructor() {
  const userService = inject(UserService);
  // ...
}

It can only be called in some specific areas:

  • in a constructor as above;
  • to initialize a class field;
  • in a factory function.

It opens the door to some interesting patterns, especially for library authors.

Router

The router received a lot of attention in this release.

Page title

It’s now possible to set a page title directly in a route declaration:

export const ROUTES: Routes = [
  { path: '', title: 'Ninja Squad | Home', component: HomeComponent },
  { path: 'trainings', title: 'Ninja Squad | Trainings', component: TrainingsComponent }
]

You can also define a resolver for the title:

export const ROUTES: Routes = [
  { path: 'trainings/:trainingId', title: TrainingTitleResolver, component: TrainingComponent }
]

@Injectable({
  providedIn: 'root'
})
export class TrainingTitleResolver implements Resolve<string> {
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): string {
    return `Ninja Squad | Training ${route.paramMap.get('trainingId')}`;
  }
}

This is not super flexible though, as you’ll probably often want to display something more meaningful like Ninja Squad | Angular training and not Ninja Squad | Training 13, and that data lives in the component: the resolver can’t access it. But it’s still a nice addition for adding static (or not too dynamic) titles.

It’s also possible to write a custom strategy to build the title by extending the built-in TitleStrategy. For example, if I want to prepend Ninja Squad | to all titles, I can do:

@Injectable()
export class CustomTitleStrategyService extends TitleStrategy {
  constructor(@Inject(DOCUMENT) private readonly document: Document) {
    super();
  }

  override updateTitle(state: RouterStateSnapshot) {
    const title = this.buildTitle(state);
    this.document.title = `Ninja Squad | ${title}`;
  }
}

Then, use this custom strategy instead of the default one:

@NgModule({
  //...
  providers: [{ provide: TitleStrategy, useClass: CustomTitleStrategyService }]
})
export class AppModule {}

And just define the specific part of the title on each route:

export const ROUTES: Routes = [
  { path: '', title: 'Home', component: HomeComponent },
  { path: 'trainings', title: 'Trainings', component: TrainingsComponent }
]

Types

Some types have been improved in the router. For example, all router events now have a type property, allowing to narrow their type like this:

// send hits to analytics API only on navigation end events
this.router.events
  .pipe(
    filter((event: Event): event is NavigationEnd => event.type === EventType.NavigationEnd),
    mergeMap(event => this.sendHit(event.url))
  )
  .subscribe();

pathMatch is now also more strictly typed and only accepts the two valid options 'full'|'prefix'. That’s why a migration will add the explicit Route or Routes types on your routes declaration if you don’t have them when running ng update. Otherwise, TypeScript will be unhappy with pathMatch: 'full' as it’ll think that 'full' is a string and not a const.

Route providers, standalone routes, loadComponent

A bunch of new things have been added to the router to support the new standalone APIs. For example, you can now define providers directly on a route, or lazy-load just a component. Check out our blog post about standalone components to learn more.

Accessibility

routerLinkActive gained a new input called ariaCurrentWhenActive, which allows to set aria-current a11y property. The possible values are 'page' | 'step' | 'location' | 'date' | 'time' | true | false. For example:

<a class="nav-link" routerLink="/" routerLinkActive="active" ariaCurrentWhenActive="page">Home</a>

TestBed

It’s now possible to configure the TestBed to throw errors on unknown elements or properties found in a template. Currently, Angular has two compilation modes: Just in Time (jit) and Ahead of Time (aot). When you run ng serve or ng build, the aot mode is used. But when running ng test, the jit compilation is used. Weirdly enough, an error in a template results in just a warning in the console when the template compilation fails on an unknown element or property in jit mode. That’s why you can sometimes see NG0303 and NG0304 warnings in the console when you run your tests, typically when you forgot to import or declare a component/directive necessary to test your component.

Starting with Angular v14, it is now possible to configure the TestBed to throw an error for these issues, and thus make sure we don’t miss them. I think this is amazing (but that’s probably because I implemented it 😬):

getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting(),
  { 
    errorOnUnknownElements: true, 
    errorOnUnknownProperties: true 
  }
);

The default for errorOnUnknownElements and errorOnUnknownProperties is false, but we’ll probably change it to true in a future release. You can also enable/disable them in a specific test with TestBed.configureTestingModule({ /*...*/, errorOnUnknownElements: false }).

In the distant future, the tests will maybe use the aot compilation, but that’s not for tomorrow. In the meantime, these new options should be helpful!

Compiler

As you probably know, you can’t use a private member of a component in a template, and you can only use public members. Starting with v14, you can now also use protected members of a component in the template.

Http

This is more a bugfix than a feature, but as it is a breaking change that may have an impact, let’s talk about it: + in query params are now properly encoded as %2B. They used to be ignored by the HttpClient that was otherwise properly encoding the other special characters in query parameters. You had to manually take care of the + signs, but this is no longer necessary: you can now delete the code that was manually encoding them after upgrading to v14.

Zone.js

Zone.js now supports Promise.any(), a new method introduced in ES2021.

Service worker

The versionUpdates observable now emits NoNewVersionDetectedEvent if the service worker did not find a newer version.

Devtools

The Angular Devtools are now available on Firefox as well 🎉.

Typescript and Node

Angular v14 drops the support of TypeScript v4.4 and 4.5 and now supports for v4.7 (recently released). It also drops the support of Node v12.

Angular CLI

As usual, you can check out our dedicated article about the new CLI version:

👉 Angular CLI v14

Summary

This release is packed with features as you can see, and the future is exciting with the standalone APIs. The roadmap now also mentions some efforts on the server-side rendering story, which is not the strong suite of Angular (compared to other mainstream frameworks).

That’s all for this release, stay tuned!

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


What's new in Angular CLI 14?

Angular CLI 14.0.0 is out!✨

If you want to upgrade to 14.0.0 without pain (or to any other version, by the way), I have created a Github project to help: angular-cli-diff. Choose the version you’re currently using (12.2.0 for example), and the target version (14.0.0 for example), and it gives you a diff of all files created by the CLI: angular-cli-diff/compare/13.2.0…14.0.0. It can be a great help along with the official ng update @angular/core @angular/cli command. You have no excuse for staying behind anymore!

Let’s see what we’ve got in this release.

Autocompletion

The first time you’re going to run an ng command in your terminal, you’ll see that Angular CLI will ask about setting up the autocompletion. If you accept, then pressing <TAB> after typing ng will list the available commands and options. However, this works only for Bash and Zsh shells on macOS and Linux operating systems.

If you don’t want to enable it and then change your mind, you can run ng completion to set it up manually.

esbuild builder

A new experimental builder that uses esbuild instead of Webpack has been introduced.

You can give it a try by replacing @angular-devkit/build-angular:browser with @angular-devkit/build-angular:browser-esbuild in your angular.json file. Note that the new builder is far from being complete. It does not support a bunch of options (and does not understand Sass for example).

These are very early days of course, but you can give it a try if you’re curious. It looks very promising, and we can hope for a faster builder in the future that would rely on esbuild. And, who knows, a CLI that uses Vite instead of Webpack (check out our article about Vite to learn more).

ng cache

A new ng cache command has been added to manage the cache system introduced in CLI v13 (see our article).

This new command has 4 subcommands:

  • ng cache enable to enable the cache.
  • ng cache disable to disable the cache.
  • ng cache clean to delete the cache from disk (useful when switching branches and Webpack gets lost).
  • ng cache info which will print statistics and information about the cache.

ng generate

ng generate has a new --standalone flag to generate standalone components/pipes/directives.

It is now also possible to specify multiple schematics collections with schematicCollections in your angular.json file (instead of the now deprecated defaultCollection).

"cli": {
  "schematicCollections": ["@schematics/angular", "@angular/material"]
}

The CLI will then look for schematics in the specified list, following the order of priority (the first schematic wins).

ng e2e, lint and deploy

The ng e2e, ng lint, and ng deploy don’t come with an implementation as you may know. All these commands now ask you what implementation you’d like to add when you run them for the first time. ng lint only offers ESLint, but ng e2e lets you pick between Cypress/Nightwatch/WebdriverIO, and ng deploy between Amazon S3/Azure/Firebase/Netlify/NPM/GitHub Pages.

Fun with flags

Some deprecated options have been removed and a schematic will automatically take care of migrating your project:

  • --all option from ng update has been removed without replacement
  • --prod option has been removed from all builders. --configuration production/-c production should be used instead.
  • showCircularDependencies option from ng build has been removed without replacement
  • defaultProject workspace option has been deprecated (the default is now the current working directory)

Note that the flag parser of the CLI changed and that camelCase arguments are no longer supported: for example, you need to write ng g c hello --skip-selector instead of ng g c hello --skipSelector.

You’ll find more interesting features in our article about the framework v14.0.0 release.

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


A guide to Standalone Components in Angular

Angular v14 introduces one major (experimental) feature, after months of discussion: the possibility to declare standalone components/pipes/directives, and to get rid of NgModule in your application if you want to 😍.

In this article, we’ll see:

  • how to declare standalone entities,
  • how to use them in existing applications
  • how to get rid of NgModule if you want to
  • how the router has changed to leverage this new feature
  • and more nerdy details!

Disclaimer: this blog post is based on early releases of Angular v14, and some details may change based on the feedback the Angular team gets. That’s why, for once, we write a blog post on a feature before its final release: this is a great opportunity to give it a try and gather feedback!

Standalone components

Components, directives, and pipes can now be declared as standalone.

@Component({
  selector: 'ns-image',
  standalone: true,
  templateUrl: './image.component.html'
})
export class ImageComponent {
}

When that’s the case, the component/directive/pipe can’t be declared in an NgModule. But it can be directly imported into another standalone component. For example, if my ImageComponent above is used in the template of a standalone UserComponent, you have to import ImageComponent in UserComponent:

@Component({
  selector: 'ns-user',
  standalone: true,
  imports: [ImageComponent],
  templateUrl: './user.component.html' 
  // uses `<ns-image>`
})
export class UserComponent {
}

This is true for every component/directive/pipe you use in a standalone component. So if the template of UserComponent also uses a standalone FromNowPipe and a standalone BorderDirective, then they have to be declared into the imports of the component:

@Component({
  selector: 'ns-user',
  standalone: true,
  imports: [ImageComponent, FromNowPipe, BorderDirective],
  templateUrl: './user.component.html' 
  // uses `<ns-image>`, `fromNow` and `nsBorder`
})
export class UserComponent {
}

This is also true for components, directives, and pipes offered by Angular itself. If you want to use ngIf in a template, the directive has to be declared. But ngIf is not a standalone directive: it is offered via the CommonModule. That’s why imports lets you import any NgModule used as well:

@Component({
  selector: 'ns-user',
  standalone: true,
  imports: [CommonModule, RouterModule, ImageComponent, FromNowPipe, BorderDirective],
  templateUrl: './user.component.html' 
  // uses `*ngIf`, `routerLink`, `<ns-image>`, `fromNow` and `nsBorder`
})
export class UserComponent {
}

You can of course import your own existing modules or modules offered by third-party libraries. If you use the DragDropModule from Angular Material for example:

@Component({
  selector: 'ns-user',
  standalone: true,
  imports: [CommonModule, RouterModule, DragDropModule, ImageComponent],
  templateUrl: './user.component.html' 
  // uses `*ngIf`, `routerLink`, `cdkDrag`, `<ns-image>`
})
export class UserComponent {
}

A standalone component can also define schemas if you want to ignore some custom elements in its template with CUSTOM_ELEMENTS_SCHEMA or even ignore all errors with NO_ERRORS_SCHEMA.

Usage in existing applications

This is all great, but how can we use our new standalone UserComponent in an existing application that has no standalone components?

Maybe you guessed it: you can import a standalone component like UserComponent in the imports of an NgModule!

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule, UserComponent], // <---
  bootstrap: [AppComponent]
})
export class AppModule {}

This is probably a sound strategy to start using standalone components, pipes, and directives in existing applications. Angular applications tend to have a SharedModule with commonly used components, directives, and pipes. You can take these and convert them to a standalone version. It’s usually straightforward, as they have few dependencies. And then, instead of importing the full SharedModule in every NgModule, you can import just what you need!

CLI support

The Angular CLI team added a new flag --standalone to ng generate in v14, allowing to create standalone versions of components, pipes, and directives:

ng g component --standalone user

The component skeleton then has the standalone: true option, and the imports are already populated with CommonModule (that will be used in pretty much all components anyway):

@Component({
  selector: 'pr-user',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {

The generated test is also slightly different. A standalone component is declared in the imports option of TestBed.configureTestingModule() instead of in the declarations option.

If you want to generate all components with the --standalone flag, you can set the option directly in angular.json:

"schematics": {
  "@schematics/angular:component": {
    "standalone": true
  }
}

You can of course do the same for the directive and pipe schematics.

Application bootstrap

If you want to, you can go one step further and write an application with only standalone entities, and get rid of all NgModules. In that case, we need to figure out a few details.

First, if we don’t have an Angular module, how can we start the application? A typical main.ts contains a call to platformBrowserDynamic().bootstrapModule(AppModule) which bootstraps the main Angular module of the application.

In a standalone world, we don’t want to use NgModule, so we don’t have an AppModule.

Angular now offers a new function called bootstrapApplication() in @angular/platform-browser. The function expects the root standalone component as a parameter:

bootstrapApplication(AppComponent);

This creates an application and starts it.

For SSR, you can use the new renderApplication function, which renders the application as a string:

const output: string = await renderApplication(AppComponent, { appId: 'app' });

Optional NgModules

NgModule is a weird concept in Angular if you think about it. They fulfill several roles at once. We use them to declare what is usable in the templates of the components, but also to configure the available providers. We can export entities, to make them available in other modules. Modules are eagerly executed, which means you can add code in their constructors if you want to run something on their initialization. They are also necessary if you want to lazy-load parts of your application.

If modules are now optional, how can we do all these tasks?

Providers

NgModules allow defining providers available for components in the module. For example, if you want to use HttpClient, you add HttpClientModule to the imports of your main module.

In an application with no module, you can achieve the same by using the second parameters of bootstrapApplication(), which allows declaring providers:

bootstrapApplication(AppComponent, { providers: [] });

In the long run, Angular will probably offer a function returning the HTTP providers. For now, to bridge the gap with modules that expose providers, we can use importProvidersFrom(module):

bootstrapApplication(AppComponent, { 
  providers: [importProvidersFrom(HttpClientModule)]
});

You can also use importProvidersFrom to configure the router:

bootstrapApplication(AppComponent, { 
  providers: [importProvidersFrom(RouterModule.forRoot([/*...*/]))]
});

Note that the BrowserModule providers are automatically included when starting an application with bootstrapApplication().

It’s also worth noting that you can’t use importProvidersFrom in component providers: it’s only usable in bootstrapApplication(). bootstrapApplication() is now responsible for the Dependency Injection work, and that’s where providers must be declared.

Lazy loading routes

The lazy-loading story in Angular has always revolved around NgModule. Let’s say you wanted to lazy-load an AdminComponent. You had to write an NgModule like the following:

@NgModule({
  declarations: [AdminComponent],
  imports: [
    CommonModule, 
    RouterModule.forChild([{ path: '', component: AdminComponent }])
  ],
})
export class AdminModule {}

and then load the module with the router function loadChildren:

{ 
  path: 'admin',
  loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}

You can now get rid of AdminModule if AdminComponent is standalone, and directly lazy-load the component with loadComponent:

{ 
  path: 'admin',
  loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)
}

This is a really nice addition! All the lazy-loaded components must be standalone of course. It’s worth noting that this feature exists in all other mainstream frameworks, and Angular was lacking a bit on this.

We can also lazy-load several routes at once, by directly loading the routes config with loadChildren:

{ 
  path: 'admin',
  loadChildren: () => import('./admin/admin.routes').then(c => c.adminRoutes)
}

We now have a nice symmetry between children/loadChildren and component/loadComponent!

But NgModules also allow to define providers for a lazy-loaded module: the providers are then only available in the components of the lazy-loaded module. To achieve the same thing, you can now declare providers directly on a route, and the providers will be available only for this route and its children:

{ 
  path: 'admin',
  providers: [AdminService],
  loadComponent: () => import('./admin/admin.component').then(c => c.AdminComponent)
}

This works with all types of routes (with component, loadComponent, children, loadChildren with routes or NgModule). In my example above, the component is lazy-loaded, but the service is not. If you want to lazy-load the service as well, you can use:

{ 
  path: 'admin',
  loadChildren: () => import('./admin/admin.routes').then(c => c.adminRoutes)
}

and define the providers in adminRoutes:

export const adminRoutes: Routes = [
  { 
    path: '',
    pathMatch: 'prefix',
    providers: [AdminService], // <--
    children: [
      { path: '', component: AdminComponent }
    ]
  }
];

Initialization

An NgModule can also be used to run some initialization logic, as they are eagerly executed:

@NgModule({ /*...*/ })
export class AppModule {
  constructor(currentUserService: CurrentUserService) {
    currentUserService.init();
  }
}

To achieve the same without a module, we can now use a new multi-token ENVIRONMENT_INITIALIZER. All the code registered with this token will be executed during the application initialization.

bootstrapApplication(AppComponent, {
  providers: [
    {
      provide: ENVIRONMENT_INITIALIZER,
      multi: true,
      useValue: () => inject(CurrentUserService).init()
    }
  ]
});

Note that importProvidersFrom(SomeModule) is smart enough to automatically register the initialization logic of SomeModule in ENVIRONMENT_INITIALIZER.

Angular compiler and Vite

On a low level, NgModules are the smallest unit that the compiler can re-compile when running ng serve. Indeed, if you update the selector of a component for example, then the Angular compiler has to check all the templates of the module that contains this component to see if something changed, and also all the modules that import that module. Right now, the Angular compiler is tightly coupled with the TypeScript compiler and does a lot of bookkeeping to only recompile what’s necessary. In an application with no NgModules, the compiler has a more straightforward task: it will for example only recompile the components that directly import the modified component.

This can be good news for the future of Angular tooling. The frontend world has been taken by storm by Vite. We talked about Vite, and the differences with Webpack, in this blog post.

TL;DR: Vite only re-compiles the files needed to display a page and skips the TypeScript compilation to only do a simple transpilation, often in parallel.

This works great for Vue, React, or Svelte, but not so great for Angular, where a lot more needs to be recompiled, and where TypeScript is needed. Standalone components are a nice step in this direction, and may allow a future Angular CLI with Vite instead of Webpack and way faster re-builds.

Caveats

To be honest, the standalone API feels great. We migrated a few applications, and this is really nice to use, and it feels good to get rid of NgModules!

A few pain points though.

  1. There are no “global imports”: you need to import a component/pipe/directive every time you use it. ngIf, ngFor, and friends are available in every standalone component generated by the CLI, as the skeleton includes the import of CommonModule. But routerLink for example is not: you need to import RouterModule if you need it. Other frameworks, like Vue for example, allow registering some components globally, to avoid importing them over and over. That’s not the case in Angular.

  2. Sometimes you forget to add an import, and your template doesn’t work, with no compilation error. For example, adding a link with [routerLink]="['/'] does not compile, but routerLink="/" does compile (and doesn’t work). I feel that these kinds of errors happen more often than they did with NgModule. IDEs will probably help us here, and I suppose typing routerLink in a template will result in an automatic addition of RouterModule in the component’s imports in VS Code/Webstorm/whatever is a few months.

  3. You can’t bootstrap multiple components at once with the new bootstrapApplication() function, whereas it was possible with the NgModule-based bootstrap.

  4. TestBed works with standalone components, but will probably include more specific APIs to simplify tests in the future. Note that it is already easier to test standalone components than classic components, as you don’t have to repeat in configureTestingModule all the dependencies the component needs.

Summary

Six years after the initial release, we can finally get rid of NgModule in Angular if we want to. Their addition to Angular was a bit rushed: they were introduced in Angular v2.0.0-rc.5 two months (!!) before the stable release, mainly to help the ecosystem build libraries. As often in our field, the rushed design resulted in an entity that mixed several concerns, with some concepts quite hard to understand for beginners.

The new “mental model” is easy to grasp: providers are declared on an application level and components just have to import what they need in their templates. It will also probably be easier for newcomers to understand how Angular works.

These standalone APIs are trying to make things clearer, and it looks like they did ♥️.

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


Strictly typed forms in Angular

We finally have them! 6 years after the first release, and after months of discussion and feedback, the most up-voted issue in the Angular repository is now solved in Angular v14.

We now have forms correctly typed in Angular 🚀.

FormControl now takes a generic type indicating the type of the value it holds. To make sure that nothing breaks in existing applications, the Angular team released an automatic migration in Angular v14.

Disclaimer: this blog post is based on early releases of Angular v14, and some details may change based on the feedback the Angular team gets. That’s why, for once, we write a blog post on a feature before its final release: this is a great opportunity to give it a try and gather feedback!

Migration to the untyped version

When updating to Angular v14, a migration will automatically replace all the form entities in your application by their untyped versions:

  • FormControlUntypedFormControl (which is an alias for FormControl<any>)
  • FormGroupUntypedFormGroup (which is an alias for FormGroup<any>)
  • FormArrayUntypedFormArray (which is an alias for FormArray<any>)
  • FormBuilderUntypedFormBuilder (which is an alias for FormBuilder<any>)

This migration will run when launching:

ng update @angular/core

Or on demand, if you already manually updated your application:

ng update @angular/core --migrate-only=migration-v14-typed-forms

At the end of the migration, all imports and instances are replaced by their untyped versions. And that can be a lot of files in large applications (we had a few hundreds of files updated in some of our applications).

The cool thing is that now the application should work exactly as before.

Migration to the typed forms API, step by step

The next step is to use the typed version of the API. How do you do that?

Let’s take an example, a simple register form, and go through it step by step.

export class RegisterComponent {
  registerForm: FormGroup;

  constructor() {
    this.registerForm = new FormGroup({
      login: new FormControl(null, Validators.required),
      passwordGroup: new FormGroup({
        password: new FormControl('', Validators.required),
        confirm: new FormControl('', Validators.required)
      }),
      rememberMe: new FormControl(false, Validators.required)
    });
  }
}

We have a login field, a subgroup, with a password field and a password confirmation field, and a “remember me” field.

When using the automated migration, you end up with:

export class RegisterComponent {
  registerForm: UntypedFormGroup;

  constructor() {
    this.registerForm = new UntypedFormGroup({
      login: new UntypedFormControl(null, Validators.required),
      passwordGroup: new UntypedFormGroup({
        password: new UntypedFormControl('', Validators.required),
        confirm: new UntypedFormControl('', Validators.required)
      }),
      rememberMe: new UntypedFormControl(false, Validators.required)
    });
  }
}

Our work is to remove all the Untyped* usage, and properly type the form. Let’s start with the code in the constructor as this is the most straightforward.

Each UntypedFormControl must be converted to FormControl<T>, with T the type of the value of the form control. Most of the time, TypeScript can infer this information based on the initial value given to the FormControl.

For example, passwordGroup can be converted easily:

passwordGroup: new FormGroup({
  password: new FormControl('', Validators.required), // inferred as `FormControl<string | null>`
  confirm: new FormControl('', Validators.required) // inferred as `FormControl<string | null>`
}),

Note that the inferred type is string | null and not string. This is because calling .reset() on a control without specifying a reset value, resets the value to null. This behavior is here since the beginning of Angular, so the inferred type reflects it. We’ll come back to this possibly null value, in a dedicated section, as it can be annoying (but can be worked around).

Sometimes though, TypeScript can’t infer the type of the control based on the initial value. For example, our login field is initialized with null, so TypeScript can’t know what type is intended here. You can of course explicitly add it:

login: new FormControl<string | null>(null, Validators.required),

Due to a subtle TypeScript bug, you also have to help TS figure out that false is a boolean:

rememberMe: new FormControl<boolean | null>(false, Validators.required)

This will probably be fixed in the future, and the type inference will hopefully be enough.

Now let’s take the field registerForm. Unlike FormControl, the generic type expected by FormGroup is not the type of its value, but a description of its structure, in terms of form controls:

registerForm: FormGroup<{
  login: FormControl<string | null>;
  passwordGroup: FormGroup<{
    password: FormControl<string | null>;
    confirm: FormControl<string | null>;
  }>;
  rememberMe: FormControl<boolean | null>;
}>;

constructor() {
  this.registerForm = new FormGroup({
    login: new FormControl<string | null>(null, Validators.required),
    passwordGroup: new FormGroup({
      password: new FormControl('', Validators.required),
      confirm: new FormControl('', Validators.required)
    }),
    rememberMe: new FormControl<boolean | null>(false, Validators.required)
  });
}

This is a bit verbose, but it works \o/. It is possible to let TypeScript infer the type of registerForm if, instead of initializing the field in the constructor like we usually do, we initialize it directly when we declare it:

registerForm = new FormGroup({
  login: new FormControl<string | null>(null, Validators.required),
  passwordGroup: new FormGroup({
    password: new FormControl('', Validators.required),
    confirm: new FormControl('', Validators.required)
  }),
  rememberMe: new FormControl<boolean | null>(false, Validators.required)
});

In this example, TypeScript properly infers the type of the form group, without a lot of work on our part.

This is also possible if you use the FormBuilder.

registerForm = this.fb.group({
  login: [null as string | null, Validators.required],
  passwordGroup: {
    password: ['', Validators.required],
    confirm: ['', Validators.required]
  },
  rememberMe: [false, Validators.required]
});

constructor(private fb: FormBuilder) {}

Nullability

As explained above, the types of the controls are string | null and boolean | null, and not string and boolean like we could expect, because calling .reset() on a field resets its value to null. Except if you give a value to reset, for example .reset(''), but as TypeScript doesn’t know if and how you are going to call .reset(), the inferred type is nullable.

You can tweak this behavior if you use the option nonNullable (which replaces the new option introduced in Angular v13.2 initialValueIsDefault, see our blog post for more details). With this option, you get rid of the null value if you want to! On one hand, this is very handy if your application uses strictNullChecks. But on the other hand, this is quite verbose, as you currently have to set this option on every field (this might change in the future):

registerForm = new FormGroup({
  login: new FormControl<string>('', { validators: Validators.required, nonNullable: true }),
  passwordGroup: new FormGroup({
    password: new FormControl('', { validators: Validators.required, nonNullable: true }),
    confirm: new FormControl('', { validators: Validators.required, nonNullable: true })
  }),
  rememberMe: new FormControl<boolean>(false, { validators: Validators.required, nonNullable: true })
}); // incredibly verbose version, that yields non-nullable types

Or you can use NonNullableFormBuilder.

NonNullableFormBuilder

Angular v14 introduces a new property on FormBuilder, called nonNullable, that returns a NonNullableFormBuilder. This new builder offers the usual control, group and array methods to build non-nullable controls:

registerForm = this.fb.nonNullable.group({
  login: ['', Validators.required]
});
// `registerForm.value` type is `{ login?: string }`

constructor(private fb: FormBuilder) {}

As using fb.nonNullable every time is a bit verbose, you can directly inject NonNullableFormBuilder instead of FormBuilder:

registerForm = this.fb.group({
  login: ['', Validators.required]
});

constructor(private fb: NonNullableFormBuilder) {}

What do we gain?

value and valueChanges

Is this migration trouble worth it? In my opinion, definitely. The original forms API is not playing very well with TypeScript. For example, the value of a control or group is typed as any. So we could write this.registerForm.value.whatever and the application would happily compile. This can be a very painful issue when refactoring an application: TypeScript would warn you about every mistake in TS and HTML files… except in forms!

This is no longer the case: the new forms API properly types value according to the types of the form controls. In my example above (with nonNullable), the type of this.registerForm.value is:

{
  login?: string;
  passwordGroup?: {
    password?: string;
    confirm?: string;
  };
  rememberMe?: boolean;
} // this.registerForm.value

You can spot some ? in the type of the form value. What does it mean?

In Angular, you can disable any part of a form. When you disable a field, its value is removed from the form value:

this.registerForm.get('passwordGroup').disable();
console.log(this.registerForm.value); // logs '{ login: null, rememberMe: false }'

This is a bit strange, but it explains why the fields are all marked as optional: if they have been disabled, they are not present in the object returned by this.registerForm.value. This is what TypeScript calls a Partial value.

If you want the complete object, with all its keys, even the disabled ones, you can use this.registerForm.getRawValue():

{
  login: string;
  passwordGroup: {
    password: string;
    confirm: string;
  };
  rememberMe: boolean;
} // this.registerForm.getRawValue()

this.registerForm.value is probably more accurate, but it forces developers to add potentially undefined when you know the value is present because the field is never disabled. For example, imagine that this value is used as parameters for calling the method of a service:

export class UserService {
  register(login: string, password: string): Observable<void> {
    // ...
  }
}

then when calling this method in our component above, we have an error:

const value = this.registerForm.value;
this.userService.register(value.login, value.passwordGroup.password).subscribe();    
// does not compile as the `login` and `password` parameters must be strings
// and `value.login`, `value.passwordGroup`, `value.passwordGroup.password`
// can all theoretically be undefined  

As the values can be undefined, and the register method expects strings, and not potentially undefined values, TypeScript is not happy.

We can handle this case by checking if the values exist (which also lets TypeScript know that they are not undefined):

const value = this.registerForm.value;
if (value.login && value.passwordGroup && value.passwordGroup.password) {
  // TypeScript narrows the types to `string` inside the `if` block
  this.userService.register(value.login, value.passwordGroup.password).subscribe();
}

But this is sometimes a bit annoying, as we know these values are present: we never disabled these fields!

In that case, you can use the lazy, but always efficient, “non-null assertion” operator !:

const value = this.registerForm.value;
this.userService.register(value.login!, value.passwordGroup!.password!).subscribe();
// not pretty, but gets the job done

valueChanges is of course properly typed as well: instead of getting an Observable<any> as we used to, you now get Observable<string | null> for this.registerForm.get('login').

setValue and patchValue are also type-safe: you can’t set a number on a FormControl<string> for example.

get()

The get(key) method is also more strictly typed. This is great news, as you could previously call it with a key that did not exist, and the compiler would not see the issue.

Thanks to some hardcore TypeScript magic, the key is now checked and the returned control is properly typed!

this.registerForm.get('login') // AbstractControl<string> | null
this.registerForm.get('passwordGroup.password') // AbstractControl<string> | null 😲

It also works with the array syntax for the key, if you add as const:

this.registerForm.get(['passwordGroup', '.password'] as const) // AbstractControl<string> | null

And it even works with nested form arrays and groups! For example, if our form has a hobbies FormArray, containing a FormGroup:

this.registerForm.get('hobbies.0.name') // AbstractControl<string> | null 🤯

If you use a key that does not exist in your form, you get an error:

this.registerForm.get('logon' /* typo */)!.setValue('cedric'); // does not compile 🚀

As you can see, get() returns a potentially null value: this is because you have no guarantee that the control exists at runtime, so you have to check its existence or use ! like above.

Note that the keys you use in your templates for formControlName, formGroupName, and formArrayName aren’t checked, so you can still have undetected issues in your templates.

A newcomer: FormRecord

FormRecord is a new form entity that has been added to the API. A FormRecord is similar to a FormGroup but the controls must all be of the same type. This can help if you use a FormGroup as a map, to which you add and remove controls dynamically. In that case, properly typing the FormGroup is not really easy, and that’s where FormRecord can help.

It can be handy when you want to represent a list of checkboxes for example, where your user can add or remove options. For example, our users can add and remove the language they understand (or don’t understand) when they register:

languages: new FormRecord({
  english: new FormControl(true, { nonNullable: true }),
  french: new FormControl(false, { nonNullable: true })
});

// later 
this.registerForm.get('languages').addControl('spanish', new FormControl(false, { nonNullable: true }));

If you try to add a control of a different type, TS throws a compilation error:

this.registerForm.get('languages').addControl('spanish', new FormControl(0, { nonNullable: true })); // does not compile

But as the keys can be any string, there is no type-checking on the key in removeControl(key) or setControl(key). Whereas if you use a FormGroup, with well-defined keys, you do have type checking on these methods: setControl only allows a known key, and removeControl only allows a key marked as optional (with a ? in its type definition).

TL;DR: If you have a FormGroup on which you want to add and remove control dynamically, you’re probably looking for the new FormRecord type.

Conclusion

We’re very excited to see this new forms API landing in Angular! This is, by far, one of the biggest changes in recent years for developers. Ivy was big but didn’t need us to make a lot of changes in our applications. Typed forms are another story: the migration is likely to impact dozens, hundreds, or thousands of files in your applications! In our applications, for the forms we migrated to the typed version, most of the work was very straightforward and repetitive. And we even caught some hidden bugs in our code! The TypeScript support in Angular has always been outstanding, but had a major blind spot with forms: this is no longer the case!

Big thanks to Dylan Hunn who has been in charge of this work, and who very patiently listened to our very early feedback!

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


What's new in Angular 13.3?

Angular 13.3.0 is here!

Angular logo

This is a minor release, mainly to support TS v4.6. But it also comes with a few improvements that are worth exploring: let’s dive in!

TypeScript 4.6

The reason of this v13.3 release is the support of TypeScript v4.6. TS v4.6 was released in February, and the Angular team did not wanted to wait for the v14 release to support it (scheduled in May).

If you want to use the new es2022 target, the improved control flow analysis or the possibility to call code before super() in constructors, you now can. Here are the full release notes if you want to learn more about the new TypeScript version.

Bundle size

The team refactored the codebase to make some errors, providers and tokens tree-shakable and removed a few useless classes (now that IE is no longer supported). This should lead to slightly lighter bundles.

The code generated by the compiler has also been improved to allow chaining some instructions. For example, consider a template like <div><div><a>Hello</a></div></div>.

Angular v13.2 generated:

// simplified compiled template in ng v13.2
elementStart(0, "div");
elementStart(1, "div");
element(2, "a");
elementEnd();
elementEnd();

Now, Angular v13.3 generates:

// simplified compiled template in ng v13.3
elementStart(0, "div")(1, "div")
element(2, "a");
elementEnd()();

This can make a little difference depending on your templates, but not that much, as the code that was previously generated was compressing very well.

Forms

It is listed as a fix rather than a new feature, but I think this is noteworthy if you’re using ngModel in forms: you can use a non-null assertion in the “banana-in-box” syntax:

<input [(ngModel)]="user.name!" />
<!-- same as -->
<input [ngModel]="user.name!" (ngModelChange)="user.name = $event">

This of course works for the “banana-in-box” syntax in general, not only with ngModel.

That’s all for this release, stay tuned!

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!


Taste our new speculoos

ngx-speculoos logo

In 2018, we released the first version of ngx-speculoos, a small library to help writing Angular unit tests.

Since then, the library hasn’t evolved much, but we keep using it on all of our Angular projects and are quite happy with it.

We’re now even happier, because we just released a new version, 7.1.0, with a batch of new features. In particular, stubbing the ActivatedRoute is now done in a much better way than it was before, and creating Jasmine mocks is much less verbose thanks to this new version.

We also improved queries to make it easier to query elements, but also components or custom test elements, by CSS and by type.

And last but not least, we completely revamped the documentation to make it much more complete, structured and readable. Reading it should not take you more than a few minutes, and should give you an pretty good idea of how ngx-speculoos tastes.

We see ngx-speculoos as a little unknown gem. If you haven’t already, you should give it a try.

And while you’re at it, our other Angular library, ngx-valdemort is another gem that we use everywhere to help with form validation. Give it a try, too.


Getting started with Vite and Vue 3

Disclaimer This blog post is a chapter of our ebook Become a Ninja with Vue. Enjoy!

You can find the french version of this article here

Comparing to other frameworks, a Vue application is super easy to start: just pure JavaScript and HTML, no tooling, and components are simple objects (as we shown in a previous blog post).

Even someone that doesn’t know Vue can understand what’s going on. And this is one of the strengths of the framework: it’s easy to start, easy to grasp, and you can progressively learn the features.

We could stick to this minimal setup for our projects, but, let’s face it, it will not scale for long. We will soon have too many components to fit in one file, we would really love to use TypeScript instead of JavaScript, to add tests, to add some kind of code analysis, etc.

We could set up all the needed tools by hand, but instead let’s leverage the work of the community and use the Vue CLI (that has been the standard for many years), or the now recommended tool Vite.

Vue CLI

Note The CLI is now in maintenance mode, and the recommended tool is Vite, that we present below. As a lot of existing projects use the CLI, we still think it’s worth introducing, and it can help to grasp the differences with Vite.

The Vue CLI (Command Line Interface) was born to help developers build Vue applications. It can scaffold an application and build it, and offers a large ecosystem of plugins. Each plugin offers some kind of features, like unit testing or linting or TypeScript support. It also offers a graphical user interface!

One of the cool features of the CLI is the ability to develop each component in a dedicated file, with a .vue extension. In this file you can define everything related to this component: its JavaScript/TypeScript definition, its HTML template, and even its CSS styles. This is called a Single File Component, or SFC.

The CLI is overall super handy to avoid having to learn and configure all the underlying tools (Node.js, NPM, Webpack, TypeScript, etc…). It is still very flexible, and you can configure most behaviors.

But the CLI is now in maintenance mode, and Vite is the recommended alternative. Let’s talk about the underlying reasons.

Bundlers: Webpack, Rollup, esbuild

When writing modern JavaScript/TypeScript applications, you often need a tool that can bundle all the assets (code, styles, images, fonts).

For a long time, Webpack was the undisputed favorite. Webpack comes with a simple but super handy feature: it understands all the JavaScript module types that exist (modern ECMAScript modules, but also AMD or CommonJS modules, formats that were existing before the standard). This understanding makes it easy to use pretty much any library you can find on the Internet (most often on NPM): you just install it, import it in one of your files, and Webpack takes care of the rest. Even if you use libraries with completely different formats, Webpack happily converts them and packages all your code and the code of the libraries together into one giant JS file: a bundle. This is a super important task, because even if the standard defined ES Modules back in 2015, most browsers have been supporting them very recently!

The other task of Webpack is to help during development, by providing a dev server and watching your project (it can even do HMR, a fancy word that stands for Hot Module Reloading). When something changes, Webpack reads the entrypoint of our application (main.ts for example), then it reads its imports and loads these files, then it reads the imports of the imported files and loads them… You get the idea! When everything is loaded, it re-bundles everything into one large file, both your code and the imported libraries from your node_modules, changing the module format if needed. The browser then reloads to display our changes 😅. This can be time-consuming when working on large projects with hundreds or thousands of files, even if Webpack comes with caches and heuristics to be as fast as possible.

Vue CLI (like a lot of tools out there) is using Webpack for most of its work, both when building the application with npm run build, or when running the dev server with npm run serve.

This is great as the Webpack ecosystem is incredibly rich in plugins and loaders: you can do pretty much what you want with it. On the other hand, a Webpack configuration can quickly get a bit overwhelming with all these options.

If I talk about Webpack and what bundlers do, it’s because we have serious alternatives nowadays, and it can be hard to understand what they do, and what are their differences. To be honest, I’m not sure that I understand all the details myself, and I’ve contributed quite a lot to Vue and Angular CLIs, both heavily based on Webpack! But let me try to explain anyway.

A serious contender is Rollup. Rollup intends to keep things simpler than Webpack, by not doing so much out of the box, but often doing it faster than Webpack. Its author is Rich Harris, who is also the author of the Svelte framework. Rich wrote a famous article called “Webpack and Rollup: the same but different”. His guideline is “Use Webpack for apps, and Rollup for libraries”. In fact, Rollup can do a lot of what Webpack does for production builds, but it does not come with a dev server that can watch your files during development.

Another incredible alternative is [esbuild][https://esbuild.github.io/]. Unlike Webpack and Rollup, esbuild itself is not written in JavaScript. It is written in Go and compiled to native code. It has also been designed with parallelism in mind. That makes it way faster than Webpack and Rollup. Like 10x-100x faster 🤯.

So why don’t we use esbuild instead of Webpack? That’s exactly what Evan You, the author of Vue, thought when developing Vue 3. He had another brilliant idea. In 2018, Firefox shipped the support of native ECMAScript Modules (often called native ESM). In 2019, it was NodeJS, and then most browsers followed. Nowadays, your personal browser can probably understand native ESM without issues. Evan imagined a tool that would serve files as native ESM to the browser, doing the heavy lifting with esbuild to transform source files into ESM files if needed (for example for TypeScript or Vue files or legacy module formats).

Vite (the French word for “fast”) was born.

Vite

The idea behind Vite is that, as modern browsers support ES Modules, we can now use them directly, at least during development, instead of generating a bundle.

So when you load a page in the browser when developing with Vite, you don’t load a single large file of JS containing all the application: you load just the few ES modules needed for this page, each in their own file (and each over their own HTTP request). If an ES module has imports, then the browser loads these imports as well.

So Vite is mainly a dev server, in charge of answering the browser requests, and responding with the requested ES modules. As we may have written our code in TypeScript, or using SFC in .vue extension (see below), Vite sometimes needs to transform the files on our disk into a proper ES module that the browser can understand. This is where esbuild comes into play! Vite is built on top of esbuild, and when a requested file needs to be transformed, it asks esbuild to do the job and then sends the result to the browser. If you change something in a file, then Vite only sends the updated module to the browser, instead of having to rebuild the whole bundle as Webpack-based tools do!

Vite also uses esbuild to optimize a few things. For example if you use a library with a ton of files, it “pre-bundles” it into a single file using esbuild and serves it to the browser in one request instead of a few dozens/hundreds. This pre-bundling is done once when starting the server, so you don’t pay the cost every time you refresh.

The fun thing is that Vite is not tied to Vue: it can be used with Svelte, React and others. In fact some other frameworks now recommend to use Vite! Svelte, from Rich Harris, was one of the first to do so, and now officially recommends it.

esbuild is really good for the JS bundling part, but it is not (yet) capable of splitting the application in several bundles, or properly handling CSS (whereas Webpack and Rollup do it out of the box). So it is not suited for bundling the application for production. That’s where Rollup comes into play: Vite relies on esbuild during development, but uses Rollup to bundle for production. Maybe in the future it’ll use esbuild for everything.

Vite is more than just an esbuild wrapper. As we saw, esbuild transforms files really fast. But Vite does not ask esbuild to transpile the requested files on every reload: it leverages the browser cache to do as little as possible. So if you load a page that you already loaded, it will be displayed in an instant. Vite also comes with a ton of other features, and a rich plugin ecosystem.

An important note: esbuild transpiles TypeScript to JavaScript, but it does not compile it: it completely ignores the type-checking part! That makes it super fast, but it also means that you have no typechecking from Vite during development. To check that your application properly compiles, you have to run Volar (vue-tsc), usually when building the application.

Are you excited? Because I am! Vite comes with project templates for React, Svelte and Vue, but the Vue team started a small project on top of Vite called create-vue. And that project is now the official recommendation when you start new Vue 3 projects.

create-vue

create-vue is built on top of Vite, and provides templates for Vue 3 projects.

To get started, you simply use:

npm init vue@3

The npm init something command in fact downloads and executes the create-something package. So here npm init vue executes the create-vue package.

You then have to choose:

  • a project name
  • if you want TypeScript or not
  • if you want JSX or not
  • if you want Vue router or not
  • if you want Pinia for state management or not
  • if you want Vitest for unit testing or not
  • if you want Cypress for e2e testing or not
  • if you want ESLint/Prettier for linting and formatting or not

and your project is ready!

We will of course deep-dive into all these technologies along our ebook.

Want to give it a try?

To build your first Vite app, follow our online exercise Getting Started It’s part of our Pro Pack, but is accessible for free. It’ll guide you to create your first application, and provides a few tweaks on the default configuration that we think are very useful.

Done?

If you followed the instructions (and reached a 100% score I hope!), you have an application up and running.

As you saw in the exercise, the created application can run the unit tests, end-to-end tests, linter… And Vite is super fast, so the developer experience is really enjoyable 🚀.

Our favorite setup includes:

  • TypeScript for the type safety it brings, both in your code and in your templates with vue-tsc
  • Vitest for the unit tests. Vitest is really similar to Jest but uses Vite to load the files to test, making it way simpler to use than Jest, as we don’t need to configure ts-jest, vue-jest, etc.
  • Cypress for the e2e tests
  • ESLint with Prettier for the code analysis and formatting

With this setup, you’re ready to get started with Vue 3!

Our ebook, online training and training are explaining all this in great details if you want to learn more!


Posts plus anciens