DEV Community

Ng-News 26/21: Angular 22.2: Router Resources, Error Boundaries, and Private Template Members

Angular 22.2 introduces router resources and error boundaries, both in developer preview. We can also access TypeScript private members from a component's template.

The demo application used in this article is available at

Router Resources

Fetching data via the router is a feature which other frameworks have already had for many years. Angular already had resolvers, but it is nice that it now also supports this in the "Angular way", i.e. via the resource feature.

Modern Angular uses resources to load data asynchronously from the backend. Since creating a resource or using functions on top of it, like rxResource, starts loading immediately, we usually create the resource in a component, or the component gets it indirectly via an Angular service.

That also means that we need to provide the code to handle the state of a resource. For example, is it loading, has it been initialized, etc.?

The following example shows the component HolidaysPage, which gets the resource from a HolidayService:

holidays-page.ts

import { Component, inject } from '@angular/core';
import { HolidayCard } from './holiday-card';
import { HolidayService } from './holiday.service';
import { Title } from './title';

@Component({
  selector: 'app-holidays',
  imports: [HolidayCard, Title],
  template: `
    <app-title />
    @if (holidays.isLoading()) {
      <p>Loading holidays...</p>
    } @else if (holidays.error()) {
      <p>Could not load the holidays.</p>
    } @else if (holidays.hasValue()) {
      @for (holiday of holidays.value(); track holiday.id) {
        <app-holiday-card [holiday]="holiday" />
      }
    }
  `,
})
export class HolidaysPage {
  readonly holidays = inject(HolidayService).createHolidaysResource();
}
Enter fullscreen mode Exit fullscreen mode

holiday.service.ts (resource factory)

createHolidaysResource() {
  return resource({
    loader: async () => {
      await new Promise((resolve) => setTimeout(resolve, 1200));
      return holidays;
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

The full service is available in StackBlitz.

Router resources are an elegant way to simplify that.

We can define a resource at the router level, where it fetches the data when the user navigates to that route. By default, it also blocks route activation, meaning the router only renders the component once the resource has successfully fetched its data. The component gets the fetched data via input binding, as if the data had been there all the time.

holidays-routes.ts

import { inject } from '@angular/core';
import { Routes } from '@angular/router';
import { HolidayService } from './holiday.service';
import { HolidaysPage } from './holidays-page';

export const routes: Routes = [
  {
    path: 'holidays',
    component: HolidaysPage,
    resources: () => {
      const holidayService = inject(HolidayService);
      return { holidays: holidayService.createHolidaysResource() };
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

holidays-page.ts

import { Component, input } from '@angular/core';
import { Holiday } from './holiday';
import { HolidayCard } from './holiday-card';
import { Title } from './title';

@Component({
  selector: 'app-holidays',
  imports: [HolidayCard, Title],
  template: `
    <app-title />
    @for (holiday of holidays(); track holiday.id) {
      <app-holiday-card [holiday]="holiday" />
    }
  `,
})
export class HolidaysPage {
  readonly holidays = input.required<Holiday[]>();
}
Enter fullscreen mode Exit fullscreen mode

The holidays input matches the resource name in the route configuration. We also need to enable router resources and component input binding:

app.config.ts

import { ApplicationConfig } from '@angular/core';
import {
  provideRouter,
  withComponentInputBinding,
  withRouterResources,
} from '@angular/router';
import { routes } from './holidays-routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withRouterResources(), withComponentInputBinding()),
  ],
};
Enter fullscreen mode Exit fullscreen mode

If multiple resources are defined across nested routes or for one specific route, their data loads in parallel, which also improves the loading time for the end user.

Router resources also have further options. They can, for example, be non-blocking, be created asynchronously, and much more. For more details, head to the official documentation.

Error Boundaries

Whenever an unhandled error is thrown during rendering or change detection, it can bubble up through the component hierarchy and interrupt rendering.

For obvious reasons, we can catch errors in TypeScript with try/catch so that we can isolate them.

For demonstration, our Title component throws an unhandled error. When it is used as a child component, it also causes problems for its parent view. In our case, the holiday cards would not be rendered, and HolidaysPage stays empty.

import { Component } from '@angular/core';

@Component({
  selector: 'app-title',
  template: '<h1>Holidays</h1>',
})
export class Title {
  constructor() {
    throw new Error('The title could not be created');
  }
}
Enter fullscreen mode Exit fullscreen mode

holidays-page.ts (template)

<app-title />

@for (holiday of holidays(); track holiday.id) {
  <app-holiday-card [holiday]="holiday" />
}
Enter fullscreen mode Exit fullscreen mode

Error boundaries bring the concept of a try/catch block from TypeScript to the template.

A @boundary template block wraps the risky template and isolates the error within it. Title will therefore cause no further havoc in the rest of the application.

The catch part would be @error, where we can provide a fallback template:

holidays-page.ts (template)

@boundary {
  <app-title />
} @error {
  <h1>Holidays</h1>
}

@for (holiday of holidays(); track holiday.id) {
  <app-holiday-card [holiday]="holiday" />
}
Enter fullscreen mode Exit fullscreen mode

There are more options, including access to the error, resetting a boundary, and selecting a fallback conditionally. For this example, a static fallback is enough. In the companion demo, use Broken title to see that fallback while the holiday cards stay visible.

Error boundaries with @boundary • Angular

The web development framework for building modern apps.

favicon angular.dev

Private Members in Templates

The third feature is a small change to the component class. We can now access TypeScript private members from its template:

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-private-title',
  template: '<h1>{{ title() }}</h1>',
})
export class PrivateTitle {
  private readonly title = signal('Holidays');
}
Enter fullscreen mode Exit fullscreen mode

Before Angular 22.2, we would normally make a component member (property or method) used by the template public or protected. protected was the choice when the member was meant for the template rather than the component's public API, although subclasses could still access it.

Now private is also an option, and for members used only by the component and its template, it is the one I would recommend. From a conceptual perspective, the component's template and TypeScript class form one unit. Accessing a private member in the template underlines that perspective.

Angular team discussion: the component and its template form one unit

One distinction is important here. TypeScript's private modifier and ECMAScript # fields are different. A field that is private at runtime, like #title, cannot be accessed directly by the template. It still makes sense for state that should stay inside the class.

For the sake of consistency, future Angular code should therefore use TypeScript's private for members that the template needs to access.

protected, on the other hand, will likely disappear from template-only use or be used mainly for class inheritance.

Top comments (0)