Angular & ASP.NET Web API Multiple File Uploads

Photo by Daria Shevtsova from Pexels
Photo by Daria Shevtsova from Pexels

Overview

The API is available here and included in the Github Repository here.

The following uses Angular with Bootstrap (To keep it simple) and ASP.NET Web API to create/delete a record and 1 to 3 images. A title is saved in the parent table (Products) and the images are saved to a folder and the file name, size, mime type, and short path (unnecessary) in another table (ProductImages). The main take away from this, is to create and save the Parent and Child records in one API call using a form with a drop zone.

Below is an overview/diagram of what this example does. Code for the UI and API can be found on GitHub.

Install the following libraries

npm i bootstrap
npm i ngx-dropzone

Generate a service with ng g s services/image-file

Inside the service add the following promise call

 constructor(private http: HttpClient) { }

  addProductImages(fileForm: any, files: any): string {
    const URL = `${environment.baseUrl}/api/product/add`;

    const formData = new FormData();
    // Add Record Title
    formData.append('Title', fileForm.Title);
    // Add the file

    for (let i = 0; i < files.length; i++) {
      formData.append(`images[${i}]`, files[i])
    }

    // formData.append('Description', fileForm.Description);
    let status = '';

    // Use a promise for this example
    const promise = new Promise((resolve, reject) => {
      this.http.post(URL, formData)
        .toPromise()
        .then(
          res => { // Success
            console.log(res);
            status = 'resolved';
          }
        )
        .catch((err) => {
          console.error(err);
          status = 'rejected';
        });
    });
    return status;
  }


  getImages(): Observable<ResultsObj> {
    const URL = `${environment.baseUrl}/api/product/get`;
    console.log(URL);
    return this.http.get(URL);
  }


  removeProduct(id: number): Observable<ResultsObj> {
    const URL = `${environment.baseUrl}/api/product/remove/${id}`;
    console.log(URL);
    return this.http.delete(URL);
  }

The key to uploading all the files in the service is the following:


for (let i = 0; i < files.length; i++) {
  formData.append(`images[${i}]`, files[i])
}

Use a promise not an observable, since the API action is asynchronous

 const promise = new Promise((resolve, reject) => {
      this.http.post(URL, formData)
        .toPromise()
        .then(
          res => { // Success
            console.log(res);
            status = 'resolved';
          }
        )
        .catch((err) => {
          console.error(err);
          status = 'rejected';
        });
    });

Component HTML form

There is a title field with the ngx-dropzone component and a submit button.

<div class="card" >
  <div class="card-body">
    <div class="row mb-5">
      <div class="col-12">
        <h5>Upload Image</h5>
        <form class="post-form" method="POST" (ngSubmit)="onSubmit($event)" [formGroup]="imageForm" >
          
        <div class="row">
          <div class="col-12">
              <label>Parent Table ID</label>
              <input type="text" class="form-control" placeholder="Title" name="Title" formControlName="Title">
            </div>
        </div>

        <div class="row mt-3">
          <div class="col-12">
            <div class="custom-dropzone" ngx-dropzone [accept]="'image/*'" (change)="onSelect($event)">
              <ngx-dropzone-label>
                <div>
                  <h2>Dropzone</h2>
                  <h5>Drag & Drop Files Here</h5>
                </div>
              </ngx-dropzone-label>
              <ngx-dropzone-image-preview ngProjectAs="ngx-dropzone-preview" *ngFor="let f of files" [file]="f" [removable]="true" (removed)="onRemove(f)" >
                <ngx-dropzone-label>{{ f.name }} ({{ f.type }})</ngx-dropzone-label>
              </ngx-dropzone-image-preview>
            </div>
          </div>
        </div>

       <div class="row mt-3">
          <div class="col">
              <button class="btn btn-primary" type="submit" [disabled]="!imageForm.valid">Save Image & Title</button>
          </div>
      </div>
    </form>
  </div>
</div>

Component TypeScript File

This component will have onSelect and onRemove methods for ngx-dropzone. onSubmit will call the addProductImages() service, alert the parent to update the list, and reset the form.

export class FileFormComponent implements OnInit, OnDestroy {
  @Output() onSubmitForm = new EventEmitter<any>();
  
  private subs = new Subscription();
  private imageFormData = new MediaImageClass();
  public message: string;
  public imageForm: FormGroup;
  public files: File[] = [];

  constructor(private fb: FormBuilder,
              private fileSVC: ImageFileService) { }

  ngOnInit() {
    this.imageForm = this.fb.group({
      Title: ['', [Validators.required]],
    });

  }

  ngOnDestroy(): void {
    if (this.subs) {
     this.subs.unsubscribe();
    }
  }

  onSubmit($event) {
    this.imageFormData.Title = this.imageForm.controls.Title.value;    
    this.fileSVC.addProductImages(this.imageFormData, this.files);
    this.onSubmitForm.emit(this.imageForm);
    this.imageForm.reset();
    this.imageForm.controls.Title.setValue('');
    this.imageForm.controls.Title.setErrors(null);  
  }

// Dropzone related
  onSelect(event) {
    console.log(event);
    this.files.push(...event.addedFiles);
  }

  onRemove(event) {
    console.log(event);
    this.files.splice(this.files.indexOf(event), 1);
  }

}

Remember to get the solution from here.

Photo by Ann H from Pexels