user2120188
user2120188

Reputation: 437

Observable with Async Pipe in template is not working for single value

I have a component that asks my service for an Observable object (that is, the underlying http.get returns one single object).

The object (an observable) is used in conjunction with an async pipe in my template. Unfortunately, I get an error:

Cannot read property 'lastname' of null

I have been breaking my head on this one. A similar type of code works correctly on a list of objects (in conjunction with *ngFor).

<li>{{(person | async)?.lastname}}</li>

Method in my service:

getPerson(id: string): Observable<Person> {        
   let url = this.personsUrl + "/" + id;
   return this.http.get(url, {headers: new Headers({'Accept':'application/json'})})
              .map(r => r.json())
              .catch(this.handleError); //error handler
}

In my component:

//... imports omitted

@Component({
  moduleId: module.id,
  selector: 'app-details-person',
  templateUrl: 'details-person.component.html',
  styleUrls: ['details-person.component.css'],
})
export class DetailsPersonComponent implements OnInit, OnDestroy 
{
   person: Observable<Person>;
   sub: Subscription;

   constructor(private personService: PersonService, private route: ActivatedRoute) {
   }

  ngOnInit() {
     this.sub = this.route.params.subscribe(params => {
                 let persId = params['id'];
                 this.person = this.personService.getPerson(persId);
                });
  }

  ngOnDestroy(): void {
     this.sub.unsubscribe();
  }
}

Apparently the observable is/returns a null object value in the pipe. I have checked if I am really getting a nonempty Observable from my service and I can confirm that there exists an (underlying) object returned from my service.

Of course, I could subscribe to the observable in my component after having retrieved the Observable from the service, but I would really like to use the async construct.

Btw, another question: Is there already a pattern on how to handle errors that occur in the async pipe? (A downside of using async pipes.... errors are delayed until rendering time of the view.

Upvotes: 3

Views: 19362

Answers (4)

Simon_Weaver
Simon_Weaver

Reputation: 146188

This can occur if you use a Subject<T> observable as opposed to a ReplaySubject<T> or BehaviorSubject<T>.

What happens with a Subject<T> is that it 'broadcasts' its value only one time to whatever listeners are registered at that time.

So if you call next('hello word') on an observable message: Subject<string> it can only update the UI if that 'piece' of UI exists at that moment in time.

So in this example the message won't appear - unless you call next twice!

<div *ngIf="message | async">
   here is the message {{ message | async }}
</div>

The easiest solution is to use a ReplaySubject<string>(1) where 1 is the number of values it remembers (we only want one here).

Upvotes: 5

sorohan
sorohan

Reputation: 856

You don't need to subscribe to the route data observable in ngOnInit.

Rather than subscribing and creating a brand new observable every time the route data observable emits, you can chain together the route data with the personService observable into a new observable. This is the essence of functional programming:

The essence of functional reactive programming is to specify the dynamic behavior of a value completely at the time of declaration.

With that in mind, you can declare everything about "person" at it's time of declaration:

person: Observable<Person> = this.route.params.pipe(
  map(params => params.id),
  mergeMap(persId => this.personService.getPerson(persId)),
);

There are multiple benefits to this:

  • The observable won't actually trigger until something subscribes to it (like an async pipe). That means unless you are using the async pipe in your template, it won't make a request for the person.
  • You don't need to handle the unsubscribe. The async pipe will do that for you.
  • It's easy to see by looking at the code what makes the "Person". It's all defined in one place.
  • You don't need to make an empty initialization observable, which might not be a valid "Person".

Having said that, the async pipe can still default to null, which you can handle with an ngIf, in your template:

 <li *ngIf="(person | async) as person">{{person.lastname}}</li>

Upvotes: 4

user2084605
user2084605

Reputation: 51

Please try to change the line

person:Observable< Person>

into

person:Observable< any>;

If that does not work , print out this {{ person | async | json }} please with the change above of course and see what you get.

Upvotes: 2

Mark Rajcok
Mark Rajcok

Reputation: 364727

The first time your view renders, person is not defined, since that component property only gets created asynchronously, when the route params subscription fires. You need to initially create an empty observable. Instead of

person:Observable<Person>;

try

person = Observable.of<Person>(Person());  // or however you create an empty person object
    // the empty object probably needs a lastname field set to '' or null

I normally handle errors in a service by

  1. return an empty observable
  2. report the error to some app-wide (singleton) service. Some (singleton) component then displays those errors (if appropriate) somewhere on the page.

Upvotes: 4

Related Questions