The smart vs dump component design is well established. I am trying to apply it where it's feasible. I use facade services to store/update my data, something like this (vastly simplified):
models:
export interface User {
id: number;
name: string;
role: number;
}
export interface Role{
id: number;
name: string;
}
export class UserDataFacade {
constructor(
private _dataService: UserApiService
) {}
private _users: ReplaySubject<User[]> = new ReplaySubject(1);
public users: Observable<User[]> = this._users.asObservable();
private _roles: ReplaySubject<Role[]> = new ReplaySubject(1);
public roles: Observable<Role[]> = this._roles.asObservable();
public loadUsers(): void {
this._dataService.loadUsers().subscribe(users => this._users.next(users));
}
public loadRoles(): void {
this._dataService.loadRoles().subscribe(roles => this._roles.next(roles));
}
//....
}
And in a component:
export MySmartComponent {
public get users$(): Observable<User[]> { return this._dataFacade.users$; }
constructor(
private _dataFacade: UserDataFacade
) {
this._dataFacade.loadUsers();
this._dataFacade.loadRoles();
}
<ng-container *ngIf=(users$ | async) as users>
<my-dumb-user-list [users]="users"></my-dumb-user-list>
</ng-container>
My user list is supposed to show the name of the role in addition to the name of the user.
Of course, I could pass the roles to my dumb component, and manually retrieve the correct role name for each user in my table.
But that would get tedious if I have multiple attributes like role, position etc. I don't want to pass 10 inputs to my dumb component.
My idea was to use a "smart" pipe that looks a bit like so:
@Pipe({
name: 'userRole$',
})
export class UserRolePipe implements PipeTransform {
public transform(id: number): Observable<Role> {
return this._dataFacade.roles$.pipe(
map(roles => roles.filter(r => r.id === id)[0] // I know, wonky, just to show whats going on
);
}
constructor(private _dataFacade: DataFacade) {}
}
And then, my dumb list could use it like that:
<ul>
<li *ngFor="let user of users">{{ user.name }}: {{ (user.role | userRole$ | async).name }}</li>
</ul>
I think there are some merits to this design:
- Simpler handling for "associated" objects, like
Role - Less HTML/ts popllution for trivial filter operations like finding the name for an id.
- Strictly speaking, still "dumb" since no api calls are made.
On the other hand:
- Strictly speaking, the pipe is impure
- Someone has to "know" that roles have to be loaded for this to work (Could by worked around by loading the roles in the pipe, but this would definitely not qualify as dumb), therfore not an obvious API.
In a sense, I guess it's somewhat similar to the way the translate pipe in ngx-translate works.
I am not sure about this approach. Is this feasible? Are there any examples where this pattern is used? I could not find much, but this might be because searching for "Angular observable pipe" yields not what I want.
Are there (good) alternatives to this approach?