30

Can anyone help me see if there is a syntax error here in my template? It does not give error, but does not fill in data into the template either:

<div *ngIf="(hero | async)">
  <h2>{{hero}}</h2>
  <h2>{{hero.name}} details!</h2>
  <div>
    <label>_id: </label>{{hero._id}}</div>
  <div>
    <label>name: </label>
    <input [(ngModel)]="hero.name" placeholder="name" />
  </div>
  <button (click)="goBack()">Back</button>
</div>

Component code

export class HeroDetailComponent implements OnInit {
    errorMessage: string;
       
    @Input() hero: Observable<Hero>;
        
    constructor(
        private _heroService: HeroService,
        private _routeParams: RouteParams
    ) {}
       
    ngOnInit() {
        let _id = +this._routeParams.get('_id');
        this._heroService.loadHero(_id);
        this.hero = this._heroService.hero$;
        this.hero.subscribe(data => 
           console.log(data)
        );
    }
}

The console.log(data) prints:

Object {_id: 11, name: "Mr. Nice"}

which means that the data is correctly retrieved.

The <div> block also shows up, which mean *ngIf sees the object as non-empty.

<h2>{{hero}}</h2> shows [object Object].

But why the {{hero.name}} is not showing?

PaulBunion
  • 346
  • 2
  • 18
Shawn
  • 5,130
  • 13
  • 66
  • 109
  • You have a few things mixed up. `hero` is an input property, but then you assign it a value in ngOnInit() -- that's odd. The assigned value is an Observable, which doesn't have a `name` property, which explains why `{{hero.name}}` won't work. This answer should help you: http://stackoverflow.com/a/34561532/215945 – Mark Rajcok Apr 22 '16 at 22:22
  • I updated the post and remove the @input. But still the same. The async pipe is supposed to turn the observable into hero object? – Shawn Apr 22 '16 at 22:45

6 Answers6

66

Objects are a bit tricky with the async pipe. With an Observable that contains an array, we can use NgFor and create a local template variable (hero below) that gets assigned each item of the array after the async pipe extracts the array from the Observable. We can then use that variable elsewhere in the template:

<div *ngFor="let hero of heroes | async">
  {{hero.name}}
</div>
<!-- we can't use hero here, outside the NgFor div -->

But with an Observable that contains a single object, I'm not aware of any way to create a local template variable that will reference that object. Instead, we need to do something more complicated:

<div>{{(hero | async)?.name}}</div>

And we would need to repeat that for each property of the object we want to display. (The above line assumes that component property hero is an Observable.)

It is probably easier to assign the object (that is inside the Observable, hero$ below) to a property of the component, using component logic:

this._heroService.hero$.subscribe(data => this.hero = data.json());

and then use NgIf or the Elvis/safe navigation operator to display the data in the view:

<div *ngIf="hero">{{hero.name}}</div>
<!-- or -->
<div>{{hero?.name}}</div>
Ketan
  • 5,861
  • 3
  • 32
  • 39
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 1
    {{(hero | async)?.name}} won't work. I did the second way: subscribe to get data into component property and reference it in template. Thanks for taking the time. I appreciate it. – Shawn Apr 23 '16 at 03:53
  • 4
    {{(hero | async)?.name}} - this is just perfect. Saved me a lot of time... – dot net learner Nov 24 '16 at 19:36
25

This is now possible using the 'as' syntax, available in v4.0.0:

<span *ngIf="user$ | async as user; else loadingUserInfo">
 {{user.firstName}} {{user.lastName}}
</span>
<ng-template #loadingUserInfo>
  Loading user information...
</ng-template>

More details available in the RFC thread on github.

jmcmichael
  • 611
  • 1
  • 7
  • 11
22

Another option would be to use @Input and leverage smart/dumb component approach. In your smart component you can pass the async object to the dumb component then in the dumb component you can use it like a normal object.

The idea is that your smart component deals with logic and data and the dumb component handles presentation.

Smart component:

<dumb-component [myHero]="hero$ | async"></dumb-component>

Dumb component class:

@Input() myHero: Hero;

Dumb component template:

<div>{{ myHero.name }}</div>
chris cooley
  • 1,325
  • 10
  • 10
1

I am just adding a precision on how to use the smart / dumb component approach in the case you need to use the async pipe and a bracket syntax.

That combine a trick found here.

< ui-gallery-image([image]="(imagesFB | async) ? (imagesFB | async)[0] : null") ></ui-gallery-image>

I've spend hours to find. Hope this help. More info on this blog post.

Community
  • 1
  • 1
slucas
  • 484
  • 5
  • 8
1

The best way to handle a Single Observable Object inside an Angular 2.3.x, or Angular 4.x template is to use an async pipe with a template variable.

Here's a common goal for angular developers. Take an array of elements from redux, and pluck a single matching element from the collection. Then render that singular object in a template.

COMPONENT

@Component({
  selector: 'app-document-view',
  templateUrl: './document-view.component.html',
  styleUrls: ['./document-view.component.scss']
})
export class DocumentViewComponent implements OnInit {

  @select(['documents', 'items']) readonly documenList$: Observable<DocumentDTO[]>;
  public documentVO$: Observable<DocumentDTO>;

  constructor(private _state: NgRedux<IAppState>,
              private _route: ActivatedRoute,
              private _documentActions: DocumentActions) {

    _route.params.subscribe(params => {
      let modelId: number = parseInt(params['modelId']); //1          
      let documentId: number = parseInt(params['documentId']); //50
      this._documentActions.getDocument(modelId, documentId);
    });
  }

  ngOnInit() {

    //documenList holds all of the documents in our application state
    //but this view only wants a single element

    this.documentVO$ = this.documenList$.map(documents => documents.find(doc => doc.documentId === 50));
  }
}

VIEW

<div class="row" *ngIf="documentVO$ | async as dto">
    <div id="about" class="col-12">
        <div id="title" class="paper meta">
            <p>{{ dto.title }}</p>
        </div>
    </div>
</div>
Jack Murphy
  • 2,952
  • 1
  • 30
  • 49
0
  1. Async pipe subscribe
  2. Unsubscribe
  3. Call markForCheck() for OnPush
@Component({
    selector: 'product-alt',
    templateUrl: './products-alt.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductAltComponent implements OnDestroy {
    pageTitle: string = '';
    pageTitle$ =  (new BehaviorSubject<string>('InitialTitle'));
   
    constructor(private cdr: ChangeDetectorRef) {
        // Asyc pipe subscribes
        this.pageTitle$.subscribe((title: string) => {
            this.pageTitle = title;
            // call markForCheck
            this.cdr.markForCheck()
        });
    }
    
    ngOnDestroy() {
        // Unsubscribe
        this.pageTitle$.unsubscribe();
    }
}
PaulBunion
  • 346
  • 2
  • 18