Tutorial: Architecting forms with Signals

In today’s post, I want to showcase several Angular framework features in one example. More specifically, we’ll use standalone components, signals with model() and input(), as well as forms.

Our goal is to build a reusable date-range picker component to select two dates:

To do so, let’s create a standalone component with two HTML date inputs (no need for component libraries!):

We use ngModel (which is from FormsModule) to capture the current value selected by the user and set a default value for those dates if needed. To do so, let’s use two signals created by the model() function from @angular/core:

Two important things to note:

  1. These two variables are signals and work seamlessly with ngModel
  2. These variables enable two-way data bindings with the parent component as follows:

start and end are signals as well. We can use those signals to pass a default value to the child component or keep that value empty if we don’t want any:

And that code is 100% functional as-is. No need for template-driven forms or reactive forms! Now, let’s add some features to our date-range picker component. Let’s say we want to enforce the rule that the start date cannot be after the end date and vice versa. The following bindings do the trick using the min and max HTML attributes:

Because this is all based on signals, it’s all reactive by default. Changing the start date or the end date automatically updates the range of what’s selectable in the other date input:

Adding more features is now as easy as adding more signals and bindings. For instance, I want to enforce a minimum start date. I added a new signal input called minDate and I bind it to the min attribute of our startDate:

And then, in the parent component, we can pass a minimum start date:

You can find that code example in action on Stackblitz here. Note that our date range picker has less than 20 lines of code and relies on basic HTML features (input, min, max) instead of adding dependencies that would impact performance.

Please email me if you’d like me to add more features to that example in a future post. I’m always happy to cover what really matters to most people reading these posts.

What’s an Angular view?

As Angular developers, we’re used to thinking of our application as made up of different components. The funny thing is that from the perspective of the Angular team (and compiler), an Angular application is made up of views.

What’s the difference?

The Angular documentation says it best: A view is the smallest grouping of display elements that can be created and destroyed together. Angular renders a view under the control of one or more directives (remember that a component is a specific kind of directive with an HTML template).

In other words, every component creates its own view, and such a view can be made of other views. For instance, when you use a structural directive such as ngIf or ngFor, you’re creating a new view.

Why does it matter?

Because of Signals. The Angular team is working hard on signal-based components for several reasons, one of them being improved change detection that will apply at the view level instead of the component level. This means that when we use signal-based components, the granularity of change detection will be significantly more accurate.

Say, for instance, you have a component with 100 lines of HTML code in its template, and two lines in that template get displayed conditionally based on the value of a signal. With the signal-based approach, changing the value of that signal will result in Angular trying to render just those two lines of the HTML template impacted by the signal update and not bothering about the other 98 lines of code.

When compared to the current default Zone.js change detection, which would check the entire component tree for changes, you can see how massive a difference signal-based components will make. That is why I keep making the point that everyone should be migrating to signals. They are a true game changer for the performance and maintainability of our Angular applications.

All about environment variables with Angular

Angular apps created with the Angular CLI used to have automatic environment creation built-in for new projects. While this automation was removed in recent years to simplify the learning curve of Angular, it is still possible to add support for different environments using the command:

ng generate environments

Before we generate such environments, it’s important to take a look at angular.json in the root folder of any Angular project, more specifically, the section called configurations in the build section:

This tells us that our project has two different configurations: production and development, and the default config for the ng build command is production.

Note that the serve section of angular.json has a similar config that extends the build targets and overrides the default configuration by making it development, which, as a result, is what we use when we run ng serve:

If we want to override the default configurations when running ng serve or ng build, we can do this by adding a configuration flag to the command and making it point to the proper configuration name, for instance:

ng serve --configuration=production

ng build --configuration=development

How do we plug environment variables into this?

When we run ng generate environments, the Angular CLI does two things:

It creates two new files in src/environments: environments.ts (the environment for production, which is also the default) and environment.development.ts for development.

angular.json gets updated with the following highlighted syntax:

As you can see, this instructs the builder to replace the production environment file with the development file when we use the --configuration=development flag.

Such environment files can contain any environment variables we want. For instance, say I want to define a SERVER_URL that is different for production and development. I can do that:

Now, to use such environment variable in my code, all I need to do is import it as follows:

Important: The import has to be from /environments/environment. Angular will pick the correct configuration based on the flag given to the build or serve command.

How do you create environments for QA, pre-prod, and more?

Now that we know that angular.json is the file where all this magic happens, if we want to create a qa environment, we can head to the configurations section of build, add a new configuration block with the name q (or anything you want, of course), and make the file replacement to a new file that I call environment.qa.ts:

Then, I can create that environment file under src/environments and add any variables I need in it:

And that’s it! We covered how to create environment variables for production and development and how to create other custom environments we might need.

How to cache HTTP requests with a service worker?

In the past few weeks, we’ve talked about how to cache data with HTTP interceptors, with Subjects, and with shareReplay.

While all these options work, they are intrusive and need additional code for your application. Another option doesn’t require touching your application code; it only requires additional configuration, and that’s using a service worker.

What’s a service worker?

A service worker is a small piece of Javascript code that runs in the browser independently from your application. A service worker can be an HTTP proxy between your front-end code and server. It can intercept all HTTP traffic between your Angular application and any remote server.

How to enable service workers in Angular?

Run the command: ng add @angular/pwa

That’s it! You can read more about what the command does in the official documentation. The idea is that instead of writing our service worker code, we let the Angular API take care of that and generate the service worker based on our config.

How to configure what to cache, and for how long?

Angular generates a file called ngsw-config.json. In that file, you can specify which URLs you want to cache, for how long, and if you wish to optimize for freshness (use the cache only if the application is offline) or performance (use the cache as much as possible even if the app is online).

For instance, data that doesn’t change very often (a list of countries or currencies) could be cached for weeks or months, while dynamic data (a list of Tweets or user preferences) could be cached for just a few minutes:

You can find a complete example of such ngsw-config.json here. The full documentation for all supported options can be found here.

Service workers are part of Progressive Web Apps (PWAs). If you want to learn more about that, here’s a quick PWA tutorial of mine.

Angular 17: Trigger options for @defer

Yesterday, we introduced the new @defer bloc to lazy-load components on the screen. We saw that @defer includes several config options to display errors, placeholder, and loading templates.

Today, let’s focus on the possible triggers for such lazy loading.

idle

This is the default option. Will load the contents of that block once the browser is in an idle state:

viewport

Triggers the deferred block when its content enters the viewport. For instance, that would be when the user scrolls down, and the block becomes “visible.” Note that this option requires a placeholder (used to detect when the element comes into the viewport):

Another interesting option is to load the deferred block when another specified element makes it into the viewport using a template reference variable. That option does not require a placeholder:

interaction

Waits for the user to interact with the placeholder to load the deferred block. Even better, this trigger can be combined with another element so you can lazy-load a component on a button click, for instance:

The above code can be tested on Stackblitz here.

hover

Similar to the two previous examples. Waits for the user to hover over the placeholder or a specified element:

immediate and timer

immediate triggers the deferred block immediately as soon as Angular has finished rendering. Timer waits for a specified delay:

You can see most of these different examples on Stackblitz here. There are a few more options for @defer that I’ll cover later to keep this newsletter short and readable. It’s incredible what such a small API can do!

Async pipe, Signals, and Change Detection

Yesterday, we covered what markForCheck() can do in terms of change detection and why it could be needed at times.

Regarding best practices, I often mention using the async pipe or Signals to improve change detection and performance in our apps. Why is that? That’s because both the async pipe and Signals have some extra code to help Angular detect changes.

Here’s what we can find in the source code of the async pipe:

The last line of code indicates that whenever an async pipe receives a new value, it marks the corresponding view for check, triggering the need for change detection. That’s the reason why the async pipe works well even when used in an OnPush component: Its internals override the behavior of whatever change detection strategy we use.

Signals do something similar by marking their consumers (or “subscribers,” in a sense) as dirty when the Signal value changes:

As a result, and back to the point I was making yesterday, Instead of trying to use markForCheck() directly in our code, we should rely on the tools designed to do it automatically and in an optimized fashion for us.

Conclusion: Always use the async pipe with RxJs (you can always do so) or use Signals for an even better experience.

ChangeDetectorRef.markForCheck()

The following method call is either something you’re either very familiar with or something you’ve never heard about: ChangeDetectorRef.markForCheck()

If you’ve never heard about it, congratulations! It means you’ve been using Angular in a way that doesn’t require you to mess with change detection, and that’s an excellent thing.

On the other end of the spectrum, if you see that method in many places in your code, your architecture might have a problem. Why is that? As we covered before, there are two types of change detection: Default and onPush.

When we use onPush, we’re telling Angular: “This is a component that relies on @Input values to display its data, so don’t bother re-rendering that component when @Input do not change”.

The thing is, sometimes people start using onPush full of good intentions. Then, they start injecting services into their component, and when the data in those services change, the component does not re-render… So, instead of disabling ChangeDetection.OnPush (or figuring out a better way to keep their component as a presentation component), they go to StackOverflow, where people say: Just use ChangeDetectorRef.markForCheck(), and problem solved! Here’s a typical screenshot of such a response:

That’s because ChangeDetectorRef.markForCheck() does exactly that: It’s a way to tell Angular that something changed in our component, so Angular must check and re-render that component as soon as possible. As you can guess, this is meant to be an exception and not the rule.

The main reason why we would use ChangeDetectorRef.markForCheck() is if we integrate a non-Angular friendly 3rd party library that interacts with the DOM and changes values that Angular cannot see because Angular doesn’t manage that DOM. In other scenarios, we should avoid using it and think about a better architecture.

Perf and template syntax – Example 1

Yesterday, I sent you 3 different examples of Angular binding syntaxes and asked you to take a look at them and identify the pros and cons of each one.

Today, let’s cover the pros and cons of our first example:

Example #1 – Array of data

<div *ngFor="let data of getData()">

The only pro of this example is the simplicity of the syntax. Other than that, the syntax uses one of the anti-patterns covered earlier in this newsletter: Calling a method in a template. The problem is that this method will be called a lot more than you’d think, and if that code happened to create a new array every single time, it could be really bad in terms of performance, as Angular would have to re-render the entire list pretty much every time the user does anything in the application.

What can we do to improve it?

  1. Use a variable instead of a method – this fixes the performance issue right away:

    <div *ngFor="let data of dataList">
  2. Use a trackBy function to avoid useless re-renders. By telling Angular what’s the unique ID of each list item, Angular would be less confused, but it’s a sub-par solution:

    <div *ngFor="let data of getData(); trackBy: trackById">

Conclusion

If you want to iterate over data that’s in your component (not an observable or a Signal), then use a variable instead of calling a method. Easy enough.

Angular Change Detection Illustrated

I see a lot of confusion around Angular change detection. I compared the OnPush and Default strategies in a past post, but let’s illustrate them with visuals.

Default Change Detection

An event happening anywhere in the DOM tree will have Angular check the entire tree for changes:

OnPush Change Detection

If a hierarchy of components is using OnPush, then only that branch on OnPush components will be checked for changes:

Signal Change Detection

This is the future of Angular. When using Signals, only the views of components that use that Signal will get updated, making it the best and most accurate option:

Creating a library with Angular

You’re probably used to creating Angular applications, but what about Angular libraries?

First, it’s essential to define what an Angular library is. A library is a collection of components/pipes/directives or services bundled together that can be shared and used in multiple code repositories. As a result, Angular libraries are designed to be published on npm (publicly or privately) so they can be shared with other people externally or internally.

If you want to start a new library from scratch, these Angular CLI commands will get it done:

The above commands create a projects/my-lib folder in the workspace, with a sample component and service in it. The main difference between a library and an application is that a library exposes public features that can be imported into other libraries or applications.

Such features are listed and exported in public-api.ts in the library folder. That’s where you decide what’s public/private in your library code. In this example, the library is just one service:

Then, to test or build your library, you can use regular Angular CLI commands such as:

Once built, a library can be published to the public npm repository with one single command. This command has to be run from the dist folder where the compiled library can be found after running your production build:

Note that this command requires an npm account and npm authentication before you can publish. Upon publishing, the version number used in your package.json will be used as the public version number on npm:

And on npm’s website:

At that point, anyone can run npm install [your-library-name] and use your code in their projects. Nice and easy!

Note that the library I used in that example is a work in progress and should not be used as-is in your apps.