<div>
{{ (data | async)?.parameter }}
</div>
We do this because we have some sort of observable that we just want to fetch a single property from each response. This can lead over time to us writing more and more of these:
<div>
{{ (data | async)?.parameter }}
{{ (data | async)?.otherParam }}
{{ (data | async)?.otherParam2 }}
</div>
This quickly becomes more and more difficult to write, but it also creates multiple subscriptions on the underlying observable. Fortunately there are a few ways of avoiding this anti-pattern, but they each come with different trade offs and costs.
Parameter Observables
Instead of having one observable that we repeatedly subscribe to, we could make an observable for each parameter;parameter = data.map(item => item.parameter);
otherParam = data.map(item => item.otherParam);
otherParam2 = data.map(item => item.otherParam2);
Pros:
- This works well when there's exactly one copy of this data and gives us a cleaner template.
Cons:
- This strategy isn't going to work within an
ngFor
. - This doesn't eliminate all of the asyncs
- We still have multiple subscriptions
Smart Components / Dumb Components
A cleaner way of unrolling this problem is to use what are called Smart and Dumb components, or Presentation and Container Components. To take advantage of this strategy, we will use a parent component to handle the async subscription, and we will provide a child component the static information.In the Smart component we have an async pipe.
<data-view [data]="data | async"></data>
In the Dumb component we take the data in as an
@Input
property, and then we can refer to all of the parameters directly.<div>
{{ data.parameter }}
{{ data.otherParam }}
{{ data.otherParam2 }}
</div>
Pros:
- Single subscription to data object
Cons:
- Requires a separation between the stream definition and the rendering, which feels a little bit like the old Angular 1.x Controller and Scope.
Magic Array Pipe
Pipes are a very powerful part of Angular. One strategy I've used is to combine the ability for*ngFor
to iterate over an array with a local variable with our ability to turn anything into an array of one.To take advantage of this, we must define a pure pipe that transforms any object it's given into an array:
@Pipe({ name: 'array' })
export class ArrayPipe implements PipeTransform {
constructor() { }
transform(value: any, ...args: any[]): any {
if (value) {
return [value];
} else {
return [];
}
}
}
This then allows us to do this in our template:
<div *ngFor="let dataPoint of data | async">
{{ dataPoint.parameter }}
{{ dataPoint.otherParam }}
{{ dataPoint.otherParam2 }}
Pros
- This should work whether data is able to resolve or not, which should help handle error cases, like nulls and undefineds
- You could extend your array pipe to also solve the common problem of iterating over the keys found in maps / objects.
Cons:
- Harder to intuit what's going on due to the use of an ng-for
Coming Soon: ngIf with local assignment
Take a look at Misko Hevery's commit that adds local assignment.<div *ngIf="userObservable | async; else loading; let user">
Hello {{user.last}}, {{user.first}}!
</div>
This technique is probably the cleanest, giving us the best of all worlds. This has landed in Angular's master repository, and will be available in the next major release, expected in February.
Pros
- A single subscription regardless of how much data we want
- Doesn't require manual or controller mapping
- Will work in an
*ngFor