Reputation: 13367
I am trying to build an upload form that is inspired by this article:
https://malcoded.com/posts/angular-file-upload-component-with-express/
I am using angular 9 and ngx-bootstrap and I am not using a dialog or angular material. So, I have levied the upload service like this:
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import {
HttpClient,
HttpRequest,
HttpEventType,
HttpResponse,
} from '@angular/common/http';
import { environment } from '@environments/environment';
@Injectable({
providedIn: 'root',
})
export class UploadService {
private url: string = `${environment.apiUrl}/halls/file`;
constructor(private http: HttpClient) {}
public upload(
files: Set<File>
): { [key: string]: { progress: Observable<number> } } {
// this will be the our resulting map
const status: { [key: string]: { progress: Observable<number> } } = {};
files.forEach((file) => {
// create a new multipart-form for every file
const formData: FormData = new FormData();
formData.append('file', file, file.name);
// create a http-post request and pass the form
// tell it to report the upload progress
const req = new HttpRequest('POST', this.url, formData, {
reportProgress: true,
});
// create a new progress-subject for every file
const progress = new Subject<number>();
// send the http-request and subscribe for progress-updates
this.http.request(req).subscribe((event) => {
if (event.type === HttpEventType.UploadProgress) {
// calculate the progress percentage
const percentDone = Math.round((100 * event.loaded) / event.total);
// pass the percentage into the progress-stream
progress.next(percentDone);
} else if (event instanceof HttpResponse) {
// Close the progress-stream if we get an answer form the API
// The upload is complete
progress.complete();
}
});
// Save every progress-observable in a map of all observables
status[file.name] = {
progress: progress.asObservable(),
};
});
// return the map of progress.observables
return status;
}
}
The only thing I have changed in the service is the url (for obvious reasons). I have then created a component to use this service:
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { Observable, forkJoin } from 'rxjs';
import { UploadService } from '@services';
import { Screenshot } from '../_core/models/screenshot';
@Component({
selector: 'app-screenshots-save',
templateUrl: './screenshots-save.component.html',
styleUrls: ['./screenshots-save.component.scss'],
})
export class ScreenshotsSaveComponent implements OnInit {
@ViewChild('file', { static: false }) file: ElementRef;
public screenshots: Screenshot[] = [];
public files: Set<File> = new Set();
public urls: string[] = [];
public progress: { [key: string]: { progress: Observable<number> } } = {};
public uploading: boolean = false;
public uploadSuccessful: boolean = false;
constructor(private uploadService: UploadService) {}
ngOnInit(): void {}
onFilesAdded(event: any): void {
const files: { [key: string]: File } = this.file.nativeElement.files;
for (let key in files) {
if (!isNaN(parseInt(key))) {
var f = files[key];
let reader = new FileReader();
reader.onload = (e: any) => {
this.urls.push(e.target.result);
};
this.files.add(f);
reader.readAsDataURL(f);
}
}
}
addFiles(): void {
this.file.nativeElement.click();
}
upload(): void {
this.uploading = true;
this.progress = this.uploadService.upload(this.files);
let allProgressObservables = [];
for (let key in this.progress) {
allProgressObservables.push(this.progress[key].progress);
}
forkJoin(allProgressObservables).subscribe(() => {
this.uploadSuccessful = true;
this.uploading = false;
});
}
clear(): void {
this.files = new Set();
this.urls = [];
this.progress = {};
this.uploading = false;
this.uploadSuccessful = false;
}
}
This is slightly difference to his, because it doesn't use a dialog, but the majority of it is the same. The main part is the upload
method. You can see that I am attaching the progress to the upload service in this line this.progress = this.uploadService.upload(this.files);
.
In my html, I have done this:
<form>
<div class="d-none">
<label>Screenshots</label>
<input #file class="form-control-file" type="file" multiple accept=".jpg,.png"
(change)="onFilesAdded($event)" />
</div>
<div class="form-group">
<button class="btn btn-dark mr-2" type="button" (click)="addFiles()">Add
screenshot(s)</button>
<button class="btn btn-primary mr-2" type="button" [disabled]="!files.size"
(click)="upload()">Upload</button>
<button class="btn btn-danger" type="button" [disabled]="!files.size"
(click)="clear()">Clear</button>
</div>
</form>
<div class="row">
<div class="col-md-4" *ngFor="let file of files; let i = index">
<div class="card mb-4">
<img class="card-img-top" [src]="urls[i]" [alt]="file.name">
<div class="card-body">
<h5 class="card-title">{{ file.name }}</h5>
<progressbar [value]="progress[file.name]?.progress | async" [striped]="true"
[animate]="true">
</progressbar>
</div>
</div>
</div>
</div>
As you can see, I am binding the progress to the ngx-bootstrap progress bar. When I add my files, it shows as 0, which it should. But when I press the upload button all of the files progress show 100 even though it hasn't actually finished uploading. There is a delay before it moves to my next screen which means that the progress bar isn't changing as I expected. It's either 0 or 100 instead of being incremental.
Can anyone spot what I am doing wrong?
Upvotes: 7
Views: 3227
Reputation: 1130
try adding observe: 'events',
to the request like this:
const req = new HttpRequest('POST', this.url, formData, {
reportProgress: true,
observe: 'events',
});
Upvotes: 0
Reputation: 780
I have the same problem, but it's not an Angular one. The problem is that your internet connection's upload speed is too high, so it goes from 0 to 100 very fast, so the upload service will go from next(0) to next(100) with a blink of an eye. Therefore, the server doesn't have the time to send you progress values.
In order to verify this, try loading your page first, then use the dev tools from Firefox or Chrome, go to 'Network' tab, click 'Online' and select 'Slow 3G'. Also, try using a larger file, this would help you see longer progress in upload-bar.
For the dev tools, you have can do F12 (for Windows), go to network and you will find bandwidth and you will find some default values (you can add your own speed)
EDIT 11/12/2020
I will share one working code (in production and working), the main diff is that i have observe: 'events' in my upload service and you have reportProgress: true, try to change it like mine
My progress service
upload(beanId: string, fileFormData: FormData, docType: DocTypeCode): Observable<number> {
const progress$ = new BehaviorSubject(0);
this.httpService.securePostWithProgress(this.localConfig.getUrls().DOCUMENT + `/upload/${docType}`, null, fileFormData, 'text')
.pipe(
tap((event: HttpEvent<any>) => {
switch (event.type) {
case HttpEventType.Sent:
break;
case HttpEventType.ResponseHeader:
break;
case HttpEventType.UploadProgress:
const progress = Math.round(event.loaded / event.total * 100);
progress$.next(progress);
break;
case HttpEventType.Response:
setTimeout(() => {
progress$.complete();
}, 500);
}
}),
catchError((e) => {
progress$.error(e);
throw e;
})
).subscribe();
return progress$.asObservable()
}
the upload service
securePostWithProgress(url: string, params: HttpParams, body?: FormData, responseType?: any): Observable<any> {
this.token = this.tokenService.getToken();
const headers = HttpHeaders.createInstance()
.addAccessToken(this.token)
.getHeaders();
const options = { headers, params, responseType: responseType, observe: 'events', reportProgress: true };
const req = new HttpRequest('POST', url, body, options);
console.log('url for securePost request : ' + url);
return this.http.request(req);
}
And the FormData
const formData = new FormData();
formData.append('file', imgBlob, name);
this.uplodaDocument(formData, docTypeCode);
Upvotes: 3