0

Is Angular 16 Starting a (R)evolution?

 11 months ago
source link: https://devm.io/angular/angular-16-features
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Breaking changes, features, and everything new in Angular 16

Is Angular 16 Starting a (R)evolution?


Angular has an especially good reputation when it comes to long-lived applications where investment protection is an important criteria. Even during critical builds, like converting the rendering engine to Ivy, the Angular team has always shown good instinct. Through continuous ng-upgrade improvements and good engineering, necessary migrations means less effort for developers. This is especially true since many changes were implemented incrementally over the course of several releases. Often, even the API interface for typical applications could be kept stable. Angular 16 sets a new tone in that regard and not everyone will like that. Only time will tell whether the Angular team, and Google behind them, is making smart decisions. To get an idea ourselves, we’ll take a look at the important changes in Angular 16 in this article.

TypeScript

Angular also pays attention to keeping its own dependencies up to date with this release.

Therefore, in Angular 16, TypeScript must be version 4.9 or 5.0. Let’s look at the major new features in TypeScript 4.9 first. The other new features in TypeScript 5 can be read in this article here.

With TypeScript 4.9, came some innovations. For example, there are improvements in build performance or file-watching by the dev server. Additionally, an “auto-accessor” keyword was introduced (example: “public accessor name: string;”), which automatically creates both a “set” and a “get” accessor for the “name” property. A compiler hint was also added to print an error as soon as a comparison with “NaN” is performed, since these comparisons are actually always an error (“NaN !== x” is always “true”, even if “x=NaN”, while “NaN === x” always returns “false:). Instead, the function “Number.isNaN(someValue)” should be used.

A new, helpful operator was also introduced. The new “satisfies” operator makes type-checking a bit more flexible. For example, an object should be created that corresponds to the “Campaign” data structure from Listing 1. Thanks to TypeScript's structural type system, a simple object “oldCampaign” can be constructed to match this structure. See Listing 1. Thanks to TypeScript’s structural type system, a simple object “oldCampaign” can be constructed that corresponds to this structure. See Listing 1.

However, an error found its way into this object. The “enddate” property has a typo. The “D” in “date” should be capitalized. This error was only noticed during further usage, or, in case of doubt, at runtime. To avoid these kinds of errors, the type can be included in the variable definition. See Listing 2. But this means that contextual information about the specific types used will be lost. Direct access to, for example, the “getFullYear()” method of “Date” is no longer possible, since the “endDate” in “Campaign” could also be the type “string” or “number”. They do not have this method. The type specification by the interface overwrites, so to speak, the information that the “endDate” is a real Date object here.

The “satisfies” operator in TypeScript 4.9 now allows us to check if “oldCampaign” complies with the type “Campaign” without overriding the specific context information. See Listing 3. Therefore, TypeScript still detects (typing) errors, but access to the “getFullYear()” method is allowed without further ado. It’s important to know when using the “satisfies” operator that the type-checking is exact. So the operator does not allow additional properties either. An additional property “description” would throw an error here, since this property doesn’t exist in the campaign.

Listing 1: Incorrect object for the “Campaign” interface

interface Campaign {
  title: string;
  startDate: Date | number | string;
  endDate: Date | number | string;
}

const oldCampaign = {
  title: 'That Millenium Campaign!'
  startDate: '2000-01-01' ,
  enddate: new Date('2000-12-31') 
  // ^--- Runtime bug ahoy!
}

Listing 2: Object with type declaration

const oldCampaign: Campaign = {
  title: 'That Millenium Campaign!'
  startDate: '2000-01-01' ,
  endDate: new Date('2000-12-31')
  // ^--- Typo would trigger TypeError
}

oldCampaign.endDate.getFullYear()
// TypeError: endDate could be string/number

Listing 3: Using the “satisfies” operator

const oldCampaign = {
  title: 'That Millenium Campaign!'
  startDate: '2000-01-01' ,
  enddate: new Date('2000-12-31')
  // ^--- Typing error triggers TypeError here too
  description: 'Demo-Campaing'
  // ^---  Additional properties are errors too!
} satisfies Campaign

oldCampaign.endDate.getFullYear()
// Access allowed in this context!

The “in” operator can be used in TypeScript for what is known as type narrowing. For example, in Listing 4, the “in” operator is used in the function “doItWithObjects()” to cause type-narrowing of the union type “Cow | Bird”: If the variable value contains the property “layEggs”, then TypeScript assumes “value” must be of the type “Bird” and allows the function call.

However, until now it wasn’t possible to generate type information about unknown objects with the “in” operator. For example, in Listing 5, the function “tryGetDataContent()” is passed a data object with unknown types, or the “unknown” type. To make sure that this passing value is in fact an object that isn’t “null”, the two queries in the first statement are needed. With these type guards, TypeScript knows that “data” is an object. It needs further type-guards to enable access to properties. Previously, you could not use the “in” operator to access properties that weren’t listed by type definitions. Accessing “data.content” resulted in a compiler error, despite being protected by an “in” typeguard. Now, in version 4.9, TypeScript recognizes that this access is protected and is therefore allowed.

Listing 4: Type narrowing through type guard

interface Cow {
	giveMilk(): void;
}
interface Bird {
	layEggs(): void;
}

function doItWithObjects(value: Cow | Bird) {
  if ('layEggs' in value) {
    value.layEggs(); // value is of type 'Bird'
  }
}

Listing 5: Type narrowing for unknown types

function tryGetDataContent(data: unknown): string | undefined {
  if (data && typeof data === "object") {
    if ("content" in data && typeof data.content === "string") {
                                      // ^ Previously: TypeError
      return data.content;
    }
  }
  return undefined;
}

Angular CLI

Almost all Angular projects are set up and built with Angular CLI. Angular CLI provides an abstraction over the underlying build system. For example, in the past, the switch from SystemJS to Webpack could be hidden as an implementation detail.

Currently, both Vite from the Vue environment and esbuild are being investigated as successors for Webpack. However, it is certainly still at least one release away until the actual switchover.

There are also some innovations in Angular CLI 16, some of which concern the aforementioned experiments.

Therefore, existing support for the esbuild-based build setup is extended with progress outputs. Furthermore, the esbuild DevServer can now use e.g. SSL. The inclusion is done as with the existing setup via “ng serve --ssl”. Additional scripts can be included in the build. This is handy when including global scripts like the Google Tag Manager.

Some enhancements have also been made to the existing build system: For example, new applications can be generated as standalone applications (i.e. without @NgModules). For this purpose, the flag “--standalone” is included when generating the application. In order to align the functionality of standalone applications with the functionality of previous Angular applications, it is now also possible to generate service workers and app shells for standalone applications via schematics.

The app or expertise in the app must be tested to ensure that the functionality of the applications you painstakingly built is guaranteed in the long term. There are different levels of testing for this, such as E2E or unit tests. Support for Protractor as the default E2E tool was discontinued several Angular CLI versions ago. Since then, Protractor itself has also been discontinued. Now it continues with unit tests. Karma was also initiated by Google, but has been discontinued in the meantime. Therefore, Angular is working on a successor. There will probably be different options. If less value is placed on a browser-oriented test environment, and the test execution speed is more important, then the Jest tool can be used. This comes from the React environment. A preview implementation for this tool is coming in Angular-CLI 16, but it shouldn’t be used in production yet. It can be activated by switching to the new Jest test builder ("builder": "@angular-devkit/build-angular:karma") instead of the previous test builder ("builder": "@angular-devkit/build-angular:jest"). Alternatively, there are efforts to connect the "Vitest" test system from the Vue environment. In order to be able to continue to run tests in the browser, Angular-CLI will prospectively switch the current Karma/Jasmine tests to the “Web Test Runner”/Jasmine combination. Similar to Karma, the “Web Test Runner” can run tests in the browser. The Jasmine syntax will continue to be used as the text syntax in order to make the changeover as simple as possible.

Breaking Changes in Angular-CLI 16

Of course, with the new major Angular CLI version, there are also some breaking changes. Support for TypeScript 4.8 and Node.js 14 has been removed. Instead, TypeScript 5 is used as the default, and Node.js should be version 16, 18, or newer.

Angular CLI 16 also cuts some obsolete or outdated braids. The Angular compatibility compiler NGCC is removed in Angular CLI 16. NGCC was responsible for transforming old libraries still based on the view engine to make them compatible with the new Ivy compiler. Since the view engine has already been deprecated since Angular 12, the Angular team assumes that all essential libraries have already been adapted accordingly so that they are made available in an Ivy-compatible way. This means that projects still using libraries built only for the view engine cannot migrate to Angular 16 until the libraries have been adapted. If these are transitive dependencies, you may not even be aware of this potential problem. In order to make a library compatible for Ivy, the respective library should be changed to a more recent Angular version (at least 12) and in the “tsconfig.lib.prod.json” the compiler mode should be set to “partial”. See Listing 6. Since this usually doesn’t have any negative effects, this mode should definitely be activated. With this change, the transition from the old view engine is finally complete. Ultimately, code omitted in this way should allow the application bundles to get smaller again.

Listing 6: Partial library compile mode

{
  // ...
  "angularCompilerOptions": {
    "compilationMode": "partial"
  }
}

In Angular 15, the “CanMatch” guard has been added to the router. This guard allows individual routes from the Angular route configuration to be blocked for matching, which occurs during every routing process. This means that the guard starts earlier in the routing process than the old CanLoad and CanActivate guards. This makes the Guard much more flexible. Because of this, the schematic for the CanLoad guard is removed in the Angular CLI. Instead, the new CanMatch guard is used.

In the routing process, resolvers allow data to be loaded in parallel with a routing operation. They can be included in the route configuration similarly to the guards and are executed after the guards in the routing process. The previously class-based resolvers and guards are now generated as functional resolvers and guards, respectively, by default. This is in line with the new standard, which we will look at in more detail in the Angular section later. The old behavior of Angular Schematics is still available until further notice. For this, “--no-functional” has to be specified as an additional switch. The class-based implementation is marked as deprecated, so developers should deal with the issue now and change if necessary.

Furthermore, various configuration parameters of angular.json have been removed after they were already deprecated in previous versions. This affects “defaultProject”, which could be used to configure the “default project” in the Angular CLI workspace. “Default" here means that the CLI commands (ng serve, ng test, ...) were executed in the default for the default project, if it was not explicitly given a different project name. However, this behavior is now no longer part of the Angular CLI. Since a few versions ago, the project in whose subfolder the respective command is executed is simply set as the “default”.

The "defaultCollection" for setting the collection used when executing Schematics commands (“ng generate xyz”) has also been removed. Instead, the property is now “schematicCollections” a replacement, which can be passed an array of different schematic collection names. See Listing 7.

Listing 7: Old and new specification of Schematic Collections

// Previously
"defaultCollection": "@angular/material"

// Now
"schematicCollections": ["@angular/material"]

Angular

A lot has also been done in the framework itself. Some of this can be seen as preparation for further planned design changes and restructuring measures.

The focus is currently on the possibilities for server-side rendering, i.e. executing the Angular application dynamically by the server and sending the result to the client as finished HTML source code. This enables faster rendering of the first view, which can be measured in core web vitals. Higher user satisfaction and ranking in search engines should result. If you do not switch frequently between different Angular applications, or are in the enterprise environment, where internal applications are rarely started, these are admittedly not high priority goals. But for applications made for end users on the Internet, these can be essential criteria for success, since switching between websites often occurs.

There is also an effort to be able to create Angular applications without the Angular module system. This should reduce the learning curve and enable less complexity, especially at the start. In particular, the Angular module system is a strength that should not be underestimated for larger and more complex projects. So we can only hope that the module system will continue to be a supported, albeit optional, feature in the future. That is exactly what it looks like at the moment.

Besides components built in the “normal” component tree, there are usually “separately” displayed components like modal dialogs, popups or even toasts. With the old view engine it was necessary to announce these separate components in the “entryComponents” property of a “@NgModule” (or a “@Component”). This is no longer necessary with Ivy, so the property has been deprecated since Angular 9. With Angular 16, the “entryComponents” property is now removed, since it had no effect after Ivy anyway. The “moduleId” property of the “@NgModule” has been deprecated. This dates back to Angular’s early days, when applications were built with SystemJS. For many versions now, this property has no function.

To pass data to child components, “@Input()” bindings can be used. It has always been possible to pass a string parameter to the input decorator, by which the input was made available externally under an alias. However, this input renaming is only recommended in special constellations and should not be used excessively. With Angular 16, another configuration option for the @Input() decorator has been added. As of the new version, inputs can be set to “required”. For this, a new flag was introduced that can be given to the input decorator via the Config object. Listing 8 shows 3 variants as an example. Above shows the previous input renaming variant, which still works. Below is a “barVal” property that gets the alias “barAlias” and is also marked as “required”. At the bottom, a property is marked as “required” only. If a property is marked as “required”, then this property must be set from the outside in any case. If the outside component does not set such a property to a specific value, Angular throws a compiler error.

Listing 8: @Input config parameter

@Input('fooAlias') fooVal = '';
@Input({alias: 'barAlias', required: true}) barVal = '';
@Input({required: true}) bazVal = '';

In TypeScript version 5, decorators similar to the ECMAScript decorators specification were introduced. In the long term, these intend to replace the previous, experimental decorators. Angular uses the TypeScript decorators to provide meta information for the dependency injection system, among other things. The information can be stored for classes, the method parameters, or the constructor. However, the final decorators are only supported on types, so “@Component”, “@Directive”, "@Pipe", “@Injectable” and “@NgModule” can be implemented with the new standard.

Constructor parameter decorators, such as "@Inject()", "@Host()", "@Optional()" and others that can be used to configure dependency injection, are not yet possible with the new specification. Since it is not yet certain whether parameter decorators will be included in the ECMAScript and TypeScript specifications, Angular continues to use "experimentalDecorators" by default. If both TypeScript 5 decorators and DI configurations will be used, the "inject()" function introduced in Angular 14 must be used, as seen in Listing 9.

Listing 9: inject()function with Flags

@Component({})
class DemoComponent {
  readonly demoService = inject(DemoService, {
    optional: true,
    host: true
  });
}

As mentioned previously, the router guard and resolver interfaces are deprecated. Therefore, all references to these interfaces will be deleted by an automatic migration when upgrading to Angular 16. At first, the Guard can continue to be used as usual, since TypeScript now checks the Guard function’s signature itself. Listing 10 shows an example of a "CanActivate" guard. Its "canActivate()" method must match the "CanActivateFn" type for the guard to be used as a CanActivate guard. Listing 10 also shows what a functional "CanActivate" guard looks like in principle. It should be mentioned here that you can also inject services in functional guards. The "inject()" function must be used for this. See Listing 10. In the future, guards will only be written functionally, but you will also still be able to write guards and resolvers class-based. In future Angular versions, this guard must be converted. The conversion can be done in the route definition. Angular provides the conversion functions. Listing 11 shows an example of the conversion function "mapToCanActivate()", which can be used to transform an array of class-based CanActivate guards into functional guards. Listing 11 also shows that class-based and functional guards can be used on the same route by concatenating the two.

Listing 10: Class-based Guard and Functional Guard

@Injectable({providedIn: 'root'})
export class DemoClassGuard {
  constructor(private authService: DemoAuthService) {}
  canActivate(route: ActivatedRouteSnapshot,
              state: RouterStateSnapshot): boolean | UrlTree {
	return this.auth.isLoggedIn();
  }
}

export const demoFuncGuard: CanActivateFn =
  (route, state) => {
     const authService = inject(DemoAuthService)
     return authService.isLoggedIn();
  };

Listing 11: Future integration of Guards

const routes: Routes = [
  {
	path: 'demo',
	canActivate: mapToCanActivate([DemoClassGuard])
                    .concat(demoFuncGuard),
	component: DemoComponent
  }
];

Another pleasant feature has been added to the router in version 16, which can make a lot of boilerplate redundant. Now you can bind data from the routing process directly to component inputs. This can involve binding data dynamically loaded via resolver, and data from the query parameters to the component. Listing 12 shows a component that is bound using the route configuration from Listing 13. In this route configuration, a static data object with the property "demoData" is stored for the route "demo". A resolver object with the property "demoResolved" is also stored. The resolver is synchronous here, but it could also return an asynchronous observable or promise, since this would be resolved by the router. It’s no coincidence that the properties in the route definition are named the same as the "@Input" properties in the component. This is actually a prerequisite for mapping the corresponding properties to each other. The feature can be enabled by passing the function "withComponentInputBinding()" when calling "provideRouter()". See Listing 13. If the new syntax is not yet used to create the router, but the RouterModule is, the flag "bindToComponentInputs: true" can be set in the "RouterModule.forRoot()".

Listing 14 shows an example of what values the component inputs assume when the "/demo?language=french" route is called in the application. It is important to note here that even the query parameters (and the matrix parameters assigned to the route) can be bound using this mechanism, which should simplify developer’s lives considerably. The query parameters to be bound must also have the same name as the input variable to be bound. If there are name collisions between the individual data sources, the data from the resolver has the highest priority. After that the static data from the route configuration is used, then the matrix parameters follow. If the property is not found in any of these sources, the data is extracted from the query params.

Listing 12: Components with Input

@Component({ template: '' })
export class DemoComponent {
  @Input() language?: string;
  @Input() demoResolved?: string;
  @Input() demoData?: string;
}

Listing 13: Route configuration

const routes = [{
  path: 'demo', 
  component: DemoComponent,
  data: {'demoData': 'My static data'},
  resolve: {'demoResolved': () => 'My resolved data'},
}];

// inside AppModule/app-config
provideRouter(routes, withComponentInputBinding())

Listing 14: Component properties at runtime

router.navigateByUrl('/demo?language=french')
// component.language === 'french';
// component.demoData === 'My static data';
// component.demoResolved === 'My resolved data'

A very important topic in this release is Angular's ability to dynamically render the browser application on the server side (server-side rendering, SSR) and send the result to the browser. This allows page content to be rendered directly after being sent to the client, without having to wait for the app to launch. This is especially interesting if the application’s initial loading time has high priority or if the application is available on the free Internet and SEO (Search Engine Optimization) is relevant for the app. Until now, whenever the Angular app was loaded after the initial page load, there was a short flicker. The newly launched app was redrawing the complete DOM. Now you can let this process run non-destructively, without rebuilding the DOM and without flickering. This “non-destructive hydration” is still in developer preview mode. First, the serverside rendering functionality must be added to the app for the new hydration to work ("ng add @nguniversal/express-engine"). Then, in the AppModule’s providers (or the "main.ts" for standalone apps), the function "provideClientHydration()" must be added, similar to Listing 16.

Listing 16: Configuration of non-destructive hydration in the AppModule or when bootstrapping the application in the case of standalone applications.

providers: [provideClientHydration()]

Another optimization is possible with the HttpClient. If Http requests are executed from the application on the server side to generate the application, the HttpClient can now be configured to cache the corresponding responses and use the cache on the client side when the application is started. To do this, the "provideHttpClient()" function must be passed the new "withHttpTransferCache()" call. If NgModules are used, the "TransferHttpCacheModule" module can also simply be imported into the AppModule.

Just like with Router and HttpClient, the Angular service worker has now been extended so it can be added to the app with function. This function is called "provideServiceWorker()" and takes the same parameters that were previously passed to "ServiceWorkerModule.register()".

Anyone who pays attention to the secure operation of their application has come into contact with the "Content Security Policy" (CSP). If a CSP policy "style-src 'self'" or even "'strict-dynamic'" is used for the styles, then the components will not work in Angular applications. This is because the browser does not allow inline styles with the setting that Angular uses for component styles. Here, the CSP directive “unsafe-inline” needed to be enabled, but this is considered potentially unsafe. In Angular 16, it’s now possible to use the "CSP_NONCE" injection token to set a CSP nonce, which Angular appends to the inline styles. If the index.html can still be modified at production time by the delivering server, then instead of the injection token, the attribute "ngCspNonce" can simply be set on the app element. See Listing 17. It’s especially important here that the value of "XYZ" is not a fixed value. It must be recalculated each time the index.html is delivered. A fixed nonce would have similar security properties as the CSP directive "unsafe-inline". Together with the nonce property in the index.html, the nonce must also be included in the CSP directive. For example: "style-src 'nonce-XYZ' 'self'" or somewhat more strictly "style-src 'nonce-XYZ' 'strict-dynamic'". This feature was developed in collaboration between the Angular core, -material and -CLI teams, so these packages also support the new mechanism accordingly.

Listing 17: Dynamic configuration of CSP nonce value in index.html

<app-root ngCspNonce="XYZ"></app-root>

Another new small feature is the "DestroyRef". This can be injected like a service and allows to register logic via callback, which is executed when the component is OnDestroy. For example, resources can be released this way without having to implement the "ngOnDestroy()" lifecycle hook, see also Listing 18. To remove the Destroy logic again, for example, if a resource has already been released for a different reason, the "unregisterFn()" can be called. In this example, "doSomethingOnDestroy()" would no longer be called.

Besides the DestroyRef, a new package "@angular/rxjs-interop" has also been introduced, which is mainly used with the new signals. This package offers the function "takeUntilDestroyed()". This function is a pipeable rxjs operator and can be used to terminate observables once the associated component has been destroyed. Under the hood, this new operator uses a DestroyRef. Listing 18 shows how to use the operator to terminate an interval observable once the component is cleared. In this case, the "takeUntilDestroyed" operator is explicitly given a DestroyRef. This step can also be omitted, then the operator gets the DestroyRef itself via "inject()" function. This would mean that the operator - or the entire observable - must be created in the constructor, since the "inject()" mechanism requires the constructor context.

Listing 18: Example of how to use the DestroyRef

constructor(private destroyRef: DestroyRef) {
  const unregisterFn = destroyRef.onDestroy(() =>
      doSomethingOnDestroy()
  );
  // unregisterFn(); // Takes back onDestroy logic
}

ngOnInit(): void {
  interval(1000)
   .pipe(takeUntilDestroyed(this.destroyRef))
   .subscribe(val => console.log('value:', val))
}

Listen to the signals…

Angular 16 introduces a new concept so groundbreaking that it deserves its own headline. We’re talking about "Signals", a concept that was certainly inspired by Vue. The long-term goal is to get along without "Zone.js" and to be able to map the change detection with other means. This is important to streamline standalone applications and potentially even open up the possibilities of implementing individual elements with Angular, comparable to React and Vue. The motivation given was that Angular applications will become simpler, since using RxJS is no longer mandatory. This should reduce the technical and conceptual complexity.

But the topic also has a bit of an aftertaste. Official discussions only started publicly about three weeks before Angular 16’s first release candidate was adopted. With a development time of six months and the potentially huge consequences of such a design change, this seems very short-term. Critical comments in the RFC discussions were dismissed very diplomatically and it seems as if everything was decided internally by the Angular team and Google. Similarities to Paris 2016 cannot be overlooked. This approach represents a serious risk for the Angular community. Whether Angular will really become conceptually simpler as long as the included HttpClient uses RxJS is also doubtful. After all, developers inevitably come into contact with both concepts. But perhaps the HttpClient will be discontinued three weeks before the first release candidate of Angular 17 with the same argument.

So even if these are promising visions and almost a new generation of Angular, a little more tact and transparency is desirable.

Technically, Signals is a reactivity engine that allows changes to the state of specific data objects to cause updates to the associated DOM elements. Unlike Promises and RxJS Observable, actions on Signals are always synchronous. Since web applications are inherently driven by events, RxJS is a great fit in principle. For transformation between Signals and Observables there is the new package "@angular/rxjs-interop" which provides transformation APIs like 'toObservable()' and 'toSignal()'.

A signal can be created simply by calling the new function "signal()". The type parameter "T" stands for the data type to be managed by this signal. Listing 19 shows with the signal counter that the type of a signal can also be defined by defining an initial value, in this case it is the number 0. The signal is read by calling it like a function. This is shown in the component’s template in Listing 19. Normally, no function should be used for output in templates for performance reasons. In fact, this is usually more of an anti-pattern. But with Signals, this is an explicit design decision by the Angular team and may actually help performance in the long run. The signal itself "notices" when its internal state changes. In case of a change, a signal can give Angular fine-grained hints about which part of the application needs to be updated. This is an important element for performant applications.

There are several methods to change a signal’s internal state. The value can be set to a concrete value by the method ".set()". In our case, we want to increase the counter by one. When we call ".set()", we read the instantaneous value of the signal. Therefore, we can simply use the ".update()" method. This method passes the signal’s current value as a parameter into a callback function. The return value of this callback becomes the new value of the signal. If a signal contains complex data structures like objects or arrays, the ".mutate()" method can be used to change the object or array internally. In the "getNewInvoice()" from Listing 19, this is used to change the invoice amount in the "invoice" signal.

This could also be done asynchronously after an HTTP call, for instance. The signals always get to know when the internal state object changes and can inform the application about this even for purely mutating changes and without the reference changing.

Besides these standard signals, there are also computed signals. The "computed()" function can create a computed signal. It is used in Listing 20 to compute the total price "total" from the invoice. The special thing about Computed Signals is that the compute function is called every time a signal used in the function changes its value. In Listing 19, only the "invoice" is accessed in the "total" signal. So every time something changes in the "invoice", the "total" signal is also recomputed. Theoretically, a computed signal can depend on any number of other signals. The computed signal is recalculated every time one of the other signals changes value.

Listing 19: Example of using the Signal API

@Component({
  selector: 'app-signal-demo',
  standalone: true,
  template: `
	<p> Counter: {{counter()}}</p>
	<button (click)="increase()">Increase</button>
  `
})
export class SignalDemoComponent {
  protected counter = signal(0);

  increase(): void {
	this.counter.set(this.counter() + 1);
	//this.counter.update(counter => counter + 1);
  }
}

Listing 20: Using the Signals API with objects

@Component({
  selector: 'app-signal-object-demo',
  standalone: true,
  template: `
    <p> Total Invoice Sum: {{total()}}</p>
    <button (click)="getNewInvoice()">GetNewInvoice</button>
  `
})
export class SignalObjectDemoComponent {
  protected invoice = signal({price: 1});
  protected total = computed(() => this.invoice().price);

  getNewInvoice(): void {
    this.invoice.mutate(inv => inv.price = 42);
  }
}

Besides signals, there are also effects. They don’t have anything to do with the NgRx effects, but they have a similar idea. Effects are supposed to execute side effects too. But that's where the similarities end. In the context of signals, effects are similar to computed signals - functions that are executed when one or more signals used in the effect have changed.

Effects use the "inject()" function internally, so effects must also always be called in the constructor or injector context. Listing 21 shows an example of what the constructor of the component from Listing 20 can look like. In this case, the "effect()" simply logs out any value change of the "invoice" signal to the console.

In effects, to avoid infinite loops, signals should only ever be read, never written. This is also checked by the Signal API and an error is thrown if necessary.

Listing 21: Example of logging as signal effect

constructor() {
  effect(() => {
    console.log(this.invoice()) 	
  })
}

Signals should also be able to control change detection in components independently of Zone.js. This behavior is not yet part of Angular 16, but you should be able to activate it by including “signals: true” in the component metadata. In this case, the component inputs will also be expressed as signals instead of changeable objects. However, the syntax used to write “@Input”, “@Output”, and similar also changes. Then, all of these Angular components are no longer @decorators, but are now special signals functions. For example, inputs become "input()" signals and outputs become “output()” signals. This fits well with the fact that standard TypeScript decorators don’t work for parameters, but only for types. Similarly, queries for 'viewChild/ren' and 'contentChild/ren' are then also provided as signals. In the lifecycle, the API also changes for signal components. With 'afterNextRender', a function is executed after the completion of the next DOM update cycle. This is similar to the previous 'ngAfterViewInit', but is called more often. With 'afterRender', the specified callback is called after each DOM update. Finally, there is 'afterRenderEffect', which calls an 'effect' with each 'afterRender'.

Overall, the design already looks very promising and future-oriented. Only the question about API uniformity in view of RxJS use remains. However, a sensible hybrid concept can also be established. Currently, signals are still in a preview API and further development will depend on feedback from the community.

Breaking Changes

According to semantic versioning conventions, incompatible changes are always expected in major releases. Angular 16 is no exception, but the impact for the majority of users is limited.

Therefore, version requirements for the tools and frameworks have changed. First, at least TypeScript 4.9 is required. Node.js must be at least version 16, as Node.js 14 has an end-of-life date of March 30, 2023. Zone.js versions older than 0.12 are no longer supported.

As an old construct, ReflectiveInjector has already been marked deprecated and has been dropped. If needed, you can use Injector.create as a replacement.

Since Angular 14, the BrowserTransferStateModule was empty. It is dropped in this release. The TransferState class can be injected directly without the module.

In the BrowserModule, the withServerTransition method has been marked deprecated. This was used for SSR to set an App ID to the app. Now, you can use the injection token “APP_ID” to assign an ID. If multiple Angular apps are placed on the same page, you must set the APP ID to an individual value per app. If only one app is used, the App ID configuration can be omitted.

In Router, all references to the “ComponentFactoryResolver” have been removed, which used to be responsible for lazy loading, among other things. However, since Angular 13, the “ComponentFactoryResolver” is no longer needed for this and is generally marked as “Deprecated”. Besides its use in the router, you could also create components manually with the “ComponentFactoryResolver”. To create components manually, simply a reference to the component class is enough. This can be used together with a “ViewContainerRef” to create a component via “viewContainerRef.createComponent()”. Especially in combination with standalone components, it’s also easy to lazy reload components independently of the router.

Conclusion

The Angular framework faces enormous competition. React, and increasingly Vue, are popular with newcomers. The perceived learning curve is flatter and the other frameworks also now have extensive ecosystems. There are also new developments, such as Qwik, which was co-founded by ex-Angular developers.

We can only speculate about the real reasons behind all the decisions. Since the bi-weekly notes were discontinued in September 2022, motivations for transparency don’t seem to be at a high level anymore either.

To be honest, the information content also decreased and became vanishingly small. But the RFC discussions also seem a bit like diplomatic tactics. Less than three weeks before Angular 16’s first release candidate, the RFC discussions on the signal API just began and criticism was "closely watched".

Prior to this, there were appropriate internal discussions and design. The question then arises as to why this has not been transparently implemented. It looks like overall, Angular is facing political pressure from emerging frameworks.

Similarly, investment in server-side rendering is also apparent. It also looks good in the Google Lighthouse metrics. They want to attract new users and make it easier to get started. So far, Angular has shone as a full-stack framework for any foreseeably complex and large applications with excellent maintainability. One hopes that this core group of enterprise developers won’t be so upset by the massive efforts involved in migrating to newer Angular versions that they no longer consider implementing new projects with Angular. As far as migrations to new versions are concerned, effort and benefit need to be proportionate. A look at things like IPv6, Python 3, and the many companies still using Java 11, or even below, shows how excellent Angular has been in the past. Hopefully, Angular will keep its eyes on the target group as well and not lose focus.

Apart from that, big steps towards the Signal API and Zone.js as an optional implementation of change detection are future-oriented and almost seem like a secret vision for a new Angular generation. The same applies to the module system, which can be used as an optional means of structuring large applications well, keeping them maintainable without forcing unjustified complexity on newcomers or small applications. Developers should definitely stay on the ball here - and actively shape what the future of Angular looks like.

Karsten Sitterberg
Karsten Sitterberg

Karsten Sitterberg holds a master's degree in Physics and is an Oracle-certified Java developer who specialises in modern Web APIs and frameworks such as Angular, Vue, and React. In his workshops, articles, and presentations, he regularly reports on new trends and interesting developments in the world of frontend tech. He is the co-founder of the meetup series "Frontend Freunde," the Java User Group Münster, and the Münster Cloud Meetup.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK