Artigo original: How to make image upload easy with Angular

Escrito por: Filip Jerga

Esta é a segunda parte do tutorial sobre como fazer o envio de uma imagem para o Amazon S3. Você pode encontrar a primeira parte aqui. Neste artigo, vamos dar uma olhada na parte do Angular.

Você também pode assistir ao meu tutorial passo a passo em vídeo do envio de uma imagem. O link é fornecido ao final deste artigo.

1. Crie um template primeiro

Primeiramente, queremos criar um componente reutilizável que possa ser facilmente acoplado a outros componentes.

Vamos começar com um template HTML simples para nossa entrada. Não se esqueça de aplicar os estilos de sua escolha. Você também pode obtê-los em meu repositório no GitHub.

<label class="image-upload-container btn btn-bwm">
  <span>Select Image</span>
  <input #imageInput
         type="file"
         accept="image/*"
         (change)="processFile(imageInput)">
</label>

O importante aqui é um tipo (type) para a entrada, que é definido como arquivo (file). O atributo accept define os arquivos aceitos como entrada. Image/* especifica que podemos escolher imagens de qualquer tipo através desta entrada. #imageInput é uma referência de entrada na qual podemos acessar os arquivos enviados.

Um evento change é acionado quando selecionamos um arquivo. Portanto, vamos dar uma olhada no código da classe.

2. Não se esqueça do código do componente

class ImageSnippet {
  constructor(public src: string, public file: File) {}
}

@Component({
  selector: 'bwm-image-upload',
  templateUrl: 'image-upload.component.html',
  styleUrls: ['image-upload.component.scss']
})
export class ImageUploadComponent {

  selectedFile: ImageSnippet;

  constructor(private imageService: ImageService){}

  processFile(imageInput: any) {
    const file: File = imageInput.files[0];
    const reader = new FileReader();

    reader.addEventListener('load', (event: any) => {

      this.selectedFile = new ImageSnippet(event.target.result, file);

      this.imageService.uploadImage(this.selectedFile.file).subscribe(
        (res) => {
        
        },
        (err) => {
        
        })
    });

    reader.readAsDataURL(file);
  }
}

Vamos dividir esse código em partes. Você pode ver no processFile que estamos recebendo uma entrada de imagem a qual recebemos do evento change. Ao gravar imageInput.files[0], estamos acessando o primeiro file/arquivo. Precisamos de um reader para poder acessar as propriedades adicionais de um arquivo. Ao chamar readAsDataURL, podemos obter uma representação de base64 de uma imagem na função de callback do addEventListener, que inserimos anteriormente.

Em uma função de callback, estamos criando uma instância do ImageSnippet. O primeiro valor é uma representação de base64 de uma imagem que exibiremos mais tarde na tela. O segundo valor é um arquivo em si, que enviaremos para o servidor para fazer o envio ao Amazon S3.

Agora, só precisamos fornecer este arquivo e enviar uma solicitação através de um serviço.

3. Precisamos de um serviço também

Não seria uma aplicação do Angular sem um serviço (em inglês, service). O serviço será o responsável por enviar uma solicitação ao nosso servidor do Node.

export class ImageService {

  constructor(private http: Http) {}


  public uploadImage(image: File): Observable<Response> {
    const formData = new FormData();

    formData.append('image', image);

    return this.http.post('/api/v1/image-upload', formData);
  }
}

Precisamos enviar uma imagem como parte do form data. Anexaremos a imagem sob a chave de uma image para form data (a mesma chave que configuramos anteriormente no Node). Por fim, precisamos apenas enviar uma solicitação ao servidor com formData em uma carga.

Agora, podemos comemorar. É isso aí! A imagem foi enviada para o upload!

Nas próximas linhas, fornecerei código adicional para uma melhor experiência de usuário.

4. Atualizações adicionais para UX

class ImageSnippet {
  pending: boolean = false;
  status: string = 'init';

  constructor(public src: string, public file: File) {}
}

@Component({
  selector: 'bwm-image-upload',
  templateUrl: 'image-upload.component.html',
  styleUrls: ['image-upload.component.scss']
})
export class ImageUploadComponent {

  selectedFile: ImageSnippet;

  constructor(private imageService: ImageService){}

  private onSuccess() {
    this.selectedFile.pending = false;
    this.selectedFile.status = 'ok';
  }

  private onError() {
    this.selectedFile.pending = false;
    this.selectedFile.status = 'fail';
    this.selectedFile.src = '';
  }

  processFile(imageInput: any) {
    const file: File = imageInput.files[0];
    const reader = new FileReader();

    reader.addEventListener('load', (event: any) => {

      this.selectedFile = new ImageSnippet(event.target.result, file);

      this.selectedFile.pending = true;
      this.imageService.uploadImage(this.selectedFile.file).subscribe(
        (res) => {
          this.onSuccess();
        },
        (err) => {
          this.onError();
        })
    });

    reader.readAsDataURL(file);
  }
}

Adicionamos novas propriedades a ImageSnippet: Pending e Status. Pending pode ser falso ou verdadeiro, dependendo do fato de a imagem estar sendo carregada no momento. O status é o resultado do processo de upload. Pode ser OK ou FAILED.

OnSuccess e onError são chamados após o upload da imagem e definem o status de uma imagem.

Certo. Agora, vamos dar uma olhada no arquivo de template atualizado:

<label class="image-upload-container btn btn-bwm">
  <span>Select Image</span>
  <input #imageInput
         type="file"
         accept="image/*"
         (change)="processFile(imageInput)">
</label>


<div *ngIf="selectedFile" class="img-preview-container">

  <div class="img-preview{{selectedFile.status === 'fail' ? '-error' : ''}}"
       [ngStyle]="{'background-image': 'url('+ selectedFile.src + ')'}">
  </div>

  <div *ngIf="selectedFile.pending" class="img-loading-overlay">
    <div class="img-spinning-circle"></div>
  </div>

  <div *ngIf="selectedFile.status === 'ok'" class="alert alert-success"> Image Uploaded Succesfuly!</div>
  <div *ngIf="selectedFile.status === 'fail'" class="alert alert-danger"> Image Upload Failed!</div>
</div>

Aqui, estamos exibindo nossa imagem carregada e os erros na tela, dependendo do estado de uma imagem. Quando a imagem está pendente, também exibimos uma bela imagem giratória para notificar o usuário sobre a atividade de upload.

5. Acrescentando a estilização

Estilos não são o foco deste tutorial. Portanto, você pode obter todos os estilos  em SCSS neste link.

Trabalho concluído! :) Esse deve ser todo o trabalho necessário para um upload de imagem. Se algo não estiver claro, certifique-se de ler primeiro a primeira parte deste tutorial.

Se gostar deste tutorial, fique à vontade para conferir o curso completo: The Complete Angular, React & Node Guide | Airbnb style app (em inglês).

Projeto concluído: meu repositório no GitHub

Videoaula: tutorial no YouTube (em inglês)