Angular 16 - More than just signals

There's more great features to talk about

If you're following Angular or people from the Angular community, you've probably heard about signals already. Angular 16 provides this new feature, which the Angular team will further develop in the future. But aside from signals, there are many other features in this release worth talking about. Maybe you are not ready to rewrite your application and you're thinking: is this new version worth it? TLDR; yes!


Self-closing tags

One thing we hate as a developer is writing duplicate code. I have my own opinion on when code is a duplicate or not (for instance when the same code is used for different behaviour), but if the statements you see are the same ones you've already seen before that should trigger a red flag.

Previously, we had to write angular tags like this:

<game-character-details 
  [character]="character"
  (rename)="renameCharacter($event)">
</game-character-details>

While the duplication is small, it clutters your view when you try to read your code. Come in Angular 16 which allows us to self-close our angular tags.

<game-character-details [character]="character" (rename)="renameCharacter($event)"/>

The change might not seem so big, but if you're looking to make your code more readable and in doing so more maintainable: use self-closing tags.

Component Input Changes

In Angular 16 they've changed a few Input-related things. I decided to group them here. One of these are required inputs. This feature only adds a compile-time check, so nothing is happening at runtime. Let's have a look at the following code.

@Component({
  selector: 'game-character-details',
  templateUrl: './game-character-details.component.html',
})
export class GameCharacterDetailsComponent {
  @Input({ required: true }) character: Character;
}

This small addition of code makes it explicit you are requiring something when using this component. If you are reusing components throughout your application or you create libraries, the required inputs will improve your API. You will now know the need for filling in some attributes, yes or no. It's small but very effective.

Apart from required inputs, Angular 16 also provides a way to bind router data to component inputs. Remember when you had route-dependent data and you needed to use it in a component? It looks something like this:

const routes: Routes = [
  {
    path: "characters/:id",
    component: CharacterComponent,
    resolve: { character: CharacterResolver }
  },
];

@Component({})
export class CharacterComponent implements OnInit {
    private route = inject(ActivatedRoute);

    character$ = this.route.data.pipe(map((data) => data['character']));

    ngOnInit() {
        this.character$.subscribe(character => { // do something with the character });
    }
}

If we were lucky or designed our application well, we were able to pass the character$ observable to other components using the async pipe. Otherwise, we'd extract properties from the character or the character itself in the subscription. I'm sure we all love writing that clutter code for closing the description during the destruction of the component, right?

Again, the Angular team has given us a new treat for this. Router components, the components used in routes, will now be able to inject data from the route directly as inputs. It changes nothing to the route declaration but the component now looks like this:

@Component({})
export class CharacterComponent implements OnInit {

    @Input()
    character: Character;

    ngOnInit() {
        // you can still do something with this.character here :-)
    }
}

Now ain't that a lot nicer to work with? Let's have a deeper look at what this means for the CharacterComponent:

  • It now makes sense to give it OnPush ChangeDetection, making the component render only when the inputs change. Before, this made no sense because... the component had no Input. Now it does and we can benefit from the Angular tools we have for other non-router components.

  • You can rename your inputs if you want. If your routeparam has the name "id", you can use @Input('id') to rename the property of your class to something like characterId.

  • It's not working now, but I'm assuming they will enable required inputs for router components as well. This means that your route configuration could give you compile time errors when using a component with required inputs that are not defined in the route definition.

And all you need to do to enable this is:

@NgModule({
  imports: [
    RouterModule.forRoot([], {
      //... other cool features
      bindToComponentInputs: true // <-- enabling this feature
    })
  ],
})
export class AppModule {}

Or if you've already adopted standalone components:

bootstrapApplication(App, {
  providers: [
    provideRouter(routes, 
        //... other cool features
        withComponentInputBinding() // <-- enabling this feature
    )
  ],
});

Support for TypeScript 5

Angular 16 supports the usage of TypeScript 5, which on its own comes with a lot of features. I'm planning a blogpost on those features, but here's a small list of what you can use with this new version:

  • Decorators: this awesome feature will allow us to reuse certain behaviour throughout different classes by adding annotations.

  • Enum improvements: there used to be some confusion about TypeScript's enums but they are working on that. They are made more type-safe as they are now fully Union Enums:

      // Color is like a union of Red | Orange | Yellow | Green | Blue | Violet
      enum Color {
          Red, Orange, Yellow, Green, Blue, /* Indigo */, Violet
      }
    
  • Support for better import sorting and adaptive switch/case completions.

  • Compilation speed improvements

If you're a TypeScript developer and this doesn't get you excited, I don't know what will!

Improved tooling

The Angular team has listened to its community and learnt that we've outgrown the Karma test suite. Instead, most project use Jest these days and the Angular team has heard us. They've added experimental Jest support and are working on improving that. I for one like that I am no longer pushed into the direction of a specific testing framework when setting up a new project. Thanks, Angular team for allowing us to choose!

Ever feel like your ng serve slows you down? That's also being looked at. Angular 16 allows you to start your local dev server with Vite. You can build your code through esbuild and the Angular team reports preliminary significant improvements:

"Early tests showed over 72% improvement in cold production builds." - Minko Gechev in https://blog.angular.io/angular-v16-is-here-4d7a28ec680d

After upgrading, you can give this a try yourself by changing your angular.json (or project.json if you use nx):

...
"architect": {
  "build": {                     /* Add the esbuild suffix  */
    "builder": "@angular-devkit/build-angular:browser-esbuild",
...

When upgrading and changing our codebases to align with the newer Angular features, it would be handy to have a migration tool for some of them. A great feature would be schematics for migration to standalone components. Oh wait, look what's in the Angular 16 release... You can read all about it on the Angular website!

Conclusion

When I read about Angular 16, most of what I read is about Signals. But this feature is not the only thing that is in this release. There are a lot of features to ease the lives of us, Angular developers.

I predict that features like required inputs, improved build or serve time, self-closing tags will all become the standard soon. Maybe faster than adopting signals or standalone components, because the impact on an existing codebase is smaller.