I’ve struggled with displaying complex-object Observables in Angular. When I try to render a variable in my template, sometimes I get nothing, other times errors, combinations of the two, and on occasion, the value with no console errors. Dealing with null or undefined objects and properties is perplexing particularly with Observables. Lack of an opinionated way of displaying objects in your template in a desired format is often frustrating.
Handling asynchronous calls to back-end data-stores creates a new way of thinking for most. The evolution from callbacks to promises to observables and their variants (subjects and behavior subjects) has greatly improved the coding experience. I find that passing asynchronous data to components using a service and observable offers a reliable and easy to grasp concept. But often I know that I have data, it just doesn’t render.
Here are few approaches that can work if you have struggled like me. A working copy of the code below is on Stackblitz at https://stackblitz.com/edit/angular-7wojgn
Part I Auto subscribe
Let’s assume the you have a service injected in your component that returns an observable from an http get.
Consider a service method that gets
{ "name": "Bob", "roles": [ { "job": "carpenter" }, { "job": "woodworker" } ] }
after at least two seconds;
fetchMembers() {
return this.http
.get<any[]>(
"https://nulls-d2af3.firebaseio.com/members.json"
)
.delay(2000)
}
and, component code that calls service and logs the result.
mdata: Observable<any>[];
constructor(private _elementService: ElementService) {}
ngOnInit(){
this.mdata = this._elementService.fetchMembers();
console.log(“Members”, this.mdata)
When executed, and looking at the browser console, you see immediately that “mdata” is an observable, but it doesn’t yet have data because I have not yet subscribed to the observable. I can subscribe in the component, but you don’t need to. You can use the async pipe as an alternative which auto subscribes and unsubscribes for you.
Example A. Use async to auto subscribe and unsubscribe followed by the json pipe to convert the observable object to a json string. It will display the entire
<!—A. async and json pipe -->
{{mdata | async | json }}
object and is generally an easy way to show that data is available to the template. Use of {{ mdata | json }}
does not work because there is no subscription. Use of {{ mdata | async }}
does not work because the object has not been converted to strings for display.
Example B. Use *ngif and async to display selected data
<!—B. *ngif and async -->
<div *ngIf="mdata | async as mdata; else loading;">
<h2>{{mdata.name}} {{mdata.roles[0].job}} {{mdata.roles}} </h2>
</div>
<ng-template #loading>Loading User Data...</ng-template>
Example B will check to see if “mdata” is not null and will display “Loading User Data…” until the observable is resolved. Note that the async pipe automatically subscribes and unsubscribes to the observable. Compared to method A, expression interpolation, method B offers better granularity, and if combined with an *ngfor property arrays could display your data in final form. For this case, the template will, after two seconds, render the following:
Bob carpenter [object Object],[object Object]
As shown, {{ mdata.roles }}
is unable to display the json data without a property name.
If you plan to let the async pipe do the subscribing for you, this is probably a good point to write unit tests for your service and component.
Part II Self subscribe
Most tutorials on Observables subscribe in the component. Let’s see how that changes the data in the template.
Component code that calls the service, subscribes, and logs the result.
mdata: Observable<any>[];
constructor(private _elementService: ElementService) {}
ngOnInit(){
this._elementService.fetchMembers().subscribe(res => {
this.mdata = res
console.log('Members', this.mdata);
});
The first thing to notice when executed is Members (in the browser console) shows the asynchronous call when Members’ properties populate after two seconds.
<!—C. async and json pipe -->
{{mdata | json }}
<!—D. *ngif and async -->
<div *ngIf="mdata; else loading;">
<h2 {{mdata.name}} {{mdata.roles[0].job}} {{mdata.roles[0]}} </h2>
<ng-template #loading>Loading User Data...</ng-template>
For Examples C and D, we remove the async pipe and output identical values to Part I. But since we’re subscribed, we now have other options.
Part III – Dealing with nulls – Safe-Navigation and Elvis operators
<!—E. Value of an array property -->
{{mdata.name }}
Example E accesses the observable’s description property. When executed Bob renders on the page after two seconds. Everything seems fine until you look at the console. It is littered with errors saying “ERROR TypeError: Cannot read property '0' of undefined.” These errors are produced before the observable is resolved and the console can’t say “Oh, never mind.”
To stop the errors, let’s try the Elvis operator. Use of the Elvis operator is saying get someProperty on someObject if someObject exists. It’s form is objectname?.property and can deal with deeply nested properties.
<!—F. Value of an array property with Elvis
{{mdata?.name }}
It too renders Bob, and the console errors are gone. But what about {{ mdata?roles?.job }}
? Turns out that in Angular the Elvis operator works well on nested properties and stops errors when an array is present, but it doesn’t display any data or the "object, object" notation. The Elvis operator works well for non-arrayed complex objects but could be misleading. I find that most of my data starts as an array of objects.
<!—G. Value of an array property expression
{{ mdata && mdata.name }}
Interpolation in Angular {{ expression }}
evaluates the expression and we can use that evaluation to check for nulls or undefined. The table below shows some possibilities
HTML | Result | Comment |
---|---|---|
{{ mdata && mdata.roles }} |
[object Object] | No console errors |
{{ 1 && 2 }} |
2 | No console errors |
{{ 1 OR 2 }} |
1 | No console errors |
{{ 3 == 3 }} |
true | No console errors |
{{ “No data” && mdata.roles[1].job }} |
woodworker | Console errors – Cannot read property [roles] - types do not match |
{{ mdata && mdata.roles[1].job }} |
woodworker | No console errors – types match |
Top comments (0)