How to upload images in Angular with a Drag & Drop component

Luis Aviles

There is no doubt that we're living in an era in which images and videos are an important part of modern web applications. For example, it is very common to find a requirement to upload images for a photo gallery, or to upload a photo for your profile.

In this MediaJam, we'll use the Angular framework, and a TypeScript-oriented solution, to create a component with the capability to upload image files from your computer from scratch.

The HTML Drag and Drop API

There is an interesting specification about the Drag and drop behavior. This has been implemented in modern browsers as part of the Web APIs, available today.

This API allows us to use JavaScript to handle the drag-and-drop features in browsers. In terms of the MDN documentation:

The user may select draggable elements with a mouse, drag those elements to a droppable element, and drop them by releasing the mouse button. A translucent representation of the draggable elements follows the pointer during the drag operation.

The good news about this API is the availability of several event types, which are going to be fired, and we can take control over them. Also, the API defines useful Interfaces such as DragEvent, DataTransfer, and others which we'll see later.

The Problem

Let's suppose you're building a web application that needs to upload multiple image files. A drop-box area should be defined so that users can drag and drop the files over it. Then, the application should display a preview section with all the images uploaded. Finally, the user can add images anytime.

To clarify the idea, see the next screenshot.

Please note that this MediaJam is meant to define the frontend logic to meet these requirements. At least for now, we will not transfer the image files to any external server. All of them will be processed and rendered in the browser only.

The Solution

Prerequisites

You'll need to have the following tools installed in your local environment:

  • The latest LTS version of Node.js version available is recommended.
  • Either NPM or Yarn as a package manager.
  • The Angular CLI tool (Command-line interface for Angular).

Initialize the Project

Let's create a small project from scratch using the Angular CLI tool.

You can create an initial project using the ng new tool. By default, it will ask some parameters while you're creating it. However, you have the option to configure them in a single command line as you will see below.

1ng new angular-upload-images-demo --routing --prefix corp --style css --minimal

This command will initialize a base project using some configuration options:

  • --routing. It will create a routing module.
  • --prefix corp. It defines a prefix to be applied to the selectors for created components(corp in this case). The default value is app.
  • --style css. The file extension for the styling files.
  • --minimal. Creates the project without any testing framework. Useful when you're working on a proof-of-concept project, for example.

The output of the previous command will be:

1CREATE angular-upload-images-demo/README.md (1032 bytes)
2CREATE angular-upload-images-demo/.gitignore (631 bytes)
3CREATE angular-upload-images-demo/angular.json (3157 bytes)
4CREATE angular-upload-images-demo/package.json (774 bytes)
5CREATE angular-upload-images-demo/tsconfig.json (538 bytes)
6CREATE angular-upload-images-demo/.browserslistrc (703 bytes)
7CREATE angular-upload-images-demo/tsconfig.app.json (287 bytes)
8CREATE angular-upload-images-demo/src/favicon.ico (948 bytes)
9CREATE angular-upload-images-demo/src/index.html (311 bytes)
10CREATE angular-upload-images-demo/src/main.ts (372 bytes)
11CREATE angular-upload-images-demo/src/polyfills.ts (2830 bytes)
12CREATE angular-upload-images-demo/src/styles.css (80 bytes)
13CREATE angular-upload-images-demo/src/assets/.gitkeep (0 bytes)
14CREATE angular-upload-images-demo/src/environments/environment.prod.ts (51 bytes)
15CREATE angular-upload-images-demo/src/environments/environment.ts (662 bytes)
16CREATE angular-upload-images-demo/src/app/app-routing.module.ts (245 bytes)
17CREATE angular-upload-images-demo/src/app/app.module.ts (393 bytes)
18CREATE angular-upload-images-demo/src/app/app.component.ts (1502 bytes)
19✔ Packages installed successfully.
20 Successfully initialized git.

If you pay attention to the generated files and directories, you'll see a minimal project structure for the source code too:

1|- src/
2 |- app/
3 |- app.module.ts
4 |- app-routing.module.ts
5 |- app.component.ts

On the other hand, the app.component.ts file will define the template, and the styles as part of the metadata.

Also, it's good to remind you that the styles defined there will be applied only to that component, and they are not inherited by any nested component.

Creating the Model

Let's create a model to represent an image file. Use the command ng generate interface to create it as follows:

1ng generate interface model/image-file

Then, update the content of the image-file.ts file with a TypeScript interface:

1export interface ImageFile {
2 file: File;
3 url: string;
4}

Creating the Angular Directive

Since we'll need control over a "Drop Box" element defined in the DOM. It would be a good idea to implement an attribute directive first.

With that attribute directive, you can change the background color and control the behavior over the box.

So, let's create it using the CLI command ng generate directive.

1ng generate directive directives/image-uploader

This command will create the image-uploader.directive.ts file under a new folder directives. Next, let's change the auto-generated content with the following code snippet.

1import {
2 Directive,
3 HostBinding,
4 HostListener,
5 Output,
6 EventEmitter,
7} from '@angular/core';
8import { ImageFile } from '../model/image-file';
9
10@Directive({
11 selector: '[corpImgUpload]',
12})
13export class ImageUploaderDirective {
14 @Output() dropFiles: EventEmitter<ImageFile[]> = new EventEmitter();
15 @HostBinding('style.background') backgroundColor;
16}

There are a couple of notes to cover, related to the previous code:

  • The @Directive() decorator's configuration specifies the attribute [corpImgUpload]. That means we can apply it later as <div corpImgUpload></div> to "draw" the drop box, and receive the files later.
  • The @Output() decorator defines the dropFiles property to be able to have an event binding through it. This can be important to process the "output" of this directive, which is a set of image files.
    • Please note the EventEmitter expects to emit an array of objects: ImageFile[]. The ImageFile was defined above as the model.
  • The @HostBinding decorator set the styles to the host element of the directive.
    • For example, if the directive is applied to a <div> element. Then the HostBinding will be equivalent to do: <div [style.background]="backgroundColor"></div>

Instead of assigning a set of hard-coded values for the background color, we can create a TypeScript enum to help. You can create it in the same file of the directive since it will be used in that context only:

1// image-uploader.directive.ts
2
3enum DropColor {
4 Default = '#C6E4F1', // Default color
5 Over = '#ACADAD', // Color to be used once the file is "over" the drop box
6}

All right! We're ready to use the Drag Events in the directive implementation. Let's start with the dragover and dragleave events:

1// image-uploader.directive.ts
2
3// ... imports
4@Directive({
5 selector: '[corpImgUpload]',
6})
7export class ImageUploaderDirective {
8 @Output() dropFiles: EventEmitter<ImageFile[]> = new EventEmitter();
9 @HostBinding('style.background') backgroundColor = DropColor.Default;
10
11 @HostListener('dragover', ['$event']) public dragOver(event: DragEvent) {
12 event.preventDefault();
13 event.stopPropagation();
14 this.backgroundColor = DropColor.Over;
15 }
16
17 @HostListener('dragleave', ['$event']) public dragLeave(event: DragEvent) {
18 event.preventDefault();
19 event.stopPropagation();
20 this.backgroundColor = DropColor.Default;
21 }
22}

Let's explain what's happening in that code snippet:

  • The backgroundColor attribute gets initialized with a default color (using the brand new enum).
  • A method dragOver has been added.
    • The parameter matches with the DragEvent interface, which represents a drag-and-drop interaction.
    • To "listen" to the DOM event, the @HostListener decorator has been used and the 'dragover' string matches with the event name of the API.
    • Finally, the background color gets updated with a different color.
  • A method dragLeave has been added with the same configuration as the previous method.
    • The only difference here is the background color change with the default value again.

Now, it's time to define the main method of the directive, which allows handling the 'drop' event.

1// image-uploader.directive.ts
2
3export class ImageUploaderDirective {
4 // attributes and other methods...
5
6 @HostListener('drop', ['$event']) public drop(event: DragEvent) {
7 event.preventDefault();
8 event.stopPropagation();
9 this.backgroundColor = DropColor.Default;
10
11 let fileList = event.dataTransfer.files;
12 let files: ImageFile[] = [];
13 for (let i = 0; i < fileList.length; i++) {
14 const file = fileList[i];
15 const url = window.URL.createObjectURL(file);
16 files.push({ file, url });
17 }
18 if (files.length > 0) {
19 this.dropFiles.emit(files);
20 }
21 }
22}

So what's happening there? Let's take a closer look:

  • Once the event is captured, again, the background color will be changed to the default one.
  • The event object now should have the details about the files that have been "dropped" over the component.
    • The event.dataTransfer represents a DataTransfer interface, which refers to the data that is transferred during a drag and drop interaction.
    • Then, event.dataTransfer.files represents a list of the files that have been transferred.
    • For every transferred file, the method is creating an ImageFile object (It contains the file itself, and an Object URL to be used to render the image).
  • If there's at least one file transferred, then the dropFiles will emit the data.

The directive is ready to be integrated with the app. Let's see how we can use it inside an Angular component next.

Creating the Wrapper Component

Let's update the app.component.ts file with the following template content:

1<div class="container">
2 <div class="row">
3 <div class="drop-box" corpImgUpload (dropFiles)="onDropFiles($event)">
4 <span class="message">Drop File Images Here</span>
5 </div>
6 </div>
7 <div class="row">
8 <img *ngFor="let file of files" [src]="file.url" />
9 </div>
10 </div>

The above code snippet defines the layout of the component.

  • The first row will display the drop box and applies the directive as an attribute (<div corpImgUPload>). At the same time, it creates an event binding to "capture" the incoming files (<div corpImgUpload (dropFiles)="onDropFiles($event)">).
  • The second row will render every image through the *ngFor, which is a structural directive in Angular.

To have the template ready, it would be useful to define some styles:

1.container {
2 display: flex;
3 flex-direction: column;
4 }
5
6 .drop-box {
7 min-height: 300px;
8 min-width: 300px;
9 display: table;
10 background-color: #c6e4f1;
11 border: solid 1px #75c5e7;
12 }
13
14 .row {
15 display: flex;
16 flex-direction: row;
17 align-items: center;
18 justify-content: center;
19 }
20
21 .message {
22 display: table-cell;
23 text-align: center;
24 vertical-align: middle;
25 color: #686868;
26 }
27
28 img {
29 width: 200px;
30 height: 200px;
31 }

Remember, both the template and the styles are defined inline in the app.component.ts file.

Finally, let's complete the component with the missing method and property.

1// app.component.ts
2import { Component } from '@angular/core';
3import { ImageFile } from './model/image-file';
4
5@Component({
6 selector: 'corp-root',
7 template: `
8 // Template go here
9 `,
10 styles: [
11 `
12 // Styles go here
13 `,
14 ],
15})
16export class AppComponent {
17 files: ImageFile[] = [];
18
19 onDropFiles(files: ImageFile[]): void {
20 this.files = [...this.files, ...files];
21 }
22}

The previous code snippet shows the onDropFiles method, which receives the transferred images as objects(ImageFile interface) and the app takes the new values to be added to the existing files array.

Fixing the Unsafe URL Values

If you give it a try with the current implementation, you'll see the images you drop in the application are not being rendered by the browser, and the browser's console will display a couple of errors:

1WARNING: sanitizing unsafe URL value blob:http://localhost:4200/a1bcee8c-4833-4578-a470-41669838efeb (see https://g.co/ng/security#xss)
2unsafe:blob:http://localhost:4200/
3GET unsafe:blob:http://localhost:4200/a1bcee8c-4833-4578-a470-41669838efeb net::ERR_UNKNOWN_URL_SCHEME

You can see them in the screenshot below too.

To prevent these errors, and ensure the image rendering, let's update the image-uploader.directive.ts file.

1// image-uploader.directive.ts
2
3// Other imports...
4import { DomSanitizer } from '@angular/platform-browser';
5
6@Directive({
7 selector: '[corpImgUpload]',
8})
9export class ImageUploaderDirective {
10 // attributes...
11
12 constructor(private sanitizer: DomSanitizer) {}
13
14 // Other event handlers...
15
16 @HostListener('drop', ['$event']) public drop(event: DragEvent) {
17 // No changes here...
18
19 for (let i = 0; i < fileList.length; i++) {
20 const file = fileList[i];
21 const url = this.sanitizer.bypassSecurityTrustUrl(
22 window.URL.createObjectURL(file)
23 );
24 files.push({ file, url });
25 }
26 // ...
27 }
28}

Let's take a closer look at the previous code snippet.

  • The DomSanitizer has been injected through the constructor.
    • This class helps to prevent Cross-Site Scripting Security(XSS) bugs. More details can be found here.
  • On other hand, the URL value we had before as a string is processed by the bypassSecureityTrusUrl function.
    • This method bypasses security and trusts the given value to be a safe URL.

Now, the URL is not a string anymore. Instead, it has the SafeUrl as the type, and that means the ImageFile interface needs to be updated as follows.

1// image-file.ts
2import { SafeUrl } from '@angular/platform-browser';
3
4export interface ImageFile {
5 file: File;
6 url: SafeUrl;
7}

Try the source code again using the ng serve -o command, and the images will be rendered after dropping them in the app!

Live Demo

Find the source code available in GitHub.

If you prefer, you can play around with the project in CodeSandbox too:

Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work.

Luis Aviles

Senior Software Engineer

Luis is a Senior Software Engineer and Google Developer Expert in Web Technologies and Angular. He is an author of online courses, technical articles, and a public speaker. He has participated in different international technology conferences, giving technical talks, workshops, and training sessions. He’s passionate about the developer community and he loves to help junior developers and professionals to improve their skills.
When he’s not coding, Luis is doing photography or Astrophotography.