Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular2 component view not updating when variable changes

I have a simple component that just renders a progress bar.

It initializes fine and the progress passes over to it just fine, but the template is not updating with the new values.

import {Component} from 'angular2/core';

@Component({
  selector : 'progress-bar',
  template : `
    <div class="progress-container">
      <div>{{currentProgress}}</div>
      <div [style.width.%]="currentProgress" class="progress"></div>
    </div>
  `,
  styles: [
    '.progress-container {width: 100%; height: 5px}',
    '.progress {background-color: #8BC34A; height:5px}'
  ]
})

export class ProgressBar {
  currentProgress: number;

  constructor() {
    this.currentProgress = 0;
  }

  onProgress(progress: number) {
    console.log(progress) //consoles correct percentages
    this.currentProgress = progress;
  }

  reset() {
    console.log(this.currentProgress) //is 100
    this.currentProgress = 0;
  }
}
~

Elsewhere

  ngOnInit() {
    this.httpFileService.progress$.subscribe((progress: number) => this.onProgress(progress));
  }

  onProgress(progress: number) {
    this.progressBar.onProgress(progress*100);
  }

I feel like I am missing something very remedial.

like image 267
Dreamlines Avatar asked Apr 17 '16 04:04

Dreamlines


1 Answers

You're going about this in a way that's working against the framework, and going to lead to much wailing and gnashing of teeth.

Right now, you're manually subscribing to an observable - httpFileService.progress$ - and then manually updating a property on a child ProgressBar component, bypassing angular's change detection mechanism - which is why the UI isn't updating. You could manually fire change detection after setting this property and the UI would update as expected - but again, you'd be working against the framework, so let's look at how to work with it instead:

I assume that "elsewhere" is a parent of your ProgressBar component - let's call that ElsewhereComponent.

@Component({
  selector: 'elsewhere',
  directives: [ProgressBar],
  template: ` 
    <div>
      <progress-bar [currentProgress]="httpFileService.progress$"></progress-bar>
    </div>  
   `
})
class ElsewhereComponent { 
  // you can remove the ngOnInit and onProgress functions you posted
  // you also don't need a reference to the child ProgressBar component
  // ... whatever else you have in this class ...
}

The most important thing to note here is the addition of [currentProgress] on the progress-bar component: This is telling angular that there is an input property named currentProgress on that component that should be bound to httpFileService.progress$.

But you have now lied to angular - as it stands,ProgressBar has no inputs at all, and angular will tell you about it when it tries to bind this nonexistent property to the given value. So we need to add the input property, and the preferred way of doing that is with the Input() decorator:

@Component({
  selector : 'progress-bar',
  pipes: [AsyncPipe]  //import this from angular2/core
  template : `
    <div class="progress-container">
      <div>{{currentProgress | async}}</div>
      <div [style.width.%]="currentProgress | async" class="progress"></div>
    </div>
  `
})
export class ProgressBar {
  @Input() currentProgress: Observable<number>;
  ...
  constructor(){ 
     // remove the line you have in here now
  }

}

There are two critical differences here to note: First, @Input() tells angular that currentProgress is an input property. We've also changed the type of that property from number to Observable<number> - this isn't strictly necessary, but it's useful because it allows the second critical difference:

AsyncPipe has been added to the component's pipes, and used in both of its template bindings to currentProgress. This is useful because it tells angular to handle all the dirtywork of subscribing to the Observable and updating the UI each time it emits a new value.

And that's all it takes: Both the width of the bar and the text above it will now update automatically to reflect the values emitted from your httpFileService, and you didn't have to write a single line of imperative code to make it happen.

like image 89
drew moore Avatar answered Sep 30 '22 17:09

drew moore