Heading Off 11 Common Angular Application Design, Best Practice, & Methodology Challenges
Improving the design, organization, functionality, scalability, and maintainability of your Angular application is the key goal of this article.
This collection of tips is intended to provide guidance for new and experienced Angular developers on some common best practices, methodologies, and design practices that will improve the design, organization, functionality, scalability, and maintainability of Angular applications.
The series assumes knowledge of the core concepts of Angular and dives into detail on more complex topics that can help take your applications to the next level.
Angular Application – Tech Brief
by Dave Cloutier
Audience
Sections
Eleven Detailed Topics Relating To Angular Application Design, Best Practice, & Methodology Challenges
1 – Smart vs. Dumb Components
We start this series with a conversation on architecture. The number one issue I see with new Angular developers is to equate a page of the application to a single component and include all functionality within the component. Properly identifying the purpose of a component and understanding how to interact with child components in a maintainable and testable way is something that can greatly improve the code quality of an application. In this article we take a large component and demonstrate how to break it down into smaller and more re-usable components that better encapsulate functionality. Click to read…
2 – Observables – Keeping Things Clean and Clear
Angular is heavily reliant on the RxJS library which is a library that implements Reactive programming in JavaScript. While it doesn’t take long at all when learning Angular to run into Observables, they can be one of the toughest concepts for new developers to comprehend, especially when coming from a backround of limited exposure to asynchronous programming. This article discusses some best practices, some opinionated ideas, and pitfalls to be aware of that are not always immediately obvious when working with Observables. Click to read…
3 –The Async Pipe
One of the most common patterns seen in Angular components is to subscribe to an Observable and display the resulting data in a component. When following the patterns described in the first two articles, applications can easily add some bloated boilerplate code. The async pipe is a powerful tool to keep that to a minimum that I have found not all developers get exposed to when learning the framework. Along with the general use, we dive into some extended functionality of the ngIf directive in conjunction with the async pipe to quickly and easily create effective display components. Click to read…
4 – Content Projection vs. Inputs
Most tutorials and training stick with the simple approach of binding data to components through an input. This is a simple to implement and effective way to pass data to a child component, however when real life requirements arise such as adding links, images, or other styling, this approach quickly hits limitations. This article aims to show the flexibility and power that comes from content projection opposed to (or often in conjunction with) component inputs. Click to read…
5 – ViewChild and ContentChild
Input, Output, and services are often all that is needed to communicate between components in Angular, however there a times when using these methods do not provide the control and flexibility needed to provide component interaction. ViewChild and ContentChild provide the ability to directly communicate between components and can be extremely powerful when working with tightly related components. Usage of these tools is often so highly situational that makes providing examples relevant to the situations you will likely face is quite challenging, but awareness of the capability as a whole is often the major hurdle needed to investigate the implementation for a specific need. Click to read…
6 – Dynamic Component Generation
Most Angular components are created by adding a reference directly into a template, however there are times where the type of component won’t be known ahead of time and will need to be dynamically created. Often this is seen with modal windows, application level alerts, or other event driven messages, but could also occur when multiple copies or instances of a component need to be created. In order to create these components you will need to take some extra steps and precautions, but it can open the door to far more advanced functionality in an application. Click to read…
7 – Provider Scopes – Component Level Services
Angular’s developers strongly encourage following the single responsibility principle when creating components and classes. When components grow too large we should turn to services to help separate complex functionality to improve readability, complexity, and testability of an application. However, there are times when the singleton nature of services can provide unexpected results. This article focuses on the ability to control the scope of Angular providers to provide the desired functionality within an application. Click to read…
8 – Control Value Accessor
Forms are a critical component to most any web application. While most forms can be completed with the standard browser inputs, sometimes a more dynamic or thematic input can make the difference in a user interface. Angular provides the ControlValueAccessor interface to provide developers the tools to seamlessly insert custom built components that can function just as a standard input. But as we will see, the ControlValueAccessor can provide much more than just custom inputs – it can serve as a gateway to better form organization and reusability within an application. Click to read…
9 – Reusable Sub-Forms
Angular’s reactive forms are a great way to quickly build complex and robust forms. However, the design tends to force developers to build large components that encompass the entire form and can make it challenging to re-use or nest sub-forms efficiently. By changing the way you think about sections of forms and leveraging ControlValueAccessor, it can become easy to build customizable form sections that can easily be shared between many forms in an application. This article heavily relies upon the information in the Control Value Accessor article, so take the time to review that information first. Click to read…
10 – RouterReuseStrategy – Maintaining Component State on Navigation
Angular provides a powerful built in router to emulate traditional web page navigation and display new components when accessing various URLs within the application. However, there are times that the default functionality can get in the or cause unexpected results in the application. There are various ways to work around some of this functionality, but wouldn’t it be great if we could have some control over the router to make some simple decisions that can greatly improve the functionality of our application without overhead? Let’s look at the RouteReuseStrategy class and find out! Click to read…
11 – Testing Complex Component Interaction
With all the techniques discussed in this series, they open up some challenges from a testing persepective. With some general understanding of the Angular module functionality, we can replace existing components with mock versions that can greatly simplify testing the interactions that arise using the techniques in this series. Click to read…
Smart vs. Dumb Components
Overview
In the same manner that the class is the basis of all object oriented applications, common design patterns set guidelines and expectations for different types of classes: models, services, repositories, factories, etc. Each type has a place and an expectation of how it should function in order to keep the structure of an application predictable, maintainable, and testable. The same mentality can be applied to the Angular Component. This article focuses on two main types of components, Smart and Dumb components , and how they can be used to build a more scalable, testable, and maintainable application.
A common mistake new Angular developers make is to treat a Component as a page in its entirety. The Angular router directly maps routes to pages, therefore it is a simple jump mentally to make that Component responsible for all of the content within that page. While this is generally a good place to start when developing, careful attention should be paid to the Component to continually refactor and break it into smaller chunks. This helps maintain a key Angular design principal: Components should have a single responsibility. Due to the way the Angular build process works, there is zero overhead during runtime with creating an additional component. This can be used to our advantage to decouple the application components into smaller, more easily maintained and tested parts. Enter the smart and dumb component structure.
What Are Smart vs. Dumb Components?
So what are smart and dumb components? You may hear many different names to refer to them (application, container, or controller vs presentation or pure components), but the idea is the same. You want components that are responsible for loading and manipulating data to not be intermingled with the presentation and display of that data.
The general guideline is that smart components act as your controller, binding service layer data to your dumb components through Inputs and Outputs.
Smart components have very little display logic, generally only rough framework or items tightly linked to the service layer (e.g. display a spinner while loading, then replace with a data table once data is loaded). The dumb components are responsible for the layout and interaction with the data (e.g. the data table in the previous example).
Example
Let’s take an example large component and break it down into these parts. Say we have a shopping site with a search page and a list of products matching the search. We also want to pre-populate the search using query params to allow sharing of search results via URL. The initial component may look something like the code below. Note I am using the Bootstrap library to provide some standard styling and display components.
search.component.ts
@Component({
selector: 'app-search',
template: `
<div class="row">
<div class="col">
<form [formGroup]="searchForm" [ngClass]="{'was-validated': attemptSearch}">
<div class="form-group">
<label for="searchTerm">Search</label>
<input type="text" class="form-control" id="searchTerm" formControlName="searchTerm" required />
</div>
<button class="btn btn-primary" type="submit" (click)="search()">Search</button>
</form>
</div>
<div class="col">
<ul class="list-group" *ngIf="items">
<li class="list-group-item" *ngFor="let item of items">
<h2>{{item.name}}</h2>
<p>{{item.description}}</p>
</li>
</ul>
<div *ngIf="searching" class="spinner-border"></div>
</div>
</div>`
})
export class SearchComponent implements OnInit {
public items: ItemInterface[];
public searching = false;
public searchForm: FormGroup;
public attemptSearch: boolean;
constructor(private formBuilder: FormBuilder, private searchService: SearchService, private route: ActivatedRoute) { }
ngOnInit() {
this.attemptSearch = false;
this.route.queryParams.subscribe(params => {
this.buildForm(params as SearchInterface);
});
}
private buildForm(init: SearchInterface) {
this.searchForm = this.formBuilder.group({
searchTerm: [ init.searchTerm, Validators.required ]
});
}
search() {
this.attemptSearch = true;
if (this.searchForm.valid) {
this.searching = true;
this.searchService.search(this.searchForm.value).subscribe(data => {
this.items = data;
this.searching = false;
});
}
}
}
As you can see this component can start to get fairly large. This also hasn’t implemented much in the way of display, form validation, or other design elements. We have a fair amount of coupling between our items, especially from running a search with the query parameters. Let’s see how we can start to break this apart.
Items
First off, let’s start with the display of the items. We can extract the display logic of the items into it’s own set of components:
search-results.component.ts
@Component({
selector: 'app-search-results',
template: `
<ul class="list-group" *ngFor="let item of items">
<li class="list-group-item">
<app-item [item]="item"></app-item>
</li>
</ul>`
})
export class SearchResultsComponent {
@Input() items: ItemInterface[];
}
item.component.ts
@Component({
selector: 'app-item',
template: `
<h2>{{item.name}}</h2>
<p>{{item.description}}</p>`
})
export class ItemComponent {
@Input() item: ItemInterface;
}
You can see here we have created two components, one responsible for the display of the individual item and one for handling a list of items. Take a moment to think about how this design becomes more re-usable (after all, we’ll probably display a item summary somewhere else), and easier to test. As the item grows and becomes more complex, it’s far simpler to test with an Input than mocking a server response to test corner cases.
This is also a good example to point out that the smart vs. dumb components don’t need to be a 1 to 1 relationship. If we imagine a shopping cart feature added to this application, the SearchResultsComponent is the ideal place to add in the related service dependency. It transforms to a smart wrapper component that is aware of the application as a whole and binds together interaction between features. In this case, selection or actions within the item can be translated to service level actions such as adding or removing from a shopping cart.
Search
The next candidate for extraction is the search form. Search forms can have many complicated internal interactions with field validations, conditional form fields, validation messages, etc. But ultimately, forms are easily extractable into a black box with a default options input, and a form submission output. Let’s see how we can accomplish this:
search-form.component.ts
@Component({
selector: 'app-search-form',
template: `
<form [formGroup]="searchForm" [ngClass]="{'was-validated': attemptSearch}">
<div class="form-group">
<label for="searchTerm">Search</label>
<input type="text" class="form-control" id="searchTerm" formControlName="searchTerm" required />
</div>
<button class="btn btn-primary" type="submit" (click)="search()">Search</button>
</form>`
})
export class SearchFormComponent implements OnInit {
@Input() default: SearchInterface;
@Output() search: EventEmitter<SearchInterface> = new EventEmitter<SearchInterface>();
public searchForm: FormGroup;
public attemptSearch: boolean;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.attemptSearch = false;
this.searchForm = this.formBuilder.group({
searchTerm: [ this.default.searchTerm, Validators.required ]
});
}
searchClick() {
this.attemptSearch = true;
if (this.searchForm.valid) {
this.search.emit(this.searchForm.value);
}
}
}
Here we have pulled out the search form and provide an Input for default values, and an Output that emits an event containing the search criteria upon a valid search request. Again, this becomes far more testable and maintainable. It’s isolated from other functionality, so adding validation and other search fields becomes straightforward and contained within the functionality of the form. We also get the ability to perform far easier and accurate testing through the Input and Output. We can directly manipulate the form and test if the search event is emitted, no need for mocking interaction with other services or components.
Bringing It Together
Now we can update our initial component to bring all these components together:
search.component.ts
@Component({
selector: 'app-search',
template: `
<div class="row">
<div class="col">
<app-search-form [default]="init" (search)="search($event)"></app-search-form>
</div>
<div class="col">
<div *ngIf="items">
<app-search-results [items]="items"></app-search-results>
<div>
<div *ngIf="searching" class="spinner-border"></div>
</div>
</div>`
})
export class SearchComponent implements OnInit {
public init: SearchInterface;
public items: ItemInterface[];
public searching = false;
constructor(private searchService: SearchService, private route: ActivatedRoute) { }
ngOnInit() {
this.route.queryParams.subscribe(params => {
this.init = params as SearchInterface;
});
}
search(criteria: SearchInterface) {
this.searching = true;
this.searchService.search(criteria).subscribe(data => {
this.items = data;
this.searching = false;
});
}
}
As you can see, this component has maintained nearly all of the dependencies and is responsible for understanding where it is in the application. This is acceptable because we will never re-use this component, it’s responsibility is to be a router endpoint and handle the logic at that specific location. It also binds the service layer to the actions taken within the application, but has no concern for the implementation of that action. It doesn’t matter if a selection is made with a button click, a drag-and-drop, gestures, or any other input method. The dumb components are responsible for translating user actions into an interface that is easy to predict and understand – and also very importantly is already validated. The smart component is responsible for reacting to those actions however they are made, and triggering actions throughout the application.
Note About Layout
In a Bootstrap-like grid system, this is generally the best location to put the grid elements for a page as a whole. This allows for a much easier to see overall application layout.
Is the page a stacked design with the search on top of the results? Is it a pinned side bar and scrolling results? This is not to say the dumb components have no layout responsibility – they are responsible for how that component appears in many situations. That is to say they should be a responsive design to appear in any of the previously mentioned situations. By having the form component reactive to switch layout of the inputs, it’s easy for the smart component to place it in any of the various locations and with different set widths (e.g. full width on top, or a narrow pinned side bar). This separation of responsibility helps maintain a complete plug and play type of architecture and greatly reduces the effort of any redesign.
Smart vs. Dumb Components
Conclusion
Next Topic – 2 of 11: Observables – Keeping Things Clean and Clear
Observables: Keeping Things Clean and Clear
Overview
The core concept in the RxJS library is the Observable. Observables are streams of data used to inform various parts of the application of a change that may be relevant to that functionality. This article discusses some best practices, some opinionated ideas, and pitfalls to be aware of that are not always immediately obvious when working with Observables.
Review
Let’s start with some review on Observables. If anything isn’t familiar, I suggest reviewing the Angular Observables documentation for a more thorough overview on the basics of Observables.
The common use pattern for an Observable is to provide updates as changes happen elsewhere in the application. A prime example of this would be the current user of the application. A user / authentication service can provide a user Observable that will emit a new value if the user logs out or a new user logs in. Any part of the application dependent on user settings can react to the change and make the necessary updates. As a user change could happen through a modal window, it’s a prime example of a change that can occur without any routing or other actions to trigger a change.
Below is a basic implementation of this concept.
@Component({
selector: 'app-greeting',
template: '<div>Hello, {{user.name}}</div>'
})
export class GreetingComponent implements OnInit {
public user;
constructor(private userService: UserService) {}
ngOnInit(): void {
this.userService.currentUser().subscribe(newUser => this.user = newUser);
}
}
Clean Up After Yourself
Observables have a potential problem. Let’s take the example above, what happens when you route to a new page and create a new component? Every time a user logs in, the new user information is still stored and processed by the GreetingComponent because the subscription is still active. Now what happens if it’s something with a much larger data set, or more frequent updates? This is a memory leak that can cause bugs and crashes that are very difficult to trace and predict.
Demonstrating a Leak
We can demonstrate the issue of a memory leak with a few changes to the example above. First, we can create a dummy service that emits a continual stream of data by emitting a new random name every two seconds:
export interface User {
name: string;
}
const names: string[] = [
'Charlie Brown',
'Darth Vader',
SNIP...
];
@Injectable({
providedIn: 'root'
})
export class UserService {
currentUser(): Observable<User> {
return interval(2000).pipe(map(this.getRandomUser));
}
getRandomUser(): User {
const index = Math.floor(Math.random() * names.length);
return {
name: names[ index ]
};
}
}
To see the issue we can add a log statement every time the data changes in the greeting component:
ngOnInit(): void {
this.userService.currentUser().subscribe(newUser => {
this.user = newUser;
console.log(newUser);
});
}
We can see the data updates in the view and we can see a logged message in the console for each historical value. This is exactly what we would expect, but what happens when we navigate away from the component? When navigating elsewhere you can see the console messages keep rolling in!
What is even worse, is if we return to the greeting component it won’t grab the existing stream, it creates a new subscription. Navigate away and back a few times and you can see the data rolling in at a high speed!
Avoiding Leaks
To avoid a memory leak, we can unsubscribe the the Observable and stop processing any updates. There are many articles and methodologies for handling this that you can review to accomplish this, however it can add a fair amount of similar code to every subscription. There are a few things to keep in mind that can reduce the boilerplate in handling Observables:
- Limit subscriptions – see section 1 – Smart vs. Dumb Components. By structuring you app into smart and dumb components, you can reduce the number of subscriptions and convert them to Inputs. If only your high level controller components need to subscribe then the issue is drastically reduced.
- Use the Async Pipe – the Angular async pipe can be used to subscribe to a promise from the view. The async pipe handles subscriptions and will unsubscribe for you, so if you only need data in the view then you don’t need to subscribe in the component.
- Use a consistent method – create a reusable component that handles most of the work for you so you don’t need to write the same boilerplate every time. Create a base class that provides a core that can be used to unsubscribe and provides an Observable that fires and completes in the OnDestroy hook that can be used to unsubscribe when leaving a component.
In order to fix the memory leak, we can implement logic to unsubscribe when destroying a component. Using the method described in the linked post (without using an abstract class to allow for more clarity), we can create our own short lived Observable that triggers an unsubscribe using the takeUntil method:
export class GreetingComponent implements OnInit, OnDestroy {
public user;
public unsubscribe = new Subject<any>();
constructor(private userService: UserService) { }
ngOnInit(): void {
this.userService.currentUser()
.pipe(takeUntil(this.unsubscribe))
.subscribe(newUser => {
this.user = newUser;
console.log(newUser);
});
}
ngOnDestroy(): void {
this.unsubscribe.next();
this.unsubscribe.complete();
}
}
Now when testing, the logging stops when navigating away and picks back up when we go back to the component.
A Problem with Observables
NOTE: This section is completely my opinion, you can find many discussions that argue against this practice with valid points which I will mention, but this has been my findings with my experience using Angular. The most important concept is to use consistency in an application to semantically communicate expected functionality to improve the quality of code.
Observables represent a continual stream of information. The have many benefits, including delayed execution – if nobody is subscribed, the action isn’t fired. But let’s take a look at an example that is slightly modified from the one above:
@Component({
selector: 'app-greeting',
template: '<div>Hello, {{user.name}}</div>'
})
export class GreetingComponent implements OnInit {
public user;
constructor(private userService: UserService) {}
ngOnInit(): void {
this.userService.currentUser().subscribe(newUser => this.user = newUser);
this.userService.getUserPreferences().subscribe(prefs => console.log(prefs));
}
}
If you look at the code above, you would expect the component to get new user information, and new user preferences when a new user logs in. However, when running this code you find that the greeting changes, but you never see the new user preferences loaded. You gnash your teeth and bang your head on the desk looking for the mistake until you finally look at the UserService:
@Injectable()
export class UserService {
private _user = new BehaivorSubject(null);
constructor(private authService: AuthService, private http: HttpClient) {}
currentUser(): Observable<User> {
return this._user.asObservable();
}
login(credentials) {
if (this.authService.validate(credentials)) {
this._user.next(credentials.userName);
}
}
getUserPreferences(): Observable<UserPreference> {
return this.http.get(/api/userPreferences/</span>${<span>this</span>.<span>_user</span>.getValue()}<span>); } }
While this example is a bit contrived and would be more in depth, the point remains the same. One observable is updated continually with new information, but one is a single execution http request. There is nothing about the service API that helps communicate this, so you are left with the need to view the underlying implementation to understand the results. So the question becomes, can we fix this to help communicate the functionality to any consumer of the service? The culprit here is the getUserPreferences method, let’s change it:
getUserPreferences(): Promise<UserPreference> {
return this.http.get(`/api/userPreferences/${this._user.getValue()}`).toPromise();
}
NOTE: The toPromise method is being deprecated in future versions of RxJS in lieu of the lastValueFrom function
This is a small change with a big impact. It clearly helps communicate this is a single value return. By maintaining this convention of single values use Promises, multiple value streams use Observables we can greatly improve the understanding of underlying implementation with very little loss of functionality or additional code. There certainly are other improvements that can be made, such as a better name, using a parameter instead of the current user value, etc. that could also help, but the use of a Promise is the single most effective communication tool on the intent of this method.
It now becomes clear how this can and should be consumed in the component:
@Component({
selector: 'app-greeting',
template: '<div>Hello, {{user.name}}</div>'
})
export class GreetingComponent implements OnInit {
public user;
constructor(private userService: UserService) {}
ngOnInit(): void {
this.userService.currentUser().subscribe(newUser => {
this.user = newUser;
this.userService.getUserPreferences().then(prefs => console.log(prefs));
});
}
}
We know that the Promise is a single return and we want to get that information whenever a user changes, so we can call the method when we need the new information.
We also get a side benefit from this. As mentioned in the previous section, we need to unsubscribe from Observables. This isn’t the case for http calls from HttpClient, but how is any consumer of a service supposed to know that? This convention helps to clearly lay out when Observables need to be unsubscribed and also reduces the need of extraneous unsubscribes when they aren’t necessary.
The downside and counter argument to this approach is Promises have less functionality than Observables, specifically in regards to the pipable functions, and I will agree with that point. There is certainly an argument for consistently using Observables in an application for this reason and also to reduce the need to learn an addtional API. Ultimately the decision depends on the application needs, but one thing is true no matter the approach – find a way that works and stick to it consistently. An application that has a reliable method of communicating functionality – be it naming convention, inference by type, or comments – is a far easier application to understand and maintain.
Observables: Keeping Things Clean and Clear
Conclusion
Next Topic – 3 of 11: The Async Pipe
The Async Pipe
Overview
To follow along with the articles there a two repositories created: a starting point and final solution.
Without the Async Pipe
To see what the async pipe provides, we will start with an example component that does not use the pipe. This component calls a service to subscribe to an Observable which it displays directly, an subscriptions that needs modification before display, and a Promise. First a look at the service:
@Injectable({
providedIn: 'root'
})
export class SampleService {
public sampleObservable(): Observable<number> {
return interval(1000);
}
public samplePromise(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => resolve('Hello from promise!'), 1500);
});
}
}
This service creates an Observable using the interval method. This fires a counter value every time the interval provided elapses. So by providing 1000 (the parameter is milliseconds), we will get a new counter value every second. The Promise that is generated is resolved with static text message after a 1500 millisecond timeout.
Now to look at our starting point component:
@Component({
selector: 'app-manual-subscribe',
template: `
<h3>Manual Subscribe Data Load</h3>
<div>Seconds since subscribe: {{counter}}</div>
<div>Time at update: {{updateTime | date:'medium'}}</div>
<div>Promise result: {{message}}</div>`
})
export class ManualSubscribeComponent implements OnInit, OnDestroy {
public counter: number;
public message: string;
public updateTime: Date;
private unsubscribe = new Subject<void>();
constructor(private sampleService: SampleService) { }
ngOnInit(): void {
this.sampleService.sampleObservable()
.pipe(takeUntil(this.unsubscribe))
.subscribe(data => this.counter = data);
this.sampleService.sampleObservable()
.pipe(takeUntil(this.unsubscribe))
.subscribe(() => this.updateTime = new Date());
this.sampleService.samplePromise()
.then(data => this.message = data);
}
ngOnDestroy(): void {
this.unsubscribe.next();
this.unsubscribe.complete();
}
}
As you can see this is a fairly large component for the limited functionality we are providing due to the necessary boilerplate. We have two subscriptions in the ngOnInit function – one to bind the new counter value and one to calculate a new date every second to display. These subscriptions are managed as described in Observables – Keeping Things Clean and Clear by using the takeUntil method that triggers an unsubscribe in ngOnDestroy. We also bind the result of the promise to a message property to display. Below we can see the results of this component before and after the async methods resolve.
Using the Async Pipe
With this starting point in place, we can bring in the async pipe to help clean everything up for us. The async pipe is used in a template on any async method or property and Angular will handle the subscription, data binding, and unsubscribe for that reference. So the first step is to provide a handle to the async objects instead of a static data value. Below are the changes to the body of the component:
export class AsyncLoadComponent implements OnInit {
public counter$: Observable<number>;
public updateTime$: Observable<Date>;
public message$: Promise<string>;
constructor(private sampleService: SampleService) { }
ngOnInit(): void {
this.counter$ = this.sampleService.sampleObservable();
this.updateTime$ = this.sampleService.sampleObservable().pipe(map(() => new Date()));
this.message$ = this.sampleService.samplePromise();
}
}
There are a few specifics to note about the changes:
- ngOnDestroy and the associated unsubscribe subject are gone – Angular handles the unsubscribe for us so we don’t need to worry about it
- We declare Observable/Promise properties as a handle to provide the template to the async result. You may often see async properties with the trailing $ on the variable name as a convention. This is not required but can be a helpful convention to communicate synchronous vs. asynchronous values
- We need to add a map function to the updateTime$ Observable to transform the data instead of transforming in the subscribe. Here we don’t need the value from the Observable but it is available just as in the subscribe method. We can pipe any function we want into the Observable stream to modify it before binding including map, filter, debounceTime, etc.
With these changes, we can implement the async pipe in the template:
<h3>Async Data Load</h3>
<div>Time since subscribe: {{counter$ | async}}</div>
<div>Time at update: {{updateTime$ | async | date:'medium'}}</div>
<div>Promise result: {{message$ | async}}</div>
All that is necessary is to add the async pipe in the data binding and Angular handles the rest. You can also see in the updateDate$ binding that we can chain pipes to still provide date formatting – just be aware that order matters as data is transformed through the pipes. The async pipe is also not limited to template binding, but can be used in binding data to child component inputs, directives, or anywhere else that you would normally bind data. We can test these changes and see that we get the exact same results as our starting point:
Async and NgIf
Often it is desirable to show some loading indicator while we wait for an async value to resolve. Let’s start with a trimmed down version of the component we created by looking at only an Observable and Promise. We only want to display the associated text once the first data loads so we need to add some ngIf statements on the wrapper divs to hide them until we have some data:
@Component({
selector: 'app-async-if',
template: `
<h3>Async Data Load With If</h3>
<div *ngIf="counter$ | async">Time since subscribe: {{counter$ | async}}</div>
<div *ngIf="message$ | async">Promise result: {{message$ | async}}</div>`
})
export class AsyncIfComponent implements OnInit {
public counter$: Observable<number>;
public message$: Promise<string>;
constructor(private sampleService: SampleService) { }
ngOnInit(): void {
this.counter$ = this.sampleService.sampleObservable();
this.message$ = this.sampleService.samplePromise();
}
}
If we test, we can see that this works as expected:
However, this approach now means we are managing 4 separate async operations instead of referencing the result twice – once in the ngIf and once in the template data binding. Here is where Angular provides extra functionality for ngIf to store the results of the conditional statement in a template variable for future use:
<div *ngIf="counter$ | async as result">Time since subscribe: {{result}}</div>
<div *ngIf="message$ | async as result">Promise result: {{result}}</div>
Test Scope: {{result}}
All that is needed is to provide “as <variable>” in the conditional statement to create the binding. Let’s test this and discuss a few important points to consider:
- Note that both bindings use the same value which is also used later as a test. The data binding is scoped to within the parent element governed by the ngIf statement. Best practice would include better results names for the data to prevent confusion, but this is to demonstrate that there are not collisions between these template variables that are created. Data also does not extend outside of the parent div, so if it is needed in multiple places you will need to place the binding at a higher level or create multiple subscriptions.
- This pattern is also helpful to avoid the need for the safe-navigation-operator (?.) when the resulting value is an object. The data will only be rendered once the object is defined so we don’t need as much null handling while waiting for data to load.
Finally, we can also implement another helpful ngIf feature by providing an alternative template to display when the data has not yet loaded. We can do this by creating an ng-template and giving the ngIf directive a reference to the template as the else statement. The Content Projection vs. Inputs article discusses ng-template in more detail. Below you can see we create a spinner template that we will show in place of each data element while the data is loading. Once loaded, we will then show the async results.
<h3>Async Data Load With If</h3>
<div *ngIf="counter$ | async as result else spinner">Time since subscribe: {{result}}</div>
<div *ngIf="message$ | async as result else spinner">Promise result: {{result}}</div>
<ng-template #spinner>
<div class="spinner-border"></div>
</ng-template>
The Async Pipe
Conclusion
As you can see, the async pipe – especially when coupled with ngIf – can provide a high level of functionality for a common UI pattern with very little code. The async pipe provides a clean interface, automatically follows best practices, and works very well with other Angular functionality. While there still are many times in which manually handling subscriptions can be easier or cleaner, you’ll find that the async pipe fits very nicely when using the Smart vs. Dumb Components design pattern.
Next Topic – 4 of 11: Content Projection vs. Inputs
Content Projection vs. Inputs
Overview
To follow along with the articles there a two repositories created: a starting point and final solution.
Display Components
For this article, let’s start by building a simple display component: a card. Cards are simply data displayed within a bordered box to help segregate the content. Below is a screenshot of a simple usage of this from the Bootstrap documentation. Let’s take this and make an Angular component!
We want to create a component that will apply the classes for us. Let’s start with a simple component and populate the body of the card using an Input.
@Component({
selector: 'app-input-card',
template: `
<div class="card">
<div class="card-body">
{{bodyText}}
</div>
</div>`
})
export class InputCardComponent {
@Input() bodyText: string;
}
<app-input-card bodyText="This is some text within a card body."></app-input-card>
<div class="card">
<div class="card-header">
A sample card
</div>
<div class="card-body">
This is some text within a card body.
</div>
</div>
Let’s wrap up the initial design by adding an input for the heading text:
@Component({
selector: 'app-input-title-card',
template: `
<div class="card">
<h5 class="card-header">
{{headerText}}
</h5>
<div class="card-body">
{{bodyText}}
</div>
</div>`
})
export class InputTitleCardComponent {
@Input() bodyText: string;
@Input() headerText: string;
}
Adding Content Projection
<app-project-body-card headerText="A sample card">This is some text within a card body.</app-project-body-card>
As you can see, we no longer have an input for the body. Instead we use the element as a wrapper around some content. This let’s us add whatever markup we need to the body. Below is an example usage and the resulting display.
<app-project-body-card headerText="A sample card">
Now I <strong>can</strong> add <em>markup!</em >
<span class="badge badge-info">And Other Elements</span>
</app-project-body-card>
To embed the content, we can use the ng-content element to tell Angular where to place the projected content. Angular takes the DOM contents from within the component tags and moves them into the template into the ng-content tags. All we need to do it replace the template interpolation {{bodyText}} with <ng-content></ng-content>.
@Component({
selector: 'app-project-body-card',
template: `
<div class="card">
<h5 class="card-header">
{{headerText}}
</h5>
<div class="card-body">
<ng-content></ng-content>
</div>
</div>`
})
export class ProjectBodyCardComponent {
@Input() headerText: string;
}
Multiple Projections
<app-project-multiple-card>
<div header>Projected Heading <button class="btn btn-primary">With buttons!</button></div>
<div body>
Now I <strong>can</strong> add <em>markup!</em >
<span class="badge badge-info">And Other Elements</span>
</div>
</app-project-multiple-card>
This is not exactly what we are looking for, everything is still being inserted into the single <ng-content> tag. Let’s try adding another tage to see if that helps us split up the values:
@Component({
selector: 'app-project-multiple-card',
template: `
<div class="card">
<h5 class="card-header">
<ng-content></ng-content>
</h5>
<div class="card-body">
<ng-content></ng-content>
</div>
</div>`
})
export class ProjectMultipleCardComponent { }
Oddly enough, we get the exact same result. What is happening here? As I mentioned before, content projection takes DOM content within the element and moves it to the location of the ng-content. However, when I add two ng-content tags, Angular won’t create DOM elements to duplicate the content – instead the content keeps getting moved around which leaves the first ng-content empty. This is important to remember when working with projected content, especially if getting into situations where the content is hidden or displayed conditionally with ngIf or display attributes. Furthermore, the content projection is evaluated before the parent ngIf. Even if you wrap the ng-content with an ngIf, the content still gets placed before the ngIf executes, and then is removed when ngIf runs. More on dealing with that situation later.
What we need is a way to tell Angular how to pick which content that should go in which location. To accomplish this, we can use the select directive on the ng-container element. We can provide any valid CSS selector to use and Angular finds the matching content to project so we will use “[header]” and “[body]” to identify the content based on the dummy attributes that were added earlier.
@Component({
selector: 'app-project-multiple-card',
template: `
<div class="card">
<h5 class="card-header">
<ng-content select="[header]"></ng-content>
</h5>
<div class="card-body">
<ng-content select="[body]"></ng-content>
</div>
</div>`
})
export class ProjectMultipleCardComponent { }
Success! We can now move the desired content where we need it to go and we have the ability to add markup to both locations. This method works great for any predictable structure (for example a input wrapper component that expects a label and an input), but since we are creating arbitrary distinctions, we would be better off implementing a few components or directives. We will create some components here to demonstrate some further benefits.
@Component({
selector: 'app-card-heading',
template: <div [ngClass]="headingClass()"><ng-content></ng-content></div>
})
export class CardHeadingComponent {
@Input() textStyle: string;
public headingClass() {
return `text-${this.textStyle}`;
}
}
@Component({
selector: 'app-card-body',
template: '<ng-content></ng-content>'
})
export class CardBodyComponent { }
@Component({
selector: 'app-project-component-card',
template: `
<div class="card">
<h5 class="card-header">
<ng-content select="app-card-heading"></ng-content>
</h5>
<div class="card-body">
<ng-content select="app-card-body"></ng-content>
</div>
</div>`
})
export class ProjectComponentCardComponent { }
<app-project-component-card>
<app-card-heading textStyle="primary">
Projected Heading <button class="btn btn-primary">With buttons!</button>
</app-card-heading>
<app-card-body>
Now I <strong>can</strong> add <em>markup!</em >
<span class="badge badge-info">And Other Elements</span>
</app-card-body>
</app-project-component-card>
We still have everything going where we need to go, but we also get the added control of styling our header with an input.
Conditional Projection
Now we can go back to the problem discussed earlier regarding conditional statements associated with the content. What if we want the option to move the header content to be the footer instead? This type of situation can arise when creating reactive layouts – we may want our styling to look different on mobile so we need to conditionally make changes to the structure of our content. For this example I’ll control this through an input on the component, but it could just as easily be an injected service to the component. In order to place the same content in multiple potential locations, you will need to enlist the help of some other Angular elements: ng-template and ng-container. We can project the content into a template and use conditional statements to display this where we need it. So what are ng-template and ng-container, and how are they different than ng-content?
<ng-template>
The Angular ng-template element is based off of the HTML <template> element. From MDN:
The HTML Content Template (
<template>
) element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript.Think of a template as a content fragment that is being stored for subsequent use in the document. While the parser does process the contents of the
<template>
element while loading the page, it does so only to ensure that those contents are valid; the element’s contents are not rendered, however.
For Angular’s ng-template, this goes a step further. If we place content into ng-template, it won’t even be included in the DOM, it gets replaced by a comment – that is until we include a structural directive to tell Angular what to do with the content.
<div *ngIf="!showTemplate else myTemplate">
I appear when showTemplate is false
</div>
<ng-template #myTemplate>
I appear when showTemplate is true
</ng-template>
This is an example of a common use case for a template, the else statement of ngIf. NgIf will render the element it’s on when true, otherwise we can tell Angular to use another template. This can be an effective way to use a loading animation in place of content while it’s loading, you can use the same template (e.g. a spinner) in place of different sections while the content loads. The contents can be replicated as needed, so you don’t have any restriction to show one template at a time.
<ng-container>
The Angular ng-container element is another element that doesn’t represent anything in the DOM, but it’s contents are always rendered. It’s a useful container to use as a wrapper around elements and you have some reason to not use a DOM element such as a div to hold it. An example usage of this is to address a limitation where you are not allowed to have multiple structural directives on the same element. Let’s say you want to loop through some elements but only display them if a display property is true.
<!-- This will throw an error and is not valid in Angular -->
<div *ngFor="let item of items" *ngIf="item.display">{{item.text}}</div>
<!-- This is legal and does what we want but does not create any nested elements -->
<div *ngFor="let item of items" >
<ng-container *ngIf="item.display">{{item.text}}</ng-container>
</div>
<ng-container *ngTemplateOutlet="template"></ng-container>
<ng-template #template>
Hello!
</ng-template>
Bringing All Three Together
@Component({
selector: 'app-multiple-locations-card',
template: `
<ng-template #content>
<ng-content select="app-card-heading"></ng-content>
</ng-template>
<div class="card">
<h5 class="card-header">
<ng-container *ngTemplateOutlet="isHeader ? content : null"></ng-container>
</h5>
<div class="card-body">
<ng-content select="app-card-body"></ng-content>
</div>
<div class="card-footer">
<ng-container *ngTemplateOutlet="!isHeader ? content : null"></ng-container>
</div>
</div>`
})
export class MultipleLocationsCardComponent {
@Input() isHeader: boolean;
}
We can now dynamically change the location of where the content goes. To emulate this, we’ll add a button to toggle a boolean value that we can pass into the input.
<button class="btn btn-info" (click)="contentLocation = !contentLocation">Toggle</button>
<div class="m-3"></div>
<app-multiple-locations-card [isHeader]="contentLocation">
<app-card-heading>My Title</app-card-heading>
<app-card-body>Some content</app-card-body>
</app-multiple-locations-card>
Content Projection vs. Inputs
Conclusion
Next Topic – 5 of 11: ViewChild and ContentChild
ViewChild and ContentChild
Overview
This article aims to provide a high level overview of the functionality provided just to give a taste of what is possible. There are many various ways to use these features and much of that will be left to the imagination and as an exercise to the reader. Reviewing the Angular documentation on these decorators is a good place to start. Usage of these tools is often so highly situational that makes providing examples relevant to the situations you will likely face is quite challenging. Awareness of the capability as a whole is often the major hurdle needed to investigate the implementation for a specific need.
To follow along with the articles there a two repositories created: a starting point and final solution.
Child vs. Children
ViewChild
Selection Types
The ViewChild decorator gives the ability to obtain a reference to an item in the template of a component. By providing a selector to the decorator, Angular will search for and bind a property on the component to the reference found by the selector. As mentioned previously, there are many different situations where this can be used. This is further highlighted by the list of valid selectors that can be used – from the Angular documentation:
The following selectors are supported.
- Any class with the
@Component
or@Directive
decorator
- A template reference variable as a string (e.g. query
<my-component #cmp></my-component>
with@ViewChild('cmp')
)
- Any provider defined in the child component tree of the current component (e.g.
@ViewChild(SomeService) someService: SomeService
)
- Any provider defined through a string token (e.g.
@ViewChild('someToken') someTokenVal: any
)
- A
TemplateRef
(e.g. query<ng-template></ng-template>
with@ViewChild(TemplateRef) template;
)
We will focus on the most commonly used selector, the component selector.
Sample Setup
Let’s set up an example to demonstrate how the decorator is used. The first thing we need is a child element – for this example we will create a wrapper element for a list group that allows for the selection of some elements:
export interface ListItemInterface {
display: string;
isSelected: boolean;
}
@Component({
selector: 'app-child-list',
template: `
<div class="list-group">
<div
class="list-group-item list-group-item-action"
*ngFor="let item of items; let i = index"
[ngClass]="{'active': item.isSelected}"
(click)="toggleItem(i)"
>{{item.display}}</div>
</div>
`,
styles: [`
.list-group-item-action {
color: inherit;
}
`]
})
export class ChildListComponent implements ListComponent {
@Input() items: ListItemInterface[];
public toggleItem(i: number): void {
this.items[i].isSelected = !this.items[i].isSelected;
}
}
@Component({
selector: 'app-view-child',
template: `<app-child-list [items]="items"></app-child-list>`
})
export class ViewChildComponent {
public items: ListItemInterface[] = [
{display: 'Apple', isSelected: false},
{display: 'Orange', isSelected: false},
{display: 'Banana', isSelected: false},
{display: 'Kiwi', isSelected: false},
{display: 'Lemon', isSelected: false}
];
}
Implementing ViewChild
For the sake of our example, let’s say we want to select items in the list component from outside of the component. We could use a service to interact with the data, but there are times that may be challenging – for example, what if the list component came from a third party library? With ViewChild, we can get a reference to the component class and directly interact with the component. First, we need to add the ViewChild decorator to our parent level component:
export class ViewChildComponent {
public items: ListItemInterface[] = ..SNIP
@ViewChild('list') listComponent: ListComponent;
}
Note: the binding of the ViewChild may be available in the ngOnInit life-cycle hook, but it is not guaranteed and should not be relied upon. Angular provides the ngAfterViewInit life-cycle hook that fires after the ViewChild binding takes place – so any actions that happen during the load of the component should take place here.
We can now interact with any property or method on the instance of the ChildListComponent. Let’s add a method to remotely select an item and bind that to a list of buttons we create:
@Component({
selector: 'app-view-child',
template: `
<div class="btn-group pb-5">
<button class="btn btn-secondary"
*ngFor="let item of items; let i = index"
(click)="toggleItem(i)">
{{item.display}}
</button>
</div>
<app-child-list [items]="items"></app-child-list>`
})
export class ViewChildComponent {
public items: ListItemInterface[] = ...SNIP
@ViewChild('list') listComponent: ListComponent;
public toggleItem(index: number): void {
this.listComponent.toggleItem(index);
}
}
A Note on Abstraction
The downside of this approach is that we have now created a tight coupling between the two components. While there are times when the components are so intrinsically linked that this isn’t a problem (e.g. a form group wrapper and a form control). But when the two components are not so tightly linked, it can be beneficial to use an interface to control what methods are available to other components. As all methods on a component that need to be accessible to the template must be public, this can create a situation where a method is changed and cause unexpected results throughout the application. For our example, let’s replace the list implementation with a new component that uses a table instead.
First, we create an interface we can implement with our child components that let us remotely toggle the selected items.
export interface ListComponent {
toggleItem(index: number): void;
}
<app-child-list [items]="items" #list></app-child-list>
@ViewChild('list') listComponent: ListComponent;
@Component({
selector: 'app-child-table',
template: `
<table class="table table-hover">
<tbody>
<tr *ngFor="let item of items; let i = index">
<td [ngClass]="{'bg-success': item.isSelected}" (click)="toggleItem(i)">{{item.display}}</td>
</tr>
<tbody>
</table>
`
})
export class ChildTableComponent implements ListComponent {
@Input() items: ListItemInterface[];
public toggleItem(i: number): void {
this.items[i].isSelected = !this.items[i].isSelected;
}
}
<app-child-table [items]="items" #list></app-child-table>
While this is overkill for this situation, it’s a useful pattern to keep in mind to help control interaction between components when directly accessing them. Where I have really seen this approach shine is in combination with Dynamic Component Generation to create some powerful user interfaces!
ContentChild
Selection Types
ContentChild is similar in function and usage to ViewChild – the major difference is it is used to select elements within the projected content of a component. If you are not familiar with content projection, see the article: 4 – Content Projection vs. Inputs. ContentChild has some additional restrictions to ViewChild in the types of selectors that can be used. It only supports Component or Directive types to be used as a selector.
Sample Setup
To demonstrate the usage of ContentChild, we will need to create a few items. First, a simple directive to use as a selector:
@Directive({
selector: '[appLabel]'
})
export class ControlLabelDirective { }
@Component({
selector: 'app-control-formatter',
template: `<ng-content></ng-content>`
})
export class ControlFormatterComponent { }
<app-control-formatter>
<div class="form-group">
<label for="exampleInput" appLabel>A field label</label>
<input type="text" class="form-control" id="exampleInput">
</div>
</app-control-formatter>
ContentChild Implementation
Our goal is for the wrapper component to implement some formatting on the label element inside of it. To start, we need to add the ContentChild decorator targeting the ControlLabelDirective we created and applied to the label element:
@Component({
selector: 'app-control-formatter',
template: `<ng-content></ng-content>`
})
export class ControlFormatterComponent {
@ContentChild(ControlLabelDirective) labelRef: ControlLabelDirective;
}
@Component({
selector: 'app-control-formatter',
template: `<ng-content></ng-content>`
})
export class ControlFormatterComponent implements AfterContentInit {
@ContentChild(ControlLabelDirective) labelRef: ControlLabelDirective;
constructor(private renderer: Renderer2) { }
ngAfterContentInit(): void {
this.renderer.addClass(this.labelRef.nativeElement, 'text-info');
}
}
@Component({
selector: 'app-control-formatter',
template: `<ng-content></ng-content>`
})
export class ControlFormatterComponent implements AfterContentInit {
@ContentChild(ControlLabelDirective, {read: ElementRef}) labelRef: ElementRef;
constructor(private renderer: Renderer2) { }
ngAfterContentInit(): void {
this.renderer.addClass(this.labelRef.nativeElement, 'text-info');
}
}
<app-my-component myDirective #myElement></app-my-component>
We can now test this out and see the results:
As you can see, the ‘text-info’ class has been applied to the label. As this example shows, ContentChild is often utilized to help control formatting or DOM manipulation when a component or directive expects a specific structure. While this example applies the class at startup, this could easily be extended to change the class based on form validation errors, animations when selecting or interacting with the input, etc.
ViewChild and ContentChild
Conclusion
Next Topic – 6 of 11: Dynamic Component Generation
Dynamic Component Generation
Overview
To follow along with the articles there a two repositories created: a starting point and final solution.
Sample Environment Setup
export interface ComponentRequest {
contextType: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
componentType: 'button' | 'alert' ;
viewPort: number;
}
@Component({
selector: 'app-button',
template: `
<button class="btn" [ngClass]="getClass()" (click)="remove.emit()">
Created: {{created | date:'medium'}}
</button>`
})
export class ButtonComponent implements OnInit {
@Input() type: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
@Output() remove = new EventEmitter<any>();
public created: Date;
ngOnInit(): void {
this.created = new Date();
}
getClass(): string {
return !this.type ? null : `btn-${this.type}`;
}
}
@Component({
selector: 'app-alert',
template: `
<span class="alert d-flex align-items-center justify-content-between my-2 p-1" [ngClass]="getClass()">
{{created | date:'medium'}}
<button type="button" class="close" (click)="remove.emit()">
<span aria-hidden="true">×</span>
</button>
</span>`
})
export class AlertComponent implements OnInit {
@Input() type: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
@Output() remove = new EventEmitter<any>();
public created: Date;
ngOnInit(): void {
this.created = new Date();
}
getClass(): string {
return !this.type ? null : `badge-${this.type}`;
}
}
@Component({
selector: 'app-dynamic-component-creation',
template: `
<div class="row">
<app-create-request class="col"
[viewPorts]="viewPorts"
(createComponent)="addToViewport($event)">
</app-create-request>
</div>
<div class="row card-deck justify-content-between">
<div class="col-6" *ngFor="let port of viewPorts">
<div class="card mt-3">
<div class="card-header">
<h3>View Port #{{port}}</h3>
</div>
<div class="card-body">
<!-- TODO: Components go here -->
</div>
</div>
</div>
</div>`
})
export class DynamicComponentCreationComponent{
public viewPorts: number[] = [1, 2, 3, 4];
public addToViewport(request: ComponentRequest): void {
//TODO
}
}
Prepping for Component Creation
Entry Components
There are two types of components within an Angular application: declarative and entry components. Declarative components are what you are most used to dealing with – they are generated by declaring a reference to the selector in a template. Entry components are imperatively loaded by type. There are two ways in which you previously have interacted with entry components, most likely without knowing it:
- The root application bootstrapped component – generated during the bootstrap process and loaded into the DOM as the application starts
- Routed components – dynamically created by the router and loaded into the DOM in the router-outlet
For a component to be declared as an entry component, it must be added to the entryComponents array in the module. Below is the module definition for our sample where you can see the entryComponents definition:
@NgModule({
declarations: [
DynamicComponentCreationComponent,
CreateRequestComponent,
ButtonComponent,
AlertComponent
],
imports: [
CommonModule,
FormsModule
],
entryComponents: [
ButtonComponent,
AlertComponent
],
exports: [
DynamicComponentCreationComponent
]
})
export class DynamicComponentCreationModule {}
So what exactly is an entry component? For that we can reference the Angular documentation on entry components:
For production apps you want to load the smallest code possible. The code should contain only the classes that you actually need and exclude components that are never used. For this reason, the Angular compiler only generates code for components which are reachable from the
entryComponents
; This means that adding more references to@NgModule.declarations
does not imply that they will necessarily be included in the final bundle.
In fact, many libraries declare and export components you’ll never use. For example, a material design library will export all components because it doesn’t know which ones you will use. However, it is unlikely that you will use them all. For the ones you don’t reference, the tree shaker drops these components from the final code package. If a component isn’t an entry component and isn’t found in a template, the tree shaker will throw it away. So, it’s best to add only the components that are truly entry components to help keep your app as trim as possible.
So when an Angular application is built, Angular will start at all entry components and walk down the template tree to see what is used in order to build the application bundle.
There are some complexities when using entry components in a lazy loaded module that can cause some unexpected headaches. The simplest solution is to declare all entry components in non-lazy loaded modules, however there are workarounds available when needed. For more detail see the related issue #14324
NOTE: With all that being said, the Ivy rendering engine aims to remove the need declaring entry components as this will automatically be handled by the @Component decorator.
ViewContainerRef
SNIP...
<div class="card-body">
<ng-template #componentTarget></ng-template>
</div>
SNIP..
@ViewChildren('componentTarget', {read: ViewContainerRef}) targets: QueryList<ViewContainerRef>;
Another pattern is to use a directive. In the directive you can get access to the ViewContainerRef for the directive’s element using dependency injection and then access that through the ViewChild instance.
@Directive({
selector: '[app-target]',
})
export class AppTargetDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}
Creating the Component
export class DynamicComponentCreationComponent implements OnInit {
private buttonFactory: ComponentFactory<ButtonComponent>;
private alertFactory: ComponentFactory<AlertComponent>;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
ngOnInit(): void {
this.buttonFactory = this.componentFactoryResolver.resolveComponentFactory(ButtonComponent);
this.alertFactory = this.componentFactoryResolver.resolveComponentFactory(AlertComponent);
}
}
private components: ComponentRef<any>[] = [];
public addToViewport(request: ComponentRequest): void {
const factory = request.componentType === 'button' ? this.buttonFactory : this.alertFactory;
const target = this.targets.toArray()[request.viewPort];
const componentRef = target.createComponent(factory);
componentRef.instance.type = request.contextType;
componentRef.instance.remove.subscribe(() => componentRef.destroy());
this.components.push(componentRef);
}
Let’s walk through what is happening here to discuss each step:
- Create the factory variable to hold the correct factory depending on the data in the request. This demonstrates how we can programmatically determine which type of component to create. We can select between as many components as we want as long as the component is defined as an entry component.
- Create a target variable to hold the correct ViewContainerRef where the component should go. Here we are selecting by using the index value passed in by the request object.
- Create the component by passing the factory to the ViewContainerRef instance. The reference to the resulting component is returned from this method so we can access that later to modify the component.
- Set input values on the component by interacting with the instance on the ComponentRef handle created in step 3.
- Subscribe to output events using the same instance property. Here you can see that when the remove event is fired we call the destroy method on the ComponentRef. This triggers Angular to remove the component from the DOM and fire all the destroy life-cycle hooks.
- Keep a reference to all the components we create. We don’t further interact with them in this example but it is often helpful to keep this reference for further interaction (generally destroying the component from some other interaction).
Demonstration
Here you can see the components getting created in different locations and styles dynamically, they also function as independent components by tracking the creation time and closing individually.
Dynamic Component Generation
Conclusion
Next Topic – 7 of 11: Provider Scopes & Component Level Services
Provider Scopes & Component Level Services
Overview
To follow along with the articles there a two repositories created: a starting point and final solution.
Setup
@Injectable({
providedIn: 'root'
})
export class LoadingCardService {
public isLoading = false;
public randomValueSubject = new BehaviorSubject<number>(null);
public randomValue(): Observable<number> {
return this.randomValueSubject.asObservable();
}
public runSomething(): void {
this.isLoading = true;
setTimeout(() => {
const randomNumber = Math.floor(Math.random() * 100);
this.randomValueSubject.next(randomNumber);
this.isLoading = false;
}, 2000);
}
}
<div class="card">
<div class="card-header">
<h1>{{type}} Card</h1>
</div>
<div class="card-body">
Your random number is: <span id="random-val">{{loadingCardService.randomValue() | async}}</span>
</div>
<div class="card-footer">
<button id="load-button" class="btn btn-primary" (click)="loadingCardService.runSomething()" [disabled]="loadingCardService.isLoading">
<span class="spinner-border spinner-border-sm" *ngIf="loadingCardService.isLoading"></span>
Click Me!
</button>
</div>
</div>
@Component({
selector: 'app-loading-card-global',
templateUrl: './loading-card.component.html'
})
export class LoadingCardGlobalComponent {
public type = 'Global';
constructor(public loadingCardService: LoadingCardService) { }
}
Dynamic Component Generation
The situation demonstrated in the article is not limited to dynamically generated components, this would just as easily occur by manually including multiple instances anywhere in the application but dynamic generation is a simpler method to interact with multiple variations of the test component.
@Component({
selector: 'app-component-scope',
template: `
<button class="btn btn-info" (click)="addGlobalCard()">Add Global Card</button>
<div class="card-group pt-5">
<ng-template #content></ng-template>
</div>`
})
export class ComponentScopeComponent {
@ViewChild('content', { read: ViewContainerRef }) content: ViewContainerRef;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
addGlobalCard(): void {
const factory = this.componentFactoryResolver.resolveComponentFactory(type);
this.content.createComponent(factory);
}
}
Testing Multiple Instances
When clicking the button to generate a value, both cards showed a spinner and the same resulting random number. This happens because all services are singletons in Angular when injected at the root level. When an object is created that requests a provider through the constructor, Angular will pass the same instance to all objects that request the provider. Normally, this is what we want and allows for a single location to store and share data throughout an application and ultimately manage the state of the application. This default approach is exactly what is expected for something like an authentication service, we wouldn’t want multiple instances that have different users or authentication states. But in this case, the default approach is misleading and could lead to unexpected behavior.
Changing Provider Scope
@Component({
selector: 'app-loading-card',
templateUrl: './loading-card.component.html',
providers: [LoadingCardService]
})
export class LoadingCardLocalComponent {
public type = 'Local';
constructor(public loadingCardService: LoadingCardService) { }
}
To test this, I’ll add the option to create cards of both types in our home component:
@Component({
selector: 'app-component-scope',
template: `
<button class="btn btn-info" (click)="addGlobalCard()">Add Global Card</button>
<button class="btn btn-info" (click)="addLocalCard()">Add Local Card</button>
<div class="card-group pt-5">
<ng-template #content></ng-template>
</div>`
})
export class ComponentScopeComponent {
@ViewChild('content', { read: ViewContainerRef }) content: ViewContainerRef;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
addGlobalCard(): void {
this.addCard(LoadingCardGlobalComponent);
}
addLocalCard(): void {
this.addCard(LoadingCardLocalComponent);
}
addCard(type: Type<unknown>): void {
const factory = this.componentFactoryResolver.resolveComponentFactory(type);
this.content.createComponent(factory);
}
}
Now, when clicking a single load button on a globally scoped service component we can see all the global components react and start loading as before and also load the same random value from the service as before – however the components with a locally scoped service remain in the initial state. when clicking the load button on one of those services it will load separately and create a different random number. The other locally scoped component still remains in the initial state.
You can see how with a single change we can control how data is shared and controlled in the application. Our global cards are bound together and work off the same set of data, the local cards have their own service instance and don’t share data with each other.
Child Component Dependency Injection
@Component({
selector: 'app-loading-button',
template: `
<button id="load-button" class="btn btn-primary" (click)="loadingCardService.runSomething()" [disabled]="loadingCardService.isLoading">
<span class="spinner-border spinner-border-sm" *ngIf="loadingCardService.isLoading"></span>
Click Me!
</button>`
})
export class LoadingButtonComponent {
constructor(public loadingCardService: LoadingCardService) { }
}
SNIP...
<div class="card-footer">
<app-loading-button></app-loading-button>
</div>
SNIP...
The buttons are still bound to the isLoading value for the instance of the provider that comes from the parent component. Remember, Angular walks up the component tree to find the lowest level provider to inject so the button will find the service scope at the global or local level depending on the setup of the parent. This means you can alter the provider at any level of an application – a module, a route component, down the the smallest display component and alter the behavior for any children.
Testing Notes
describe('LoadingCardGlobalComponent', () => {
let component: LoadingCardGlobalComponent;
let fixture: ComponentFixture<LoadingCardGlobalComponent>;
let loadingCardService: jasmine.SpyObj<LoadingCardService>;
beforeEach(() => {
loadingCardService = jasmine.createSpyObj(['runSomething', 'randomValue']);
loadingCardService.randomValue.and.returnValue(of(5));
TestBed.configureTestingModule({
declarations: [LoadingCardGlobalComponent, LoadingButtonComponent],
providers: [
{ provide: LoadingCardService, useValue: loadingCardService }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(LoadingCardGlobalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should show the random number', () => {
const valElement = fixture.debugElement.query(By.css('#random-val')).nativeElement;
expect(valElement.innerText).toContain(5);
});
});
We don’t see an injection error, we just aren’t seeing the mock value because an actual instance of the service is being used. The creation of the component is lower in the tree than the TestBed module, so that takes precedence. What needs to be done is to override the component level definition:
SNIP...
beforeEach(() => {
loadingCardService = jasmine.createSpyObj(['runSomething', 'randomValue']);
loadingCardService.randomValue.and.returnValue(of(5));
TestBed.configureTestingModule({
declarations: [LoadingCardLocalComponent],
providers: [
{ provide: LoadingCardService, useValue: loadingCardService }
],
schemas: [NO_ERRORS_SCHEMA]
});
TestBed.overrideComponent(LoadingCardLocalComponent, {
set: {
providers: [
{ provide: LoadingCardService, useValue: loadingCardService }
]
}
});
TestBed.compileComponents();
fixture = TestBed.createComponent(LoadingCardLocalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
SNIP...
Now the test is using the mock service we as we expect it to.
Provider Scopes & Component Level Services
Conclusion
Next Topic – 8 of 11: Control Value Accessor
Control Value Accessor
Overview
To follow along with the articles there a two repositories created: a starting point and final solution.
A Rating Input Component
class RatingStar {
icon: IconDefinition;
index: number;
selected: boolean;
constructor(index: number, rating: number) {
this.index = index;
this.selected = index < rating;
this.icon = this.selected ? faStarSolid : faStarEmpty;
}
}
@Component({
selector: 'app-rating-control',
template: <br /></span><span> <div> <br /></span><span> <fa-icon *ngFor="let star of ratingStars; let i = index"<br /></span><span> size="lg" <br /></span><span> [icon]="star.icon"<br /></span><span> [ngClass]="{'selected': star.selected}" <br /></span><span> (click)="setRating(i)" > <br /></span><span> </fa-icon> <br /></span><span> </div>,
styles: [ fa-icon { cursor: pointer; } .selected { color: GoldenRod; } ]
})
export class RatingControlComponent implements OnChanges {
@Input() maxRating = 10;
@Input() rating = 0;
@Output() ratingChange = new EventEmitter<number>();
public ratingStars: RatingStar[] = [];
ngOnChanges(): void { this.calculateRating(); }
private calculateRating(): void {
this.ratingStars = [];
for (let i = 0; i < this.maxRating; i++) { this.ratingStars.push(new RatingStar(i, this.rating)); }
}
setRating(index: number): void {
this.rating = index + 1;
this.calculateRating();
this.ratingChange.emit(this.rating);
}
}
The high level overview of this component is:
- It uses an array of a RatingStar class that is used to determine which icon to show for a given star
- The rating array is recalculated whenever an input changes or the user clicks on a star
- The user’s click on a star also emits the new value to enable two way data binding
The resulting component is used in a parent component bound to a max rating input and displays the output rating value to verify functionality.
Implementing ControlValueAccessor
Using the Component without ControlValueAccessor
We have a component that gives us some data, let’s see if we can use it in a form. Below is the form test bed we will use for testing, it includes the following features:
- A native input for comparison to the custom control we are building
- The custom rating control – note that the two way binding to rating is no longer included or needed
- Default values for each control
- Options to enable or disable the entire form
- A button to reset the state of the form
- Output of the form value and touched status
@Component({
selector: 'app-control-value-accessor',
template: <br /></span><span> <form [formGroup]="testForm" autocomplete="off" class="w-25 was-validated"><br /></span><span> <div class="form-group"><br /></span><span> <label>Test Input</label><br /></span><span> <input class="form-control" type="text" formControlName="testInput"><br /></span><span> </div><br /></span><span><br /></span><span> <div class="form-group"><br /></span><span> <label>Rating</label><br /></span><span> <app-rating-control [maxRating]="5" formControlName="rating"></app-rating-control><br /></span><span> </div><br /></span><span> </form><br /></span><span> <button class="btn btn-secondary" (click)="testForm.disable()">Disable Form</button><br /></span><span> <button class="btn btn-success" (click)="testForm.enable()">Enable Form</button><br /></span><span> <button class="btn btn-danger" (click)="testForm.reset()">Reset Form</button><br /></span><span> <div style="white-space: pre"><br /></span><span> {{testForm.value | json}}<br/><br /></span><span> Touched: {{testForm.touched | json}}<br/><br /></span><span> Valid: {{testForm.valid}}<br/><br /></span><span> Errors: {{testForm.controls['rating']?.errors | json}}<br/><br /></span><span> </div>
})
export class ControlValueAccessorComponent implements OnInit {
public testForm: FormGroup;
constructor(private formBuilder: FormBuilder) { }
ngOnInit(): void { this.testForm = this.formBuilder.group({ testInput: 'abc', rating: 3 }); }
}
The error specifies that for the element we specified as the ‘rating’ form control, Angular isn’t able to find a value accessor to know how to interact with the component. So what exactly is a value accessor?
The ControlValueAccessor Interface
ControlValueAccessor is an interface than Angular uses to keep the view in sync with forms data models and vice-versa. There are 4 methods on the interface, one of which is optional. We will walk through each method to better understand how Angular handles forms under the hood:
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
WriteValue
The writeValue method is responsible for updating the view when there are programmatic changes to the form. In the example of our rating component, this would be when the Input value changes for the rating and we need to recalculate which stars are shown.
RegisterOnChange
The registerOnChange method is called at the initialization of the form and registers a callback function to notify Angular when our control view has changed and the data needs to propagate to the form data model. The parameter of this method is a function that the control will need to save the reference and then call at the appropriate times. In our example, this should fire whenever a user clicks a star.
RegisterOnTouch
The registerOnTouch method is similar to the registerOnChange method as it also registers a callback function. This function is to notify the forms data model that the control has been touched. In a standard input, this is the blur event when a user clicks or tabs into the input but has not yet made a change. In our example, there is no blur method so we won’t have anything to implement. This is not uncommon and the touched property will get updated along with the change callback.
SetDisabledState
The setDisabledState method is an optional method for the interface. The method receives a boolean parameter that specifies if the control should be disabled or enabled. This is used to set the appropriate styling on the component to signify the state.
Implementing ControValueAccessor on a Component
There are two necessary steps to set a component to serve as a form value accessor, we must implement the ControlValueAccessor interface and also identify the component as a control value provider.
Step 1 – Interface Implementation
We can implement each interface method one by one to see how they function.
Write Value
To implement writeValue we will need a way to store the current value of the control within the component. We can use the rating property, but we no longer need to set it as an input to the control. When writeValue is called, we should set the rating to the provided value and update the view:
public rating: number;
writeValue(obj: any): void {
this.rating = obj;
this.calculateRating();
}
To implement registerOnChange we need a property to save a handle to the function provided. This method can replace the ratingChange event we had implemented as it serves the same exact purpose. All we need to do is save the method passed into the function and call it with the new value every time the control changes:
public onChangeFn;
setRating(index: number): void {
this.rating = index + 1;
this.calculateRating();
this.onChangeFn(this.rating);
}
RegisterOnTouched
We could leave registerOnTouched as an empty function as we don’t really have a touch event for the form control. However, for demonstration we can consider the control touched when the user hovers over the rating. All we need to do is call the method whenever the mouseleave event occurs on the wrapping element:
template: `<div (mouseleave)="onTouchFn()">
<fa-icon ...></fa-icon>
</div>`
SNIP...
public onTouchFn;
registerOnTouched(fn: any): void {
this.onTouchFn = fn;
}
The setDisabledState method is optional, but we can fairly easily implement it for this control. All we need to do is apply some styling to gray out the control and prevent the user click’s from changing the value. First – in the component we need to track if the control is disabled and make a change to setRating to stop any changes from happening when disabled:
public isDisabled: boolean;
setRating(index: number): void {
if (!this.isDisabled) {
this.rating = index + 1;
this.calculateRating();
this.onChangeFn(this.rating);
}
}
setDisabledState?(isDisabled: boolean): void {
this.isDisabled = isDisabled;
}
fa-icon {
cursor: pointer;
}
.selected {
color: GoldenRod;
}
.disabled {
color: gray !important;
cursor: default;
}
<fa-icon ...SNIP... [ngClass]="{'selected': star.selected, 'disabled': isDisabled}"></fa-icon>
Step 2 – Control Provider
Even with the ControlValueAccessor interface implementation we still get the same error as before when testing the form. The reason is that once the code is compiled, Angular has no way to identify that a component implements an interface. Instead, we need to register the component as a provider so Angular can easily obtain a reference to the class it will use to communicate with the form view. To do so, we can add a provider to the RatingControlComponent:
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RatingControlComponent),
multi: true
}
]
Testing The Form
You can see the touched value get updated when the cursor leaves the component, data is bound to the form just like the native input, the disabled status is applied, and the control resets with the form reset command. In summary, the custom control acts just as the native input and there is no difference from the form’s perspective on how to interact with the control.
Control Validation
Control Defined Validation
Custom controls may have internal validation that is always active – for example a date picker control may validate that the input is a valid date. For this example we can implement a minimum rating option that we define as an input to the component. To prepare we can add an input to the component to
@Input() minRating = 3;
<form [formGroup]="testForm">
<app-rating-control [maxRating]="5" [minRating]="minRating" formControlName="rating"></app-rating-control>
</form>
SNIP...
<div class="form-group">
<label>Min Rating</label>
<input name="minRating" class="form-control" type="number" [(ngModel)]="minRating">
</div>
// In Component default the value
public minRating = 3;
Validate
The required method to implement is the validate method. It provides the control as a parameter (as an AbstractControl) to the method and expects a null (valid) or a key-value pair (invalid) result. This signature is very similar to implementing any custom form validator. For our example, we just want to make sure that the value is above or equal to the minimum rating:
validate(control: AbstractControl): ValidationErrors {
return control.value >= this.minRating ? null : { tooLow: 'It's not that bad...'};
}
RegisterOnValidatorChange
The registerOnValidatorChange method is optional and registers a callback to call whenever the logic to determine if the control is valid changes. If the validation is a static check then this is not required. Going back to the date picker example we would only want Angular to check the validation whenever the control value changed. In our example, a change from outside the form (the minRating input value) could impact the valid status of the control so we want to execute the callback to trigger Angular to recheck the control. It’s sufficient for this control to just put the callback in the ngOnChanges method.
public onValidatorChangeFn;
registerOnValidatorChange?(fn: () => void): void {
this.onValidatorChangeFn = fn;
}
ngOnChanges(): void {
this.calculateRating();
if (this.onValidatorChangeFn) {
this.onValidatorChangeFn();
}
}
Provider Registration
We also need to register the component as a Validator, this is nearly identical to the process for ControlValueAccessor just using a different token:
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RatingControlComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => RatingControlComponent),
multi: true
}
]
User Defined Validation
Custom controls can have validation also applied the way you would for any other input. Here we can make the rating required so we can see how the two methods of validation interact:
this.testForm = this.formBuilder.group({
testInput: 'abc',
rating: [3, Validators.required]
});
Visualizing Status
We want to be able to see when the component is invalid, so we can add some simple CSS to the form component to change the stars to red when invalid. Angular applies a ng-invalid class to controls automatically when they are not valid that we can utilize:
app-rating-control.ng-invalid {
color: red;
}
Valid: {{testForm.valid}}<br/>
Errors: {{testForm.controls['rating'].errors | json}}<br/>
Testing Validation
With this in place, we can test the validation and see how the control reacts.
We can see that when the rating goes below the minimum the control is invalid. Also, when it is reset the required error is triggered and the control is also invalid. There is no difference between the two methods for how the control displays as both will add the ng-invalid class to be used for styling.
Control Value Accessor
Conclusion
Next Topic – 9 of 11: Reusable Sub-Forms
Reuseable Sub-Forms
Overview
To follow along with the articles there a two repositories created: a starting point and final solution.
Use Case
To begin, we can create a component that will represent our sub form. We want the form to have three inputs: first name, last name, and email address. We will implement some validation by requiring the last name and validating the email address pattern. I have added some validation styling using Bootstrap to help clearly show the status of the controls at all times:
@Component({
selector: 'app-user-form',
template: `
<form [formGroup]="userForm" class="was-validated">
<div class="form-group">
<label for="firstName">First Name</label>
<input id="firstName" type="text" class="form-control" formControlName="firstName">
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input id="lastName" type="text" class="form-control" formControlName="lastName" required>
<div class="invalid-feedback">
Last Name is required
</div>
</div>
<div class="form-group">
<label for="email">Email</label>
<input id="email" type="email" class="form-control" formControlName="email">
<div class="invalid-feedback">
Not a valid email address
</div>
</div>
</form>
`
})
export class UserFormComponent implements OnInit {
public userForm: FormGroup;
constructor(private formBuilder: FormBuilder) { }
ngOnInit(): void {
this.userForm = this.formBuilder.group({
firstName: null,
lastName: [null, Validators.required],
email: [null, Validators.email]
});
}
}
Making It Reusable with ControlValueAccessor
Implement ControlValueAccessor
Just as with a single value control, we can implement ControlValueAccessor for our sub-form. The forms API makes some of this quite easy to implement, the relevant changes are show below:
Template
<ng-container [formGroup]="userForm">
<input id="firstName" type="text" class="form-control" formControlName="firstName" (blur)="formTouchFn()">
<input id="lastName" type="text" class="form-control" formControlName="lastName" required (blur)="formTouchFn()">
<input id="email" type="email" class="form-control" formControlName="email" (blur)="formTouchFn()">
</ng-container>
public formTouchFn;
constructor(private formBuilder: FormBuilder, private ngForm: FormGroupDirective) { }
writeValue(obj: any): void {
const emtptyForm = {
firstName: null,
lastName: null,
email: null
};
const data = Object.assign(emtptyForm, obj);
this.userForm.patchValue(data);
}
registerOnChange(fn: any): void {
this.userForm.valueChanges.subscribe(fn);
}
registerOnTouched(fn: any): void {
this.formTouchFn = fn;
}
setDisabledState?(isDisabled: boolean): void {
if (isDisabled) {
this.userForm.disable();
} else {
this.userForm.enable();
}
}
Implement Validator
We also want any validation errors on our form to propagate to the consuming parent level form. In order to do this, we need to implement the Validator interface.
validate(control: AbstractControl): ValidationErrors {
return this.userForm.valid ? null : { invalidForm: { valid: false, errors: this.userForm.errors } };
}
registerOnValidatorChange?(fn: () => void): void { }
validate(control: AbstractControl): ValidationErrors {
const form = this.userForm;
if (form.valid) {
return null;
}
const errors = {};
Object.keys(form.controls).forEach(k => {
if (form.controls[k].invalid) {
errors[k] = form.controls[k].errors;
}
});
return errors;
}
Register Providers
Lastly – we just need to register the component as a value accessor and validator provider:
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => UserFormComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => UserFormComponent),
multi: true
}
]
Testing The Form
// In Template
<app-user-form formControlName="user"></app-user-form>
// In Component
this.testForm = this.formBuilder.group({
testInput: 'abc',
rating: [3, Validators.required],
user: {firstName: 'Steve', email: 'steve@test.com'}
});
The sub form works as expected to populate the user data, validation on the individual controls in the sub form carry to the parent form validity, and all controls follow the disabled and reset commands just as the native input on the parent level of the form.
Reusable Sub-Forms
Conclusion
Next Topic – 10 of 11: RouteReuseStrategy – Maintaining Component State on Navigation
Reuseable Sub-Forms
Overview
To follow along with the articles there a two repositories created: a starting point and final solution.
RouteReuseStrategy
export class DefaultRouteReuseStrategy implements RouteReuseStrategy {
shouldDetach(route: ActivatedRouteSnapshot): boolean { return false; }
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void { }
shouldAttach(route: ActivatedRouteSnapshot): boolean { return false; }
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { return null; }
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
}
In order to use a custom implementation of the RouteReuseStrategy, you need to complete two steps:
-
1. Create a class extending the RouteReuseStrategy – a sample minimal implemenation is below that uses the default Angular functionality as a starting point:
export class CustomReuseStrategy extends RouteReuseStrategy {
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
return null;
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return false;
}
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return false;
}
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
}
}
providers: [
{ provide: RouteReuseStrategy, useClass: CustomReuseStrategy}
]
Example Situation
A common situationin web applications is a master-detail relationship. There is some master page (e.g. search results) which the user can select individual details to view. In this pattern, we are going to set a few constraints for how we want the application to perform:
- The master page should have some interactivity state that should be maintained between detail views such as sorting, filtering, etc. – we will represent this by a random number displayed in the component where if we maintain the same random number we can demonstrate a preservation of the state of the component
- The detail is viewable via a distinct URL containing an ID for the detail record
- The detail view should load the details upon routing and be able to run some initialization for the detail
- Components need to run cleanup code when destroyed such as unsubscribing from observables – we will represent this by logging a message to the console.
We can implement two commonly used designs to meet the requirements. As we will see, each can have some drawbacks and areas that we can gain additional control using the RouteReuseStrategy. Both approaches will use the same detail page and a similar base class for the master page.
Shared Components
This example will share as much as possible between the implementations to help distill the different approaches to only the differences.
Master
The first shared functionality is a base abstract class to be used by the master page.
export abstract class MasterComponent implements OnInit, OnDestroy {
public randoms: number[];
public abstract displayName: string;
ngOnInit(): void {
this.randoms = [];
const numberOfLinks = this.getRandom(10);
for (let i = 0; i < numberOfLinks; i++) {
this.randoms.push(this.getRandom(100));
}
}
getRandom(max: number): number {
return Math.floor(Math.random() * max);
}
ngOnDestroy(): void {
console.log(Destroy </span>${<span>this</span>.<span>displayName</span>}<span>);
}
}
There are a few things happening in this component to take note of:
- When the component initializes, it generates a new list of results – this represents user interaction with a list of items such as filtering or sorting
- The component logs a message with an identifier to be overridden by the consuming component
Detail
Both approaches will use the same detail component:
@Component({
selector: 'app-detail',
template: `
<div class="alert alert-primary" role="alert">
<strong>Detail ID:</strong> {{routeParam}}
</div>
<div>
<strong>Random value:</strong> {{randomVal}}
</div>
`
})
export class DetailComponent implements OnInit, OnDestroy {
public routeParam;
public randomVal: number;
constructor(private route: ActivatedRoute) { }
ngOnInit(): void {
this.route.params.subscribe(p => this.routeParam = p.detailId);
this.randomVal = Math.floor(Math.random() * 100);
}
ngOnDestroy(): void {
console.log(Destroy detail: </span>${<span>this</span>.<span>routeParam</span>}<span>);
}
}
There are a few things happening in this component to take note of:
- When the component initializes, it gets the route parameter from the URL to display in the view – this represents any data loading for the detail from a back-end service
- Generates a new random value to display in the view – this represents some view initialization for the detail page
- The component logs a message with the route parameter when it is destroyed
Parent-Child Design
The first design is a parent-child relationship where the contents of the master page stay on screen when viewing the detail. This is best used when the detail is small and can reasonably fit on the same page. This method places a router-outlet in the master page to display the child content:
@Component({
selector: 'app-parent',
template: `
<h1>Parent Component</h1>
<ul class="nav nav-tabs">
<li class="nav-item" *ngFor="let random of randoms">
<a class="nav-link" [routerLink]="['detail', random]" routerLinkActive="active">Go to {{random}}</a>
</li>
</ul>
<router-outlet></router-outlet>`
})
export class ParentComponent extends MasterComponent {
displayName = 'ParentComponent';
And adds the detail as a child route for the detail in the application route configuration:
{
path: 'parent',
component: ParentComponent,
children: [
{
path: 'detail/:detailId',
component: DetailComponent
}
]
}
We can test and see how this setup meets our needs when navigating between detail pages:
It is also important to note that the console for the application is completely empty, but when navigating elsewhere we see the destroy message.
When comparing these results to our requirements, we see the following results:
- PASS – Master page is maintained
- PASS – Detail view is a unique URL
- FAIL – Detail view initialization does not run – the random value is the same every time even when routing between different details
- PARTIAL – Detail component is never cleaned up between detail views, but it is after navigating away
This approach initially results in one failure and one partial that we need to review. Why does this fail to meet our requirement?
Look at the Angular DefaultRouteReuseStrategy class – specifically the shouldReuseRoute method. The implementation returns true when the destination route has the same route configuration as the source route. When going from one detail to the next this is always true – only the parameter changes and the same route configuration and components are used. When the shouldReuseRoute method returns true, Angular will not destroy the component tree in turning firing the ngOnInit and ngOnDestroy hooks – instead the only thing that happens it the router emits new values for the route parameters and other Observables. Therefore, we see the Detail ID update but not the random number.
We could easily solve the random number generation issue by moving the code into the subscribe callback on the params Observable. This is often the best method in a detail type view to distinguish between code that creates the component and updates its contents when new data arrives or the route parameter changes. Angular is designed around the idea of using Observables for communication and it shows here. Most of the time, using the approach of binding to the param update instead of the overall life-cycle hooks will meet your application needs, but what happens when it doesn’t? What if we really need the ngOnDestroy hook to fire when navigating away for that parameter and not just the component?
Returning to the RouteReuseStrategy – we can provide a custom implementation of the shouldReuseRoute method. We can set up a custom class implementation as discussed earlier copying the functionality of the default Angular implementation. The only change we will make now is to not reuse any routes:
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return false;
}
With only this change let’s test again and see what happens when we select a detail this time:
The ngOnDestroy hook runs all right, but we’ve made this far worse. The ngOnDestroy hook ran for the ParentComponent as well and we lost the state of the parent, we have new random links so a fresh search to the back-end has been called and the user loses all interaction. We can now see why Angular has the default functionality we saw earlier. When a routing event happens, it doesn’t just happen at the detail level. Angular needs to review the entire component tree generated by the route. When routing between siblings, the default functionality keeps the parent components intact and only rebuilds what is necessary – which not only gives a mostly expected behavior but has large performance benefits as well.
What we need is a way to direct Angular to only rebuild certain components but reuse others by default. We want to add the default functionality back in to shouldReuseRoute and only deviate when we specifically request it. We can accomplish this by using the data property on the route configuration which is designed exactly for this type of extensibility to a route. Let’s add a flag to the detail route definition:
{
path: 'detail/:detailId',
component: DetailComponent,
data: {
alwaysRefresh: true
}
}
And check for this in shouldReuseRoute:
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
if (future.routeConfig === curr.routeConfig) {
return !future.data.alwaysRefresh;
} else {
return false;
}
}
Now to test again:
Excellent! We now are only rebuilding the detail component and we leave the parent component untouched. We have a design that meets all our requirements – just be aware of the consequences if the detail view is to have any children added to it.
Search-Detail
The other design approach is to create two separate pages, a search results page and a detail view page. This situation becomes far more desirable than the Master- Detail as the detail page gets larger and adds functionality that doesn’t easily fit on a single page. It is also very reasonable for a single site to employ both approaches – for example JIRA allows users to switch between both display options when reviewing search results.
To implement this approach, we create sibling routes instead of a parent-child relationship. These can be moved around to different parents depending on the desired URLs for the application, but we will put both under an empty parent to match the URL design of the Parent-Child approach:
@Component({
selector: 'app-search',
template: `
<h1>Search Component</h1>
<ul class="nav nav-pills flex-column">
<li class="nav-item" *ngFor="let random of randoms">
<a class="nav-link" [routerLink]="['detail', random]" routerLinkActive="active">Go to {{random}}</a>
</li>
</ul>
`
})
export class SearchComponent extends MasterComponent {
displayName = 'SearchComponent';
}
{
path: 'search',
children: [
{
path: '',
component: SearchComponent
},
{
path: 'detail/:detailId',
component: DetailComponent
}
]
}
Let’s review how this stacks up against our requirements:
- FAIL – The master page loses its context when we return to it
- PASS – Detail view is a unique URL
- PASS – Detail view initialization runs and gives a new random value
- PASS – Detail component is cleaned up between detail views
This approach gives an opposite problem as the Parent-Child design. We initialize the detail view as expected, but we lose the context of the search results.
NOTE: The problem we had before with the initialization of the detail view still would exist if the detail view provided a direct link to another detail – this approach just doesn’t show it with this testing because we are using the master page to navigate between details.
As we are navigating to a new page, we need some way to store the results of the search page somewhere in memory so we can retrieve it when returning to the page. There are three commonly seen approaches to this:
- Using a service or other state management technique, but this approach often requires manually binding everything in the component into a service which gets tedious and error prone as the application grows.
- Use the Parent-Child approach and hide or cover the parent page when a detail is visible (e.g. detail is a modal window or some other overlay). This again can work for simple implementations but has drawbacks for side effects and requires tightly coupling the components. If trying to use a full screen overlay this can also have adverse effects on usability if the user uses the back button to try to go back to the parent.
- RouteReuseStrategy to the rescue!
To see how we can use RouteResuseStrategy we need to examine the process of routing and how the methods are used:
- shouldDetach – This determines if the route the user is leaving should save the component state.
- store – This stores the detached route if the method above returns true.
- shouldAttach – This determines if the route the user is navigating to should load the component state.
- retrieve – This loads the detached route if the method above returns true.
With these methods, we would expect the following for our custom implementation:
- shouldDetach returns true for the search results page and false for all other routes
- shouldAttach returns true when we have a saved search result
- we need a way to clear a saved search for when the user conducts a new search
Let’s start with a similar approach we took before by adding a flag to the route configuration. This could also be done through identifying the URL or any other identification technique:
{
path: 'search',
children: [
{
path: '',
component: SearchComponent,
data: {
saveComponent: true
}
},
{
path: 'detail/:detailId',
component: DetailComponent
}
]
}
This gives a straightforward approach to the shouldDetach method:
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return route.data.saveComponent;
}
And to store the component we need an internal map to use for storage, we can add this as a private property to the custom RouteReuseStrategy class:
private savedHandles = new Map<string, DetachedRouteHandle>();
And now to store the component under a unique key (this is important to prevent conflict between multiple saved routes if this strategy is used for multiple routes):
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
const key = this.getRouteKey(route);
this.savedHandles.set(key, handle);
}
// Routes are stored as an array of route configs, so we can find any with url property and join them to create the URL for the rotue
private getRouteKey(route: ActivatedRouteSnapshot): string {
return route.pathFromRoot.filter(u => u.url).map(u => u.url).join('/');
}
That completes the setup for saving the component when leaving, now to check if we should load a saved component when navigating to a route we can check if we have anything saved for the provided route and return the value as needed:
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return this.savedHandles.has(this.getRouteKey(route));
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
return this.savedHandles.get(this.getRouteKey(route));
}
Now we can test with this implementation of RouteReuseStrategy:
Success! We are maintaining the search result component state across navigation and creating a new detail component for each detail we view.
There is one remaining item to address, what happens when we want to conduct a new search? Even if the contents of the search result page are populated via an Observable, we may still want the ngOnInit hook to fire to reset the sorting, filters, pagination, etc. We need a way to clear the saved component on demand. Let’s create a method to do so in the custom RouteReuseStrategy class:
public clearSavedHandle(key: string): void {
this.savedHandles.delete(key);
}
We need a way to access this method however. Think back, what is the key to implementing a custom RouteReuseStrategy in the first place? We implemented a provider – we can use that! This would likely happen in a service and be executed whenever the user conducts a new search, but for simplicity lets add a button on the detail page:
<button class="btn btn-danger" (click)="clearStoredRoute()">Clear Search</button>
constructor(private route: ActivatedRoute, private routeReuseStrategy: RouteReuseStrategy) { }
clearStoredRoute(): void {
(this.routeReuseStrategy as CustomReuseStrategy).clearSavedHandle('/router-reuse/search/');
}
Note we need to cast the injected object to our custom class – the injection token is for the abstract class so that’s all that Typescript gives us. Also note that we need to provide the URL key that was generated in our class implementation as the saved handle key. Keep this in mind when determining an implementation for triggering a save of a route. This example mixed a URL route and route config data, but generally a single approach is preferred for simplicity and maintenance.
Let’s see this in action – first a test to make sure it still works as before then clearing the search:
There we have it – a new search result any time the Clear Search button is clicked. There is one very subtle problem left. We have a new search component – but the old one never got destroyed! This would result in memory leaks and other potential unintended consequences. When Angular detaches a component, it pulls it out of its responsibility and hands it over to our RouteReuseStrategy implementation. When we cleared the saved handle, we just removed the object from our tracking map, but that never triggers the lifecycle hooks we would expect. Let’s add some logic to address this in our custom RouteReuseStrategy:
public clearSavedHandle(key: string): void {
const handle = this.savedHandles.get(key);
if (handle) {
(handle as any).componentRef.destroy();
}
this.savedHandles.delete(key);
}
We have access to the component instance itself from the saved handle so all we need to do is call destroy to trigger the destruction life-cycle of the component.
RouteReuseStrategy – Maintaining Component State on Navigation
Conclusion
Next Topic – 11 of 11: Testing Complex Component Interaction
Testing Complex Component Interaction
Overview
To follow along with the articles there a two repositories created: a starting point and final solution.
The Situation
Let’s say we have a reporting dashboard with 3 components, a criteria component, a results component, and an input component. The input component provides the trigger to run or update the report, but we need to get the criteria from a separate component in order to know what to run. That builds a request to send to a service, and displays the resulting data in the results component. We have some unique situation that prevents the input component from interacting with the criteria component, for example the input is a toolbar and the criteria is a filter section in another location. We will need to get the criteria as it current is set at any point – this means we need to use ViewChild instead of an Output event:
@Component({
selector: 'app-report',
template: <br /></span><span> <app-report-criteria #criteria></app-report-criteria><br /></span><span> <app-report-input (runReport)="refresh()"></app-report-input><br /></span><span> <app-report-results [data]="reportResults"></app-report-results>
})
export class ReportComponent {
public reportResults: ItemInterface[];
@ViewChild('criteria') criteriaComponent: ReportCriteriaComponent;
constructor(private searchService: SearchService) { }
public refresh() {
this.reportResults = null;
if (this.criteriaComponent.validateCriteria()) {
const criteria = this.criteriaComponent.getSearchCriteria();
this.searchService.search(criteria).subscribe(data => this.reportResults = data);
}
}
}
The Solution
To test this component, we will need to generate a testing component that we can use to render instead of the actual component. The first step we can implement is leveraging TypeScript to provide some insurance that our mock implementation aligns with our actual implementation. We can do this by using an interface that we implement for the component and the mock component. Let’s implement an interface now. We can even make it generic to the type of data we get from the service so we could re-use this for other criteria or forms:
export interface ReportCriteriaFormInterface<T> {
validateCriteria(): boolean;
getSearchCriteria(): T;
}
export class ReportComponent {
public reportResults: ItemInterface[];
@ViewChild('criteria') criteriaComponent: ReportCriteriaFormInterface<SearchInterface>;
//SNIP...
}
Mock Component
Let’s start by creating our test spec, if there is anything unfamiliar here then review the testing documentation from Angular. We set up a standard test with a service spy providing some dummy data.
describe('ReportComponent', () => {
let component: ReportComponent;
let fixture: ComponentFixture<ReportComponent>;
let searchService: jasmine.SpyObj<SearchService>;
const testData: ItemInterface[] = [
{ name: 'Apples', description: 'Juicy, red, and delicious' },
{ name: 'Socks', description: 'So your feet don't get cold' },
{ name: 'Tires', description: 'Because we sell everything' }];
beforeEach(() => {
searchService = jasmine.createSpyObj(['search']);
searchService.search.and.returnValue(of(testData));
TestBed.configureTestingModule({
declarations: [ReportComponent],
providers: [
{ provide: SearchService, useValue: searchService }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(ReportComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
});
describe('ReportComponent', () => {
// SNIP
TestBed.configureTestingModule({
declarations: [ReportComponent, ReportInputComponent],
providers: [
{ provide: SearchService, useValue: searchService }
],
// SNIP
it('should run a search', () => {
fixture.debugElement.query(By.css('button')).triggerEventHandler('click', {});
expect(searchService.search).toHaveBeenCalled();
});
});
@Component({
selector: 'app-report-criteria',
template: ''
})
export class MockReportCriteriaComponent implements ReportCriteriaFormInterface<SearchInterface> {
public isFormValid = true;
public searchData: SearchInterface;
validateCriteria(): boolean {
return this.isFormValid;
}
getSearchCriteria(): SearchInterface {
return this.searchData;
}
}
Secondly, notice the selector of the component. This must match the selector for the actual component. This is how Angular finds the component to inject in the template, so the selector (and Inputs and Outputs) must match. We just need to declare the component in our test to use it:
describe('ReportComponent', () => {
// SNIP
TestBed.configureTestingModule({
declarations: [ReportComponent, ReportInputComponent, MockReportCriteriaComponent],
providers: [
{ provide: SearchService, useValue: searchService }
],
// SNIP
});
it('should run a search', () => {
const mock = component.criteriaComponent as MockReportCriteriaComponent;
mock.isFormValid = true;
const testSearch = {searchTerm: 'foo'};
mock.searchData = testSearch;
fixture.debugElement.query(By.css('button')).triggerEventHandler('click', {});
expect(searchService.search).toHaveBeenCalledWith(testSearch);
});
it('should not run a search for invalid criteria', () => {
const mock = component.criteriaComponent as MockReportCriteriaComponent;
mock.isFormValid = false;
fixture.debugElement.query(By.css('button')).triggerEventHandler('click', {});
expect(searchService.search).not.toHaveBeenCalled();
});
Testing Complex Component Interaction
Conclusion
For A Team That Brings More To The Project Than Just Heads-Down Programming: Contact Us Today!
GET TO KNOW US
Consultant
Dave Cloutier – Author
Business and technology acumen have dovetailed in Dave Cloutier’s career, making him an ideal Intertech consultant. After an early start as an aerospace engineer, Dave became a business analyst at Prosar (now ProPharma Group). While maintaining most of his business analyst responsibilities, he also became lead developer and led the implementations of multiple enterprise systems. Dave joined Intertech in 2019, where he devises creative solutions to complex problems. He is a natural teacher and leader who can work individually or as part of a team.
“Risk versus reward is the critical factor in most decisions,” says Dave. “There rarely are times when difficult decisions have an obvious choice. Finding the choice that maximizes the potential reward for an acceptable level of risk only can be done when a solid relationship exists between the client and consultant.”
Why Did You Choose Software Consulting Over Your Previous Experience As An Aerospace Engineer & Business Analyst?
I believe IT solutions are at their best when the lanes of communication between developers and end users are open. Developers who know the ins and outs of the business can design creative solutions using emerging technology that end users may never have imagined. An open, creative and iterative approach yields better software and pushes both sides to learn and grow.
Sideline
I like to pretend that I’m good at hockey and play in an adult winter league, but my favorite hobby is woodworking. I’m slowly working to replace all the furniture in my house with something I made. After working in a virtual world during the day, it’s refreshing to build something physical!
The Fastest Way To Build Software Is “Right” The First Time!
Understanding your industry is one thing. Understanding the technology you are using is another. When you read studies that tell you that 75% of projects are doomed from the beginning, it has to make you pause before signing your name to the outcome.
Consider letting our proven professionals take a look at your project. They’ve seen what can go wrong and know how to avoid costly errors.
We build custom software from start to finish. We plug into your environment with the proven expertise you need for us to work independently or in co-development. And, we bring the soft-skills that make the task enjoyable, and the experience to leave your team stronger and ready to take over.
We Bring You…
Team-Complete™ Development
Soft-Skills For A Winning Experience
Sometimes the most critical person in the room is the one with a calm voice and the knowledge to select the right words. Bringing a development team together or presenting a clear concept for stakeholders can make all the difference between success or failure. Intertech consultants are at the top of their field. They navigate challenging decisions, guide with a confident voice, and know when to get out of the way.
Intertech takes the worry out of custom software development.
Let’s Build Something Great!
Tell us what you need and we’ll get back with you ASAP!
Consider Intertech…
Tell us about your project and we’ll gather the right people, discuss your request, clarify any questions, and provide a price estimate that you can use to decide if Intertech is a good fit for you!
“We Take The Worry Out Of Custom Software Development!”
Turn-Key Custom Application Development Solutions Since 1991
Independent & Co-Development
Tom Salonek Founder & CEO