Skip to content

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

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.

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.

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.

A Drag and Drop Component to upload Images - Mockup

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.

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).

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.

ng new angular-upload-images-demo --routing --prefix corp --style css --minimal
Code language: JavaScript (javascript)

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:

CREATE angular-upload-images-demo/README.md (1032 bytes)
CREATE angular-upload-images-demo/.gitignore (631 bytes)
CREATE angular-upload-images-demo/angular.json (3157 bytes)
CREATE angular-upload-images-demo/package.json (774 bytes)
CREATE angular-upload-images-demo/tsconfig.json (538 bytes)
CREATE angular-upload-images-demo/.browserslistrc (703 bytes)
CREATE angular-upload-images-demo/tsconfig.app.json (287 bytes)
CREATE angular-upload-images-demo/src/favicon.ico (948 bytes)
CREATE angular-upload-images-demo/src/index.html (311 bytes)
CREATE angular-upload-images-demo/src/main.ts (372 bytes)
CREATE angular-upload-images-demo/src/polyfills.ts (2830 bytes)
CREATE angular-upload-images-demo/src/styles.css (80 bytes)
CREATE angular-upload-images-demo/src/assets/.gitkeep (0 bytes)
CREATE angular-upload-images-demo/src/environments/environment.prod.ts (51 bytes)
CREATE angular-upload-images-demo/src/environments/environment.ts (662 bytes)
CREATE angular-upload-images-demo/src/app/app-routing.module.ts (245 bytes)
CREATE angular-upload-images-demo/src/app/app.module.ts (393 bytes)
CREATE angular-upload-images-demo/src/app/app.component.ts (1502 bytes)
✔ Packages installed successfully.
    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:

|- src/
    |- app/
        |- app.module.ts
        |- app-routing.module.ts
        |- 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.

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

ng generate interface model/image-file
Code language: PHP (php)

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

export interface ImageFile {
  file: File;
  url: string;
}
Code language: CSS (css)

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.

ng 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.

import {
  Directive,
  HostBinding,
  HostListener,
  Output,
  EventEmitter,
} from '@angular/core';
import { ImageFile } from '../model/image-file';

@Directive({
  selector: '[corpImgUpload]',
})
export class ImageUploaderDirective {
  @Output() dropFiles: EventEmitter<ImageFile[]> = new EventEmitter();
  @HostBinding('style.background') backgroundColor;
}

Code language: JavaScript (javascript)

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:

// image-uploader.directive.ts

enum DropColor {
  Default = '#C6E4F1', // Default color
  Over = '#ACADAD', // Color to be used once the file is "over" the drop box
}
Code language: PHP (php)

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

// image-uploader.directive.ts

// ... imports 
@Directive({
  selector: '[corpImgUpload]',
})
export class ImageUploaderDirective {
  @Output() dropFiles: EventEmitter<ImageFile[]> = new EventEmitter();
  @HostBinding('style.background') backgroundColor = DropColor.Default;

  @HostListener('dragover', ['$event']) public dragOver(event: DragEvent) {
    event.preventDefault();
    event.stopPropagation();
    this.backgroundColor = DropColor.Over;
  }

  @HostListener('dragleave', ['$event']) public dragLeave(event: DragEvent) {
    event.preventDefault();
    event.stopPropagation();
    this.backgroundColor = DropColor.Default;
  }
}
Code language: PHP (php)

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.

// image-uploader.directive.ts

export class ImageUploaderDirective {
  // attributes and other methods...

  @HostListener('drop', ['$event']) public drop(event: DragEvent) {
    event.preventDefault();
    event.stopPropagation();
    this.backgroundColor = DropColor.Default;

    let fileList = event.dataTransfer.files;
    let files: ImageFile[] = [];
    for (let i = 0; i < fileList.length; i++) {
      const file = fileList[i];
      const url = window.URL.createObjectURL(file);
      files.push({ file, url });
    }
    if (files.length > 0) {
      this.dropFiles.emit(files);
    }
  }
}
Code language: JavaScript (javascript)

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.

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

  <div class="container">
    <div class="row">
      <div class="drop-box" corpImgUpload (dropFiles)="onDropFiles($event)">
        <span class="message">Drop File Images Here</span>
      </div>
    </div>
    <div class="row">
      <img *ngFor="let file of files" [src]="file.url" />
    </div>
  </div>
Code language: HTML, XML (xml)

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:

  .container {
    display: flex;
    flex-direction: column;
  }

  .drop-box {
    min-height: 300px;
    min-width: 300px;
    display: table;
    background-color: #c6e4f1;
    border: solid 1px #75c5e7;
  }

  .row {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
  }

  .message {
    display: table-cell;
    text-align: center;
    vertical-align: middle;
    color: #686868;
  }

  img {
    width: 200px;
    height: 200px;
  }
Code language: CSS (css)

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.

// app.component.ts
import { Component } from '@angular/core';
import { ImageFile } from './model/image-file';

@Component({
  selector: 'corp-root',
  template: `
    // Template go here
  `,
  styles: [
    `
      // Styles go here
    `,
  ],
})
export class AppComponent {
  files: ImageFile[] = [];

  onDropFiles(files: ImageFile[]): void {
    this.files = [...this.files, ...files];
  }
}
Code language: JavaScript (javascript)

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.

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:

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

You can see them in the screenshot below too.

Unsafe Blob Error - Screenshot

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

// image-uploader.directive.ts

// Other imports...
import { DomSanitizer } from '@angular/platform-browser';

@Directive({
  selector: '[corpImgUpload]',
})
export class ImageUploaderDirective {
  // attributes...

  constructor(private sanitizer: DomSanitizer) {}

  // Other event handlers...

  @HostListener('drop', ['$event']) public drop(event: DragEvent) {
    // No changes here...

    for (let i = 0; i < fileList.length; i++) {
      const file = fileList[i];
      const url = this.sanitizer.bypassSecurityTrustUrl(
        window.URL.createObjectURL(file)
      );
      files.push({ file, url });
    }
    // ...
  }
}
Code language: JavaScript (javascript)

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.

// image-file.ts
import { SafeUrl } from '@angular/platform-browser';

export interface ImageFile {
  file: File;
  url: SafeUrl;
}
Code language: JavaScript (javascript)

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

The App rendering the uploaded images

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.

Back to top

Featured Post