Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Observable and xhr.upload.onprogress event

I have two services.

1) UploadService - send file throug ajax to server, and also get and store progress value (inside xhr.upload.onprogress event handler)

2) SomeOtherService worked with DOM, get progress value from first service, displayed it on Bootstrap progressbar.

Because, xhr.upload.onprogress is asynchronious - i use Observable in first service:

constructor () {
    this.progress$ = new Observable(observer => {
        this.progressObserver = observer
    }).share();
}

private makeFileRequest (url: string, params: string[], files: File[]): Promise<any> {
    return new Promise((resolve, reject) => {
        let formData: FormData = new FormData(),
            xhr: XMLHttpRequest = new XMLHttpRequest();

        for (let i = 0; i < files.length; i++) {
            formData.append("uploads[]", files[i], files[i].name);
        }

        xhr.onreadystatechange = () => {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    resolve(JSON.parse(xhr.response));
                } else {
                    reject(xhr.response);
                }
            }
        };

        xhr.upload.onprogress = (event) => {
            this.progress = Math.round(event.loaded / event.total * 100);

            this.progressObserver.next(this.progress);
        };

        xhr.open('POST', url, true);
        xhr.send(formData);
    });
}

Insise second service, i subscribe on this observer:

this.fileUploadService.progress$.subscribe(progress => {
    this.uploadProgress = progress
});

this.fileUploadService.upload('/api/upload-file', [], this.file);

Now my problem:

That code does not work. In second service i get observed value only once, on 100%.

But if i insert (yes, with empty callback body)

setInterval(() => {
}, 500);

inside makeFileRequest method - i will get progress value inside second service every 500 milliseconds.

private makeFileRequest (url: string, params: string[], files: File[]): Promise<any> {
    return new Promise((resolve, reject) => {
        let formData: FormData = new FormData(),
            xhr: XMLHttpRequest = new XMLHttpRequest();

        for (let i = 0; i < files.length; i++) {
            formData.append("uploads[]", files[i], files[i].name);
        }

        xhr.onreadystatechange = () => {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    resolve(JSON.parse(xhr.response));
                } else {
                    reject(xhr.response);
                }
            }
        };

        setInterval(() => {
        }, 500);

        xhr.upload.onprogress = (event) => {
            this.progress = Math.round(event.loaded / event.total * 100);

            this.progressObserver.next(this.progress);
        };

        xhr.open('POST', url, true);
        xhr.send(formData);
    });
}

What is that? Why that happened? How i can correct use Observable with onprogress event without that setInterval?

UPD: after Thierry Templier answer inside

this.service.progress$.subscribe( data => { 
    console.log('progress = '+data); 
}); 

i got correct values, but that values not updated inside template dynamically, only on 100% again.

like image 367
Качалов Тимофей Avatar asked Sep 26 '22 10:09

Качалов Тимофей


2 Answers

I don't have the complete code so it's difficult to give you a precise answer.

The fact you tell that "In second service i get observed value only once, on 100%." makes me think that it should be related to the use of promises. As a matter of fact, promises can be resolved or rejected once. There is no support of multiple events contrary to observables.

If you can post a plunkr with you code, I'll be able to debug it so I should provide you a more precise answer.

Edit

I created a plunkr (http://plnkr.co/edit/ozZqbxIorjQW15BrDFrg?p=preview) to test your code and it seems that it works. By changing the promise into an observable, I get more progress hints.

makeFileRequest(url: string, params: string[], files: File[]): Observable> {
  return Observable.create(observer => {
    let formData: FormData = new FormData(),
        xhr: XMLHttpRequest = new XMLHttpRequest();

    for (let i = 0; i < files.length; i++) {
        formData.append("uploads[]", files[i], files[i].name);
    }

    xhr.onreadystatechange = () => {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                observer.next(JSON.parse(xhr.response));
                observer.complete();
            } else {
                observer.error(xhr.response);
            }
        }
    };

    xhr.upload.onprogress = (event) => {
        this.progress = Math.round(event.loaded / event.total * 100);

        this.progressObserver.next(this.progress);
    };

    xhr.open('POST', url, true);
    xhr.send(formData);
  });
}

Here is the traces I had:

app.component.ts:18 progress = 21
app.component.ts:18 progress = 44
app.component.ts:18 progress = 71
app.component.ts:18 progress = 100

With the promise, I only get two traces

like image 64
Thierry Templier Avatar answered Sep 29 '22 07:09

Thierry Templier


You can solve the issue by wrapping with zone.run(() => ...). Import NgZone, inject it into your constructor, and do something like this:

constructor(private zone: NgZone) {
    this.service.progress$.subscribe(progress => {
        this.zone.run(() => this.uploadProgress = progress);
    });
}

The above is what actually works with the [email protected]. Below is what I think should work, but simply didn't work in my tests.

The reason you are not seeing updates is because change detection in Angular 2 is performed by reference, and the reference to your observable property is not changing. You can solve this by telling angular that it should run change detection manually like so:

this.service.progress$.subscribe(progress => {
    this.uploadProgress = progress;
    this.cd.markForCheck();
});

where cd needs to be injected into your constructor like so:

 constructor(private cd: ChangeDetectorRef) {}

after importing ChangeDetectorRef from @angular/core.

For more details you can see this excellent article on Thoughtram. I have reproduced the most relevant tidbits but the whole article is worth reading!

... let’s take a look at this component:

@Component({
  template: '{{counter}}',
  changeDetection: ChangeDetectionStrategy.OnPush
})
class CartBadgeCmp {

  @Input() addItemStream:Observable<any>;
  counter = 0;

  ngOnInit() {
    this.addItemStream.subscribe(() => {
      this.counter++; // application state changed
    })
  }
}

... the reference of addItemStream will never change, so change detection is never performed for this component’s subtree. This is a problem because the component subscribes to that stream in its ngOnInit life cycle hook and increments the counter. This is application state change and we want to have this reflected in our view right?

... what we need is a way to detect changes for the entire path of the tree to the component where the change happened. Angular can’t know which path it is, but we do.

We can access a component’s ChangeDetectorRef via dependency injection, which comes with an API called markForCheck(). This method does exactly what we need! It marks the path from our component until root to be checked for the next change detection run.

Let’s inject it into our component: constructor(private cd: ChangeDetectorRef) {} Then, tell Angular to mark the path from this component until root to be checked:

  ngOnInit() {
    this.addItemStream.subscribe(() => {
      this.counter++; // application state changed
      this.cd.markForCheck(); // marks path
    })
  }

Boom, that’s it!

like image 36
Radu Avatar answered Sep 29 '22 07:09

Radu