Angular 5—Communicating Between Components with Delegates

Igor Vorobiov
Jun 21, 2018 · 3 min read
Photo by rawpixel on Unsplash

If you’ve ever developed an application on Swift for iOS devices you might came across such term as “delegate”.

The greatest advantage of the delegate is loose coupling.

It enables component A (the delegate) to depend on component B (the delegating component) without component B having to have any knowledge of component A. With that, we can provide different implementations of component A without touching component B.

In fairness, I would say that without delegates we can solve the same problem with numerous of “@Output” declarations in our component B and respective functions in our component A.

Let’s see how this could look like in the code:

a.component.ts

@Component({
...
})
export class AComponent implements OnInit {
headers: string[] = ['column1', 'column2', 'column3']; data: Car[] = []; ngOnInit() {
this.carService
.loadAll()
.subscribe(cars => this.data = cars, error => ...);
}
saveData(car: Car) {
...
}
}

a.component.html

<b-component 
(onSaveData)="saveData($event)"
[data]="data"
[headers]="headers"></b-component>

b.component.ts

@Component({
...
})
export class BComponent {
@Input() headers;
@Input() data;
@Output() onSaveData: EventEmitter<Car> = ...
...
}

I can see two problems in the code above:

  • How to pass a filter from component B to component A so that component A could load the data according to the filter.

While those problems are solvable even with the implementation above, I’d like to introduce another approach with a delegate involved.

Let’s see how this would look like in the code:

a.component.ts

@Component({
...
})
export class AComponent implements BDelegate {
headers: string[] = ['column1', 'column2', 'column3'];

loadData(filter: Filter): Observable<Car[]> {
return this.carService.loadAll(filter);
}
saveData(car: Car): Observable<any> {
...
}
}

a.component.html

<b-component [delegate]="this"></b-component>

b.component.ts

export interface BDelegate {    headers: string[];

loadData(filter: Filter): Observable<Car[]>;
saveData(car: Car): Observable<any>;
}
@Component({
...
})
export class BComponent implements OnInit {

@Input() delegate: BDelegate;
headers: string[]; data: any[]; ... ngOnInit() {
this.headers = this.delegate.headers;
this.delegate
.loadData(this.filter)
.subscribe(data => this.data = data, error => ... );
...
}
save() { this.delegate
.saveData(this.table.data)
.subscribe(() => ..., error => ...);
...
}
...
}

I have introduced the “BDelegate” interface. This interface ensures that whichever delegate is passed it will do what’s expected by the delegating component, in our case is component B.

Next, since component A implements the “BDelegate” interface we can safely pass its reference to component B in the template via the “delegate” input. With that, we give a complete control over the process to component B. That means that we will have a single place responsible to manage that specific task.

Simply put, component B— manager and component A— worker.

In addition to solving the above mentioned problem I found that with this approach we can write code more safely and confidently because having a delegate interface ensures that a user of component B will know what and how to implement. What we can’t say about the first approach.

Moreover, with the delegate approach we don’t have to have tons of inputs and outputs in templates. All the interactions between components can be done in the code directly which gives more flexibility and more room to solve problems differently.

Summary

I hope you found this way of components interaction very helpful. I can see a big potential in it which could help solve many problems in Angular. If so do you then feel free to share this post and applaud me. Otherwise, I am open to discuss with you drawbacks of this approach.