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.
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 isapp
. -
--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 thedropFiles
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[]
. TheImageFile
was defined above as the model.
- Please note the
- 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 theHostBinding
will be equivalent to do:<div [style.background]="backgroundColor"></div>
- For example, if the directive is applied to a
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 aDataTransfer
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).
- The
- 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.
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 thebypassSecureityTrusUrl
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!
Find the source code available in GitHub.
If you prefer, you can play around with the project in CodeSandbox too: