11

I'm new to Angular 2 and I'm facing a problem with async http request and binding with interpolation.

Here's my component:

@Component({
  selector: 'info',
  template: `<h1>{{model.Name}}</h1>`
})
export class InfoComponent implements OnInit {

    model: any;

    constructor(
        private _service: BackendService
    ) { }

    ngOnInit() {
         if (this.model == null) {
            this._service.observableModel$.subscribe(m => this.model = m);
            this._service.get();
        }     
    }
}

When the template is rendered I get an error because "model" is not set yet.

I solved the problem with this very ugly hack:

@Component({
    selector: 'info',
    template: `
  <template ngFor #model="$implicit" [ngForOf]="models | async">
  <h1>{{model.Name}}</h1>
  </template>
  `
})
export class NeadInfoComponent implements OnInit {

    models: Observable<any>;

    constructor(
        private _service: BackendService
    ) { }

    ngOnInit() {
         if (this.models == null) {
            this._service.observableModel$.subscribe(m => this.models = Observable.of([m]));
            this._service.get();
        }     
    }
}

My question is: how to defer the template rendering until my http call is completed or how interpolate the "model" values directly in template without binding to another component?

Thanks!

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492

2 Answers2

12

If you are returning an object from your server, you can use the safe navigation (previously "Elvis") operator (?.) in your template:

@Component({
  selector: 'info',
  template: `<h1>{{model?.Name}}</h1>`
})
export class InfoComponent implements OnInit {
    model: any;
    constructor(private _service: BackendService) { }

    ngOnInit() {
       this._service.getData().subscribe(m => this.model = m);
       // getData() looks like the following:
       //    return this._http.get('....')  // gets JSON document
       //       .map(data => data.json()); 
    }
}

See this answer for a working plunker.

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 1
    Thanks Mark! The Elvis did the trick! But I don't understand yet the render flow. The template is rendered every time there is any change in the component properties? – Tony Alexander Hild Jan 26 '16 at 20:30
  • 2
    @TonyAlexanderHild, during Angular change detection (which runs after every event), by default, all of your view/template bindings are dirty checked, meaning they are checked for changes. When data returns from the server, that's an event, so change detection runs. `model.Name` is dirty checked and found to have changed, so Angular updates the DOM. After Angular returns control to the browser, it sees the DOM change and updates what we see on the screen. – Mark Rajcok Jan 26 '16 at 20:40
0

This discussion lists a few strategies https://github.com/angular/angular/issues/6674#issuecomment-174699245

The question has many implications. In some cases, the observables should be managed outside of the framework.

A. You can scan all observables before you bootstrap

B. Bootstrap then scan all observables before passing the object to the top level component.

C. You can also change the changeDetection within the component to either trigger when inputs changes or manually trigger the change detector them.

D. If you're using |async then it should only be used at the top level if you don't feel like using .subscribe but you really should just use .subscribe.

E. If you're using |async everywhere then what you're really doing is inverting control over rendering to the observables which means we're back in the Angular1 days of cascading changes so you either need to do C, D, B, or A

ChangeDetectionStrategy doesn't seem to be working at the moment. You would simple set the component as Detached.

We can also use ngOnInit lifecycle hook to remove the component from the change detection tree. You would need to run this.ref.detach(); where ref is injected via ChangeDetectorRef

  ngOnInit() {
    this.ref.detach();
  }
  makeYourChanges() {
    this.ref.reattach(); // attach back to change detector tree

    this.data.value = Math.random() + ''; // make changes

    this.ref.detectChanges(); // check as dirty

    this.ref.detach(); // remove from tree
    // zone.js triggers changes
  }

ChangeDetectorRef

You can also not include zone.js and manually control all changes. You can also inject NgZone to run an operation outside of zone.js so it won't tell angular to trigger chanes. For example,

// this example might need a refactor to work with rxjs 5
export class Timeflies {
  pos   = 'absolute';
  color = 'red';
  letters: LetterConfig[];
  constructor(
    private service: Message,
    private el: ElementRef,
    private zone: NgZone) {

  }
  ngOnInit() {
    // initial mapping (before mouse moves)
    this.letters = this.service.message.map(
      (val, idx) => ({
        text: val,
        top: 100,
        left: (idx * 20 + 50),
        index: idx
      })
    );
    this.zone.runOutsideAngular(() => {
      Observable
        .fromEvent(this.el.nativeElement, 'mousemove')
        .map((e: MouseEvent) => {
          //var offset = getOffset(this.el);

          // subtract offset of the element
          var o = this.el.nativeElement.getBoundingClientRect();

          return {
            offsetX: e.clientX - o.left,
            offsetY: e.clientY - o.top
          };
        })
        .flatMap(delta => {
          return Observable
            .fromArray(this.letters
              .map((val, index) => ({
                letter: val.text,
                delta,
                index
              })));
        })
        .flatMap(letterConfig => {
          return Observable
            .timer( (letterConfig.index + 1) * 100)
            .map(() => ({
              text:  letterConfig.letter,
              top:   letterConfig.delta.offsetY,
              left:  letterConfig.delta.offsetX + letterConfig.index * 20 + 20,
              index: letterConfig.index
            }));
        })
        .subscribe(letterConfig => {
          // to render the letters, put them back into app zone
          this.zone.run(() => this.letters[letterConfig.index] = letterConfig);
        });

    });//zone
  }
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567