1

I have a DashboardComponent that will display certain general data to the user. One such point of data is the number of Users we have on board.

I also have a UserService which has a method called getUsers() which returns JSON-mapped HTML response. So, in DashboardComponent I subscribe to this response:

this._uService.getUsers()
               .subscribe(
                 users => this.users,
                 error => alert(error)
               )

So now all I have to do is to display the length of this array in the DOM, with something like

We have {{users.length}} users on board!

(Or create a variable in DashboardComponent called companyCount and equate it to the users array length.)

The problem here is, when I do so, it tells me that users is as of yet undefined. Which is understandable as it's an observable and when it's trying to access it, it's not there. But I am not quite sure about the solution.

To me, the odd thing is that even if {{users.length}} (or even {{users[0]}}) gives an error, I can still use *ngFor #user of users, and display all the users nicely in a table, and it won't give me an error.

I found a way around, even though it's not what I want. I never actually get the users array but do this instead:

this._uService.getUsers()
               .subscribe(
                 users => this.userCount = user.length,
                 error => alert(error)
               )

and then in the template

<div *ngIf="userCount">We have {{userCount}} users on board!!</div>

So this div isn't displayed until the userCount is defined, meaning the subscription data has become available.

This does not look like a good solution to me, especially since on the dashboard I will eventually need to display stats with more complex calculations, tables etc.

What is the general approach to tackling this problem?

irenicus
  • 163
  • 1
  • 1
  • 5

2 Answers2

1

Use the Elvis or safe navigation operator

{{users?.length}} 

this way length is only evaluated when user!= null

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • That's a more elegant version of the solution I came up with... but let's say I wanna do a more complex calculation on the array (like adding all of its elements together and displaying the result) - how would I do it? Meaning, how would I display a result that needs a separate function to perform an operation on the array? -- Also, Can you give me a quick explanation as to why I can't display {{users[0]}} directly but can loop through users with the ngFor array just fine? I am sorry I don't seem to be able to put in line breaks. – irenicus Feb 18 '16 at 06:08
  • `users[0]` calls the `[]` operator on `users`. `null` doesn't have this operator therefore an exception is thrown. Obviously `ngFor` itself checks whether `users != null` before it accesses properties. I don't really understand your question about the complex calcolation. – Günter Zöchbauer Feb 18 '16 at 06:12
0

The component is attempting to render before the content arrives.

You can't control when the data arrives because it's fetched asynchronously. Instead, use the null object pattern to set a default value (ie null object not undefined) so you have something to check against.

service

this.users = [];

this._uService.getUsers()
               .subscribe(
                 users => this.users,
                 error => alert(error)
               )
               .startWith(this.users);

Note: startsWith() sets a null initial value (ie null object pattern), giving your template something to check against.

component

this._uService.getUsers().subscribe(users => this.users = users)

Note: There's no benefit to extracting and storing the length local to the component. It's best to avoid storing intermediate state as much as possible.

template

<div *ngIf="users.length > 0">We have {{ users.length }} users on board!!</div>

If you want to do more operations on the users array wrap the group with a <template> element.

template2

<template [ngIf]="users.length > 0">
<div>We have {{ users.length }} users on board!!</div>
<div *ngFor="#user of users">
  {{ user.name }}
</div>
</template>

Only the template contents will display in the ContentView. If users.length isn't greater then 0 no additional checks are made and the template contents don't show up in the ContentView.

If you're working with complex objects that are loaded asynchronously via an observable, I've found that adding a template wrapper around the template group to be the cleanest approach.

Evan Plaice
  • 13,944
  • 6
  • 76
  • 94
  • Well I could find neither `.startsWith` nor `.startWith`. I get a compiler error when I try to implement that. And if I do *ngIf users.length>0, it's still trying to access users.length, which gives an error. It's fine if I do *ngIf users though. – irenicus Feb 18 '16 at 21:02
  • @irenicus It's an import issue. To use rxjs operators (ex startWith) you'll have to install rxjs5 and import the operator with `import { startWith } from 'rxjs/operator/startWith';`. For a quick-and-dirty solution you could do what you already mentioned and just check against the existence of `users` since it won't exist until after the observer receives data. – Evan Plaice Feb 21 '16 at 07:28