Upload файла в Angular по клику кнопки

Подготовка

В качестве бекенда принимающего файлы будет создано nestjs приложение.

$ npm i -g @nestjs/cli
$ nest new server

Приложение по-умолчанию работает на 3000 порту, чтобы разрешить запросы с 4200 необходимо включить CORS, для этого достаточно добавить строчку app.enableCors(); в основной файл приложения.

main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors(); // <===== 2 часа искал это решение
  await app.listen(3000);
}
bootstrap();

В контреллер добавляется хендлер upload, который ожидает файл в свойстве upload_file.

  @Post('upload')
  @UseInterceptors(FileInterceptor('upload_file'))
  uploadFile(@UploadedFile() file) {
    console.log(file);
    return { id: '1', name: file.originalname };
  }

С бекендом завершено, можно запускать:

npm start

Выбор и отправка файла по клику

Выбор и отправка файла по клику подразумевает, что на форме отсутствует input для файла.

  • Следует динамически создать input
  • Установить тип file
  • Повешать обработчик на событие change
    • В свойстве события event.target.files[0] будут содержаться выбранный пользователем файл.
    • Подготовить multipart
    • Отправить POST запрос на сервер.
    • Убрать ссылку на созданный input, чтобы сборщик мусора его удалил
  • Кликнуть на созданный input

Получается примерно такой обработчик:

uploadFile(): void {
    let fileInput = this.document.createElement('input');
    fileInput.type = 'file';
    fileInput.addEventListener('change', event => {
        const target = event.target as HTMLInputElement;
        const selectedFile = target.files[0];
        const uploadData = new FormData();
        uploadData.append('upload_file', selectedFile, selectedFile.name);
        // непосредственно отправить файл (post запрос)
        fileInput = null;
    });
    fileInput.click();
}

Отправка запроса с отслеживанием прогресса

Чтобы получить прогресс, нужно установить 2 свойства в опциях post запроса http клиента:

this.http.post('http://localhost:3000/upload', uploadData, {
    reportProgress: true, // Без observe: 'events' не работает
    observe: 'events', // без reportProgress: true только HttpEventType.Sent и HttpEventType.Response
})

Результат

Написано в реактивном стиле. Полностью можно посмотреть в репозитарии.

import { Component, Inject, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
import { HttpClient, HttpEventType } from '@angular/common/http';
import { DOCUMENT } from '@angular/common';

import { fromEvent, Subject } from 'rxjs';
import { mergeMap, finalize, takeUntil, first } from 'rxjs/operators';

@Component({
  selector: 'app-root',
  template: `
    <h1>Welcome to File upload!</h1>
    <button (click)="chooseAndUploadFile()">Upload!</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnDestroy {
  private destroy$ = new Subject<void>();

  constructor(
    @Inject(DOCUMENT) private document: Document,
    private http: HttpClient
  ) {}

  /**
   * Open file dialog and upload file to server
   */
  public chooseAndUploadFile(): void {
    let fileInput = this.document.createElement('input');
    fileInput.type = 'file';
    fromEvent(fileInput, 'change')
      .pipe(
        first(),
        mergeMap(event => {
          const target = event.target as HTMLInputElement;
          const selectedFile = target.files[0];
          // formData обязательно в 2 строчки
          const uploadData = new FormData();
          uploadData.append('upload_file', selectedFile, selectedFile.name);
          return this.http.post('http://localhost:3000/upload', uploadData, {
            reportProgress: true, // Без observe: 'events' не работает
            observe: 'events', // без reportProgress: true только HttpEventType.Sent и HttpEventType.Response
          });
        }),
        finalize(() => {
          // должен быть удален, т.к. счетчик ссылок обнулится
          fileInput = null;
          console.log('fileInput = null');
        }),
        takeUntil(this.destroy$)
      )
      .subscribe(
        event => {
          // console.log(event);
          switch (event.type) {
            case HttpEventType.Sent:
              console.log('Request sent!');
              break;
            case HttpEventType.ResponseHeader:
              console.log('Response header received!');
              break;
            case HttpEventType.UploadProgress:
              const kbLoaded = Math.round(event.loaded / 1024 / 1024);
              const percent = Math.round((event.loaded * 100) / event.total);
              console.log(
                `Upload in progress! ${kbLoaded}Mb loaded (${percent}%)`
              );
              break;
            case HttpEventType.Response:
              console.log('Done!', event.body);
          }
        },
        () => console.log('Upload error'),
        () => console.log('Upload complete')
      );
    fileInput.click();
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Единственный момент требующий пояснения это first(). Несмотря на то, что в реальности получается только 1 change событие из input'a и затем он удаляется, подписка на его событие change будет жить в ожидании следующих событий. Поэтому необходимо добавить оператор, который завершит поток. Подойдет first() или take(1).


Похожие записи

Переиспользуемые анимации в Angular

Для создания анимации используется функция animation(). Для её использования — useAnimation(). Приводится пример переиспользуемой анимации, а также создание анимации через литерал объекта.

09 октября 2018 г. в Angular

Angular routerLink conditionally

<a [routerLink]="myVar ? '/home' : null" routerLinkActive="is-active">Home</a>
or
<a [routerLink]="myVar ? ['/home'] : []">Home</a>
02 сентября 2019 г. в Angular

Angular Let Directive

*ngIf не отображает содержимое в falsy случаях (0, null, undefined) на async pipe, в пакете @rx-angular/template предлагается решение