Angular 2+ Upload de arquivo com barra de progresso

O objetivo deste artigo é mostrar como construir um componente de barra de progresso e como efetuar um upload mostrando a porcentagem em tempo real.

Construção do componente barra de progresso

Sem muita enrolação, vamos criar um módulo para o componente barra de progresso:

import { NgModule } from '@angular/core';
import { BarraProgressoComponent } from './barra-progresso.component';

@NgModule({
  declarations: [
    BarraProgressoComponent,
  ],
  exports: [
    BarraProgressoComponent,
  ]
})
export class BarraProgressoModule {

}
barra-progresso.module.ts

Para quem está começando, o exports faz com que o componente BarraProgressoComponent possa ser utilizado por um outro módulo que importe este módulo.

O componente terá apenas uma entrada de dados, que será o valor da barra de progresso em porcentagem:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-barra-progresso',
  templateUrl: './barra-progresso.component.html',
  styleUrls: ['./barra-progresso.component.css']
})
export class BarraProgressoComponent {
  @Input() porcentagemAtual: number = 0;
}
barra-progresso.component.ts

O estilo visual ficará bem simples e funcional, então um pouco de CSS já resolverá o problema:

:host {
  background: #efefef;
  display: block;
}

div {
  background: green;
  height: 10px;
  width: 75%;
}

:host, div {
  border-radius: 8px;
}
barra-progresso.component.css

Para quem ainda não utilizou, o :host refere-se ao <app-barra-progresso>. Da mesma forma que aplicamos um estilo em cima de um <button> ou um <div>, podemos também aplicar (utilizando o :host) em cima do seletor do nosso componente, que neste caso é o app-barra-progresso.

Por fim o template do componente:

<div [style.width.%]="porcentagemAtual"></div>
barra-progresso.component.html

Testando o componente barra de progresso

Agora que o componente está pronto, vamos importá-lo no AppModule e adicionar algumas referencias no template do AppComponent para ver como ele é renderizado na tela:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { HelloComponent } from './hello.component';
import { BarraProgressoModule } from './barra-progresso/barra-progresso.module';

@NgModule({
  imports:      [ BrowserModule, FormsModule, BarraProgressoModule ],
  declarations: [ AppComponent, HelloComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }
app.module.ts
<app-barra-progresso [porcentagemAtual]="0"></app-barra-progresso>
<br>
<app-barra-progresso [porcentagemAtual]="25"></app-barra-progresso>
<br>
<app-barra-progresso [porcentagemAtual]="50"></app-barra-progresso>
<br>
<app-barra-progresso [porcentagemAtual]="75"></app-barra-progresso>
<br>
<app-barra-progresso [porcentagemAtual]="100"></app-barra-progresso>
app.component.html - Template do AppComponent
Renderização do componente de barra de progresso

É possível ver que a renderização da barra de progresso está respeitando a porcentagem que passamos no [porcentagemAtual].

Vamos avançar um pouco mais e ver como integrar essa barra de progresso ao upload de um arquivo.

Upload de arquivos com barra de progresso

Para quem já trabalhou com upload de arquivos utilizando AJAX, já deve conhecer bem o FormData.

A interface FormData fornece uma maneira fácil de construir um conjunto de pares chave/valor representando campos de um elemento form e seus valores, os quais podem ser facilmente enviados utilizado o método send() do XMLHttpRequest. Essa interface utiliza o mesmo formato que um form utilizaria se o tipo de codificação estivesse configurado como "multipart/form-data".

fonte: https://developer.mozilla.org/pt-BR/docs/Web/API/FormData

O FormData utiliza a estrutura de dados multipart/form-data, que inclusive comentei em um outro post: Upload de arquivos e imagens utilizando o Multer - express - Node.js

Então a ideia é bem simples, vamos criar um formulário com um <input type="file"> e no evento onChange vamos pegar os detalhes do arquivo que o usuário selecionou no método preview:

<label for="meuArquivo">Arquivo</label>
<input type="file" id="meuArquivo" (change)="preview($event)" />
app.component.html
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  preview(event: Event) {
    const arquivos = (event.target as HTMLInputElement).files;
    this.arquivo = arquivos[0];

    console.log('   Nome: ', this.arquivo.name);
    console.log('Tamanho: ', this.arquivo.size);
    console.log('   Tipo: ', this.arquivo.type);
  }
}
app.component.ts

Veja na animação abaixo que quando selecionamos o arquivo, o evento (change) é disparado e o método preview recebe os detalhes do evento. Nestes detalhes conseguimos obter algumas informações do arquivo selecionado:

Angular - upload de arquivos - obtendo informações sobre o arquivo

Preview do upload

Antes de entrarmos no upload, vamos mostrar na tela um preview do que o usuário selecionou para upload. Para efetuar essa tarefa, vamos precisar da ajuda do FileReader, que nos permite ler o bytes do arquivo selecionado e guardá-los em uma variável.

O objeto FileReader permite aplicações web ler assincronamente o conteúdo dos arquivos (ou buffers de dados puros) do computador do usuário, utilizando o objeto File ou Blob para especificar o arquivo ou os dados a serem lidos.

fonte: https://developer.mozilla.org/pt-BR/docs/Web/API/FileReader

Vamos fazer as seguintes alterações:

import { Component } from '@angular/core';
 
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  // Guarda a referência do arquivo selecionado
  arquivo: File;
  
  // Guarda os bytes obtidos através da leitura
  // do this.arquivo - utilizado no preview
  arquivoPreview: any;

  preview(event: Event) {
    const arquivos = (event.target as HTMLInputElement).files;
    this.arquivo = arquivos[0];

    const reader = new FileReader();
    
    // A leitura é feita de forma assíncrona. Então
    // quando a leitura for finalizada, a função abaixo
    // será executada
    reader.onloadend = () => {
      this.arquivoPreview = reader.result;
    };

    reader.readAsDataURL(this.arquivo);
  }
}
app.component.ts

E no template:

<label for="meuArquivo">Arquivo</label>
<input type="file" id="meuArquivo" (change)="preview($event)" />

<div class="preview">
  <img *ngIf="arquivoPreview" [src]="arquivoPreview" alt="preview" />
</div>

Observe que o this.arquivoPreview é passado no [src] do <img> e o resultado é:

Preview da imagem selecionado, antes do upload

Efetuando o upload do arquivo

Antes de mais nada, certifique-se de ter importado o módulo HttpClientModule do @angular/common/http para não pegar o erro:

ERROR NullInjectorError: R3InjectorError(AppModule)[HttpClient -> HttpClient -> HttpClient]: NullInjectorError: No provider for HttpClient!

Para efetuar o teste do upload, utilizei uma API construída no artigo: Upload de arquivos e imagens utilizando o Multer - express - Node.js

Então nosso endereço de API que receberá o upload será: localhost:3000/arquivos e minha aplicação angular está rodando em localhost:4200. Então para não termos problemas com CORS:

  • Chrome Windows: execute-o utilizando a seguinte opção:
    chrome.exe" --disable-web-security --disable-gpu --user-data-dir=~/chromeTemp.
  • Chrome macOS: open -n -a /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
  • Safari: vá até a aba Developer e selecione a opção Disable Cross-Origin Restrictions

As alterações no template ficaram bem simples, basicamente foi adicionado um <button> e um evento de (click):

<label for="meuArquivo">Arquivo</label>
<input type="file" id="meuArquivo" (change)="preview($event)" />

<div class="preview">
  <img *ngIf="arquivoPreview" [src]="arquivoPreview" alt="preview" />
</div>

<button (click)="efetuarUpload()"
        type="button">Upload</button>

E no componente construímos o método efetuarUpload() ativado pelo clique do botão Upload:

import { Component } from '@angular/core';
 
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  arquivo: File;
  arquivoPreview: any;

  constructor(private http: HttpClient) {

  }

  preview(event: Event) {
    const arquivos = (event.target as HTMLInputElement).files;
    this.arquivo = arquivos[0];

    const reader = new FileReader();
    reader.onloadend = () => {
      this.arquivoPreview = reader.result;
    };

    reader.readAsDataURL(this.arquivo);
  }

  efetuarUpload() {
    const formData = new FormData();
    formData.append('arquivo', this.arquivo);

    this.http.post(
      'http://localhost:3000/arquivos',
      formData
    ).subscribe(resultadoUpload => {
      console.log('Resultado do Upload', resultadoUpload);
    });
  }
}
app.component.ts

Veja que ao selecionar o arquivo e clicar no botão Upload, é efetuada uma requisição que retorna o httpStatusCode 201 indicando que o registro foi criado com sucesso:

Upload de imagem utilizando angular

Mostrando a porcentagem do upload com a barra de progresso

Para finalizar, vamos integrar a barra de progresso com a porcentagem do upload. Para isto precisamos "dizer" ao Angular que observe os eventos emitidos pela requisição HTTP e também nos reporte sobre o progresso do upload. Para fazer isto basta informar um terceiro argumento ({ reportProgress: true, observe: 'events' }) na chamada do http.post conforme o código abaixo mostra:

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


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  porcentagemUpload = 0;
  arquivo: File;
  arquivoPreview: any;

  constructor(private http: HttpClient) {

  }

  preview(event: Event) {
    const arquivos = (event.target as HTMLInputElement).files;
    this.arquivo = arquivos[0];

    const reader = new FileReader();
    reader.onloadend = () => {
      this.arquivoPreview = reader.result;
    };

    reader.readAsDataURL(this.arquivo);
  }

  efetuarUpload() {
    const formData = new FormData();
    formData.append('arquivo', this.arquivo);

    this.http.post(
      'http://localhost:3000/arquivos',
      formData,
      { reportProgress: true, observe: 'events' }
    ).subscribe(resultadoUpload => {

      if (resultadoUpload.type === HttpEventType.UploadProgress) {
        const { loaded, total } = resultadoUpload;
        const progresso = Math.round(100 * (loaded / total));
        this.porcentagemUpload = progresso;

        console.log('Progresso: ', progresso);
        return;
      }

      if (resultadoUpload.type === HttpEventType.Response) {
        console.log('Resultado do Upload:', resultadoUpload);
      }
    });
  }
}
app.component.ts

Antes dessa alteração o subscribe emitia um valor quando a requisição era finalizada. Agora o subscribe emitirá vários valores e eventos, então vamos pegar o evento UploadProgress para calcular a porcentagem do upload e depois pegar o Response indicando que a requisição foi finalizada.

Durante o UploadProgress calculamos a porcentagem do upload e passamos o valor para uma variável chamada porcentagemUpload que é passada como parâmetro na barra de progresso, veja no template:

  <label for="meuArquivo">Arquivo</label>
  <input type="file" id="meuArquivo" (change)="preview($event)" />

  <div class="preview">
    <img *ngIf="arquivoPreview" [src]="arquivoPreview" alt="preview" />
  </div>

  <button (click)="efetuarUpload()" type="button">Upload</button>

  <app-barra-progresso [porcentagemAtual]="porcentagemUpload">
  </app-barra-progresso>
app.component.html

Não se esqueça de importar o BarraProgressoModule no seu AppModule!

Como a API está rodando no meu computador, utilizei um recurso bem interessante do Chrome para simular uma redução na velocidade das requisições. Na aba Network tem um dropdown com a opção padrão Online que pode ser alterada para por exemplo, slow 3G, para simular uma conexão mais lenta. Muito útil durante o desenvolvimento :)

Desta forma conseguimos ver bem a barra de progresso trabalhando, veja no GIF abaixo o resultado final:

Upload de arquivo com barra de progresso

Considerações

Neste artigo mostramos como construir um componente de barra de progresso, como efetuar o upload de arquivos utilizando Angular e como integrar a porcentagem do upload com a barra de progresso.

Vou deixar abaixo a implementação do projeto, lembrando que como o projeto está rodando no stackblitz, o localhost:3000(API que recebe o upload) não irá funcionar, então apontei para um link do mocky.io só para exemplificar mas deixei comentado no código:

angular-upload-barra-progresso - StackBlitz
Exemplo de como fazer upload de arquivos com barra de progresso
Exemplo da implementação do projeto

Links interessantes: