Imagine the following requirements for your Angular app:
- Your customer wants a view that displays multiple cards at once.
- A button at the top of the page should toggle the edit mode.
- While in edit mode, all cards are editable at the same time.
- As soon as you click "save" only those cards that actually have been edited are going to send a request to the server.
- If you decide to discard the changes by clicking on "cancel" all cards should be reset to their initial state.
This is a common scenario in any more sophisticated web application. Nevertheless, I just recently discovered a very handy way to handle those requirements in Angular. In this article I want to share my solution, leveraging the @ViewChildren property decorator to implement the Card Composite pattern.
Find the whole example: here
Implementing a specific card component
Before we implement the card composite component, lets start with the implementation of its cards. We begin with the simple implementation of a single card, but since the card composite component will need to access the same functionality across every card, we later refactor our code into a base component class.
export interface User {
firstName: string;
lastName: string;
age: number;
}
<h4>Card Title</h4>
<form #form="ngForm">
<label for="firstname">Firstname</label>
<input
name="firstname"
[disabled]="!inEditMode"
[(ngModel)]="user.firstname"
/>
<label for="firstname">Lastname</label>
<input
name="lastname"
[disabled]="!inEditMode"
[(ngModel)]="user.lastname"
/>
<label for="age">Age</label>
<input
name="age"
type="number"
[disabled]="!inEditMode"
[(ngModel)]="user.age"
/>
</form>
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
styleUrls: ['./user-card.component.css']
})
export class UserComponent implements OnInit {
@Input()
inEditMode: boolean = false;
@Input()
title: string = 'User';
@ViewChild('form')
form!: NgForm;
user!: User;
unchangedUser!: User;
constructor(private userService: UserService) {}
ngOnInit() {
this.loadUser();
}
loadUser(): void {
this.user = this.userService.getEntity();
this.unchangedUser = { ...this.user};
}
save(): void {
if (this.form.dirty) {
this.entityService.setEntity(this.user);
this.unchangedUser = { ...this.user};
}
}
cancel(): void {
this.user = { ...this.unchangedUser };
}
}
Notice: For simplicity reasons, the services used in this tutorial only sets or gets an entity from local storage. In a real application they would most likely fetch the entity from server.
In the code above we have inEditMode as an input property to control whether or not the card's form controls should be disabled. Since we use two-way data binding, changes in the form are immediately reflected in our User entity. Therefore, in order to reset the User to its initial value, we need to make a copy after loading the User on init.
The card composite component is later going to leverage the save and cancel functions of a card to execute respective behavior.
Refactoring the card component to a generic base component
With all this functionality in place we are already good to go. We could just copy the code from the UserCardComponent and reuse it to create more cards, that share the same save and cancel behavior. Nonetheless, as an aspiring programmer a good practice is to reduce repetitive code. Therefore we are going to create a generic BaseCardComponent as follows.
@Component({
selector: '',
template: '',
})
export class BaseCardComponent<T> implements OnInit {
@Input()
inEditMode: boolean = false;
@ViewChild('form')
form!: NgForm;
entity!: T;
unchangedEntity!: T;
constructor(private entityService: BaseEntityService<T>) {}
ngOnInit() {
this.loadEntity();
}
loadEntity(): void {
this.entity = this.entityService.getEntity();
this.unchangedEntity = { ...this.entity };
}
save(): void {
if (this.form.dirty) {
this.entityService.setEntity(this.entity);
this.unchangedEntity = { ...this.entity };
}
}
cancel(): void {
this.entity = { ...this.unchangedEntity };
}
}
As you notice, the BaseCardComponent does not look to different from the specific UserCardComponent. With the generic in place it is now much easier to create new card components just by extending from this base class. Please take a look at the examples below.
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
styleUrls: ['./user-card.component.css'],
standalone: true,
imports: [FormsModule, CommonModule],
})
export class UserCardComponent extends BaseCardComponent<User> {
constructor(
entityService: UserService
) {
super(entityService);
}
}
@Component({
selector: 'app-bank-account-card',
templateUrl: './bank-account-card.component.html',
styleUrls: ['./bank-account-card.component.css'],
standalone: true,
imports: [FormsModule, CommonModule],
})
export class BankAccountCardComponent extends BaseCardComponent<BankAccount> {
constructor(entityService: BankAccountService) {
super(entityService);
}
}
Notice: Of course all cards have their specific template that should include a in order to reference it within the component.
Implementing the Composite Card Component
With all the cards setup, we finally can start the implementation of the composite card component. Because it uses the @ViewChildren decorator at its core, the following abstract gives a more detailed explanation of how we are going to use it.
@ViewChildren
The @ViewChildren decorator allows you to get a reference to multiple elements of your template. While you can pass a single component to query the DOM for all occurrences of that Component, there is a less known query option that comes in handy for our use case. Instead of passing a component class, the @ViewChildren decorator also accepts multiple comma separated string selectors, like below.
@ViewChildren('component1, component2')
cards!: QueryList<any>;
this is exactly what we want as the basis of our card composite component.
Composite Card Component
We start out by defining our template as depcited below.
<ng-container *ngIf="inEditMode === false">
<button (click)="onEdit()">Edit</button>
</ng-container>
<ng-container *ngIf="inEditMode === true">
<button (click)="onCancel()">Cancel</button>
<button (click)="onSave()">Save</button>
</ng-container>
<app-user-card #userCard [inEditMode]="inEditMode"></app-user-card>
<app-bank-account-card #bankAccountCard [inEditMode]="inEditMode"></app-bank-account-card>
Depending on the inEditMode property, we either show or hide the edit, cancel and save buttons at the top of our template. As mentioned before, we pass the inEditMode property down to our card components.
export class CardCompositeComponent {
@ViewChildren('userCard, bankAccountCard')
cards!: QueryList<BaseCardComponent<unknown>>;
inEditMode: boolean = false;
onEdit(): void {
this.inEditMode = true;
}
onCancel(): void {
this.cards.forEach((c) => c.cancel());
this.inEditMode = false;
}
onSave(): void {
this.cards.forEach((c) => c.save());
this.inEditMode = false;
}
}
In our .ts file, we get a reference to all our cards by listing them within the @ViewChildren decorator. To take care of type safety, we just add the refactored BaseCardComponent as a generic type to the QueryList. When clicking on save or cancel, all that is left to do is, to loop through all cards and call the respective function of our card components.
Conclusion
The @ViewChildren decorator makes it very easy to "compose" multiple components at once. Even though component inheritance in Angular should be handled with care, in this article I showed how it reduces code and provides a common interface for the composite component to interact with.
If you like to read about Angular, you can browse my other articles here.
If you want to support me, follow me on X.
Top comments (0)