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

Luis Aviles

In a previous post, I explained the main aspects of uploading images and rendering them in the browser through a Drag & Drop component written in Angular and TypeScript.

In this MediaJam we'll use the same Angular project as a starting point to enable the files uploading in a secure cloud storage system, provided by Cloudinary, in just a few steps.

Cloudinary

What is the Cloudinary Platform?

According to the official website:

Developers and marketers use Cloudinary to quickly and easily create, manage, and deliver their digital experiences across any browser, device, and bandwidth.

The platform provides image and video APIs to store, transform, optimize, and deliver media assets. The APIs are easy-to-use, and the platform comes with powerful widgets too!

You can create an account for free, and you'll be ready to upload your images along with this tutorial.

Uploading Assets to Cloudinary

The Cloudinary platform provides a variety of options for customizing how the files can be uploaded from your application:

  • Upload from a server-side code. It supports different programming languages already.
  • Upload using the REST API, which is customizable, and can be used from the browser.
  • Upload using the Cloudinary's client libraries(SDK). These libraries wrap Cloudinary's REST APIs and add useful helper methods.

In this example, we'll use the REST API available to perform the request directly from the browser.

Account Details

Please be logged into the Cloudinary platform, and have the following data ready to use in the source code:

  1. The Cloud Name. This can be found under the Dashboard > Account Details menu.
  2. The Upload Preset. To get that value go to the Settings > Upload > Upload presets menu and you'll see the option "Enable unsigned uploading" enabled by default, as the next screenshot shows:

This means the platform expects parameters for authenticated requests by default. However, you can enable Unsigned Uploading requests after a click on "Enable unsigned uploading" link, and then you'll have a new Upload Preset with the Mode set as Unsigned. Take note of the preset name since we'll use that value later.

Project Setup

Prerequisites

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

  • I recommed having the latest LTS version of Node.js installed
  • Either NPM or Yarn as a package manager
  • The Angular CLI tool (Command-line interface for Angular)

Also, make sure you have your Cloudinary account ready to use. You must verify your email if it's your first time using the platform.

Initialize the Project

Let's create a clone, or download the project seed before adding any changes in the source code:

1git clone https://github.com/luixaviles/angular-upload-images-demo.git
2cd angular-upload-images-demo/
3git checkout -b 01-drag-drop tags/01-drag-drop
4npm install

In the previous step, you will download a local copy of the project, and create a new branch 01-drag-drop in preparation for the following steps.

Source Code Files

Pay attention to the current project structure since it comes with a set of files and default configurations for Angular and TypeScript. Open the angular-upload-images-demo folder in your favorite code editor.

1|- angular-upload-images-demo/
2 |- src/
3 |- app/
4 |- app.module.ts
5 |- app.component.ts
6 |- directives/
7 |- image-uploader.directive.ts
8 |- model/
9 |- image-file.ts

Please take a look at these files and identify the purpose of them: Application Module, Angular Component, Angular Directive, and the Model.

Implementation

Creating the Model

Every time you upload an asset over the Cloudinary Platform through the REST API, the response comes as a JSON object with several details related to it. Find an example below:

1{
2 "asset_id": "7a6d4839fe25a041ac1a9cbe8e62097c",
3 "public_id": "smvwr6wvuanpo0mhfhvb",
4 "version": 1616802885,
5 "version_id": "b171b2b766188643ea8d20fd1ecff304",
6 "signature": "802a90cec7e5feb54473828f753f20ec2b4ca1b8",
7 "width": 250,
8 "height": 250,
9 "format": "png",
10 "resource_type": "image",
11 "created_at": "2021-03-26T23:54:45Z",
12 "tags": [],
13 "bytes": 2385,
14 "type": "upload",
15 "etag": "9db278d630f5fabd8e7ba16c2e329a3a",
16 "placeholder": false,
17 "url": "http://res.cloudinary.com/luixaviles/image/upload/v1616802885/smvwr6wvuanpo0mhfhvb.png",
18 "secure_url": "https://res.cloudinary.com/luixaviles/image/upload/v1616802885/smvwr6wvuanpo0mhfhvb.png",
19 "original_filename": "angular (1)"
20}

Let's focus on some relevant properties only to create a TypeScript interface and model this response:

1ng generate interface model/cloudinary-asset

This command will create the cloudinary-asset.ts file where we can add a couple of attributes and types as the below code snippet shows.

1// cloudinary-asset.ts
2
3export interface CloudinaryAsset {
4 asset_id: string;
5 url: string;
6 width: number;
7 height: number;
8 format: string;
9}

Creating the Angular Service

Since we have the Angular component already, we may think to send the request from there. However, as best practice, we should create an Angular Service instead to encapsulate all the business logic related to the client-server communication (HTTP request/response processing).

Let's create it using the Angular CLI command ng generate service.

1ng generate service services/image-uploader

This command will create the image-uploader.service.ts file under a new folder services. Next, let's update the auto-generated content.

1// image-uploader.service.ts
2
3import { HttpClient } from '@angular/common/http';
4import { Injectable } from '@angular/core';
5
6const uploadUrl = 'https://api.cloudinary.com/v1_1/<cloud-name>/image/upload';
7const uploadPreset = 's55foqri';
8
9@Injectable({
10 providedIn: 'root',
11})
12export class ImageUploaderService {
13 constructor(private httpClient: HttpClient) {}
14}

Let's understand what's happening there.

  • The uploadUrl value is set using the Cloud Name which is assigned to your account.
  • The uploadPreset value is set with the name of your Upload Preset set as "Unsigned".
  • The HttpClient gets injected into the Angular Service.

In general, the uploadUrl value can be configured as:

1https://api.cloudinary.com/v1_1/<cloud_name>/<resource_type>/upload

The cloud_name is the name of your Cloudinary account and the resource_type can be: image, raw, video or auto. You can find more details about these configurations here.

Since the HttpClient has been injected already, don't forget to import the HttpClientModule in the app.module.ts file:

1// app.module.ts
2
3// ... other imports
4import { HttpClientModule } from "@angular/common/http";
5
6@NgModule({
7 declarations: [
8 // ... declarations
9 ],
10 imports: [
11 BrowserModule,
12 AppRoutingModule,
13 HttpClientModule
14 ],
15 providers: [],
16 bootstrap: [AppComponent]
17})
18export class AppModule { }

Next, we'll need to define a method to be able to process incoming files and send them to the cloud. But first, let's create a method to process a single file.

1// image-uploader.service.ts
2
3// ... other imports
4const uploadUrl = 'https://api.cloudinary.com/v1_1/luixaviles/image/upload';
5const uploadPreset = 's44foqri';
6
7@Injectable({
8 providedIn: 'root',
9})
10export class ImageUploaderService {
11
12 constructor(private httpClient: HttpClient) {}
13
14 private getFormData(file: File): FormData {
15 const formData = new FormData();
16 formData.append('file', file);
17 formData.append('upload_preset', uploadPreset);
18 return formData;
19 }
20}

The getFormData() method takes a File object as an input and returns a FormData object.

  • FormData is an interface to construct an object using key/value pairs. This object can be easily sent using the XMLHttpRequest.send() or even fetch().
  • The required parameters for unauthenticated requests are file and upload_preset only. Find more information about them here.

It's time to implement the main function to process all the image files.

1// image-uploader.service.ts
2
3// ...
4
5@Injectable({
6 providedIn: 'root',
7})
8export class ImageUploaderService {
9
10 // ...constructor
11
12 uploadImages(imageFiles: ImageFile[]): Observable<CloudinaryAsset[]> {
13 const files = imageFiles.map((imageFile) => imageFile.file);
14 const files$ = from(files);
15 return files$.pipe(
16 map((file) => this.getFormData(file)),
17 mergeMap((formData) =>
18 this.httpClient.post<CloudinaryAsset>(uploadUrl, formData)
19 ),
20 toArray()
21 );
22 }
23
24 // ... private method
25}

Let's describe what's happening in this code snippet.

  • The uploadImages() method will receive a set of ImageFile objects and is expected to return a set of CloudinaryAsset(model defined above) objects as an Observable.
  • The imageFiles.map() operation makes sure to "extract" the File objects only.
  • The files$ = from(files) instruction says we'll take the files array as an input to create an Observable. Next, we'll use a couple of RxJS operators to process each file arriving in the data stream.
  • The map((file) => this.getFormData(file)) operation says we'll "map" a file to a FormData object.
  • The mergeMap() operator takes the previous FormData object to send the HTTP request: POST using the URL and the formData as a payload. The result will be an Observable<CloudinaryAsset>.
  • Finally, the toArray() operator "collects" all previous results to emit all of them as a single array. This happens when the stream has been finished (All files have been uploaded).

Updating the Angular Component

Since the Business Logic has been updated, and we have an Angular Service ready to be injected, we'll need to update the Angular Component, which displays the dropbox, and also renders the uploaded images.

1// app.component.ts
2
3// ...
4export class AppComponent {
5 imageFiles$: Observable<CloudinaryAsset[]>;
6
7 constructor(private imageUploaderService: ImageUploaderService) {
8
9 }
10
11 onDropFiles(imageFiles: ImageFile[]): void {
12 this.imageFiles$ = this.imageUploaderService.uploadImages(imageFiles);
13 }
14}

The logic around this component involves:

  • imageFile$ replaces the previous Array. It's now an Observable that will "emit" a set of CloudinaryAsset objects.
  • The ImageUploaderService gets injected using the constructor. This means we'll have an instance of the service ready to be used.
  • The onDropFiles method invokes the Angular Service method and expects to get an Observable.

However, the template still expects an Array, and it's not ready to process the Observable. Let's fix that by opening the same app.component.ts file:

1<div class="container">
2 <!--Previous code not changes at all-->
3 <div class="row">
4 <a
5 *ngFor="let file of imageFiles$ | async"
6 [href]="file.url"
7 target="_blank"
8 >
9 <img [src]="file.url" />
10 </a>
11 </div>
12 </div>

The main change here is the use of the async pipe, which is required to subscribe to the imageFiles$ Observable.

The template will render the image after the upload process is complete. Also, you can click over any image to open it in a separate tab/window using the original URL provided by Cloudinary in the response.

Live Demo

Find the source code available in GitHub.

If you prefer, you can play around with the project in CodeSandbox - Uploading Images to Cloudinary with Angular too:

Conclusion

Cloudinary offers a practical way to upload our assets, using different methods and APIs.

The solution implemented here doesn't involve any server-side code, and that's the reason why we're creating an Unsigned Upload Preset. In other words, if you need to manage authenticated requests, you'll need to have at least one server endpoint to generate a signature, using your API secret (Dashboard > Account Details). Be sure to never expose your API secret in the client-side code!


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.