Create a Photo Gallery with Angular

Create a Photo Gallery with Angular


In this angular tutorial, we are going to create a photo gallery with Angular and Cloudinary.

Cloudinary is a service that allows you to seamlessly manage your website’s images in the cloud – image upload, cloud storage, image manipulation, image API and fast CDN.

We will be using Angular IDE by CodeMix, so if you do not have it installed go ahead and download it from here.

Now, Open Angular IDE and create a new project ImageGallery.

The ImageGallery will have to views, a manager view to upload new images and a gallery view to view uploaded images. Let us create two components ManagerComponet and GalleyComponent, you create a new component by right-clicking on the app folder in project explorer; hover on New; then click Component.

angular ide


Let us setup a navigation section to switch between the gallery view and the manager view.

First, we include bootstrap CSS in the head section of our index.html file.


<!-- Latest compiled and minified CSS -->
<link rel="stylesheet"
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
    integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
    crossorigin="anonymous">

<!-- Optional theme -->
<link rel="stylesheet"
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
    integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
    crossorigin="anonymous">

The manager view and gallery view can be thought of as two separate web pages, and as we know, different web pages have different URLs. It is common in web development to think of each unique URL in our application as a route. Unlike React and Vue (which focus on the V in MVC), Angular includes a router.

The Angular Router enables navigation from one view to the next as users perform application tasks. Let us proceed to use the Angular Router. First, we update app.component.html, the new content is as follows

<!--The whole content below can be removed with the new code.-->
<base href="/">
<div class="navbar navbar-default">
	<div class="container">
		<div class="navbar-header">
			<a class="navbar-brand">{{title}}</a>
		</div>

		<ul class="nav navbar-nav navbar-right">
			<li routerLinkActive="active"><a routerLink="/gallery">Gallery</a></li>
			<li routerLinkActive="active"><a routerLink="/manager">Manager</a></li>
		</ul>
	</div>
</div>

<div class="container">
	<router-outlet></router-outlet>
</div>

The Angular Router docs say most routing applications should add a base element to the index.html as the first child in the tag to tell the router how to compose navigation URLs.

Now we need to update app.module.ts

import { RouterModule, Routes } from '@angular/router'; // add to import statements at top of file

/** Add below import statements **/
const appRoutes: Routes = [
  {path: 'gallery', component: GalleryComponent},
  {path: 'manager', component: ManagerComponent},
  {path: '', redirectTo: '/gallery', pathMatch: 'full'}
];

// update the imports property of Apps Module
  ...
  imports: [
    RouterModule.forRoot(appRoutes),
    BrowserModule
  ]
  ...

appRoutes is the configuration for Angular Routes in our application, what we have done is
* When our URL matches http://DOMAIN_NAME/gallery i.e. when the path is gallery angular displays the GalleryComponent
* When our URL matches http://DOMAIN_NAME/manager i.e. when the path is manager angular displays the ManagerComponent
* Redirect to http://DOMAIN_NAME/gallery when the path is http://DOMAIN_NAME

Now let’s proceed to develop the other components.

Manager

On the manager, we want to be able to do the following
* Have a drop area for images dragged from our desktop
* Upload images dropped in the area to Cloudinary
* Store the image data from cloudinary to Local storage
* Preview thumbnails of uploaded images

In order to do the steps above easily we will be installing a few npm packages, so we execute the following lines of code in our cli.

npm install @cloudinary/angular-5.x cloudinary-core ng2-file-upload lokijs --save

cloudinary-angular and cloudinary-core are Angular libraries that allow us to easily upload to and display images from Cloudinary, ng2-file-upload allows us to easily create file drop zone and file upload in angular while lokijs is a lightweight javascript in-memory database that we can persist to local storage.


Now we need to update app.module.ts

// add to import statements at top of file
import {CloudinaryModule, CloudinaryConfiguration, provideCloudinary} from '@cloudinary/angular-4.x';
import * as cloudinary from 'cloudinary-core';

import { FileUploadModule } from 'ng2-file-upload';


// update imports in the @NgModule decorator
  imports: [
    CloudinaryModule.forRoot(cloudinary, CloudinarySettings),
    FileUploadModule,
    RouterModule.forRoot(appRoutes),
    BrowserModule
  ]

Please do the following also:
* Create a cloudinary account
* Create an upload preset on cloudinary to allow unsigned uploading directly from the browser
* make a copy of settings.ts.sample and rename as settings.ts
* update the variables in settings.ts

Now we are ready to update ManagerComponent, the content of manager.component.html is updated to:

<h3>Upload Image</h3>
<div class="row">
	<div class="col-md-3">
		<h3>Select files</h3>
		<div ng2FileDrop [ngClass]="{'nv-file-over': hasBaseDropZoneOver}"
			(fileOver)="fileOverBase($event)" [uploader]="uploader"
			class="well my-drop-zone">Drop image here</div>
	</div>

	<div class="col-md-9" style="margin-bottom: 40px">
		<h3>Upload queue</h3>
		<p>Queue length: {{ uploader?.queue?.length }}</p>
		<table class="table">
			<thead>
				<tr>
					<th width="50%">Name</th>
					<th>Size</th>
					<th>Progress</th>
					<th>Status</th>
					<th>Actions</th>
				</tr>
			</thead>
			<tbody>
				<tr *ngFor="let item of uploader.queue">
					<td><strong>{{ item?.file?.name }}</strong></td>
					<td *ngIf="uploader.isHTML5" nowrap>{{
						item?.file?.size/1024/1024 | number:'.2' }} MB</td>
					<td *ngIf="uploader.isHTML5">
						<div class="progress" style="margin-bottom: 0;">
							<div class="progress-bar"
								[ngStyle]="{ 'width': item.progress + '%' }"></div>
						</div>
					</td>
					<td class="text-center"><span *ngIf="item.isSuccess"><i
							class="glyphicon glyphicon-ok"></i></span> <span *ngIf="item.isCancel"><i
							class="glyphicon glyphicon-ban-circle"></i></span> <span
						*ngIf="item.isError"><i class="glyphicon glyphicon-remove"></i></span>
					</td>
					<td nowrap>
						<button type="button" class="btn btn-success btn-xs"
							(click)="item.upload()"
							[disabled]="item.isReady || item.isUploading || item.isSuccess">
							<span class="glyphicon glyphicon-upload"></span> Upload
						</button>
						<button type="button" class="btn btn-warning btn-xs"
							(click)="item.cancel()" [disabled]="!item.isUploading">
							<span class="glyphicon glyphicon-ban-circle"></span> Cancel
						</button>
						<button type="button" class="btn btn-danger btn-xs"
							(click)="item.remove()">
							<span class="glyphicon glyphicon-trash"></span> Remove
						</button>
					</td>
				</tr>
			</tbody>
		</table>
		<div>
			<div>
				Queue progress:
				<div class="progress" style="">
					<div class="progress-bar"
						[ngStyle]="{ 'width': uploader.progress + '%' }"></div>
				</div>
			</div>
			<button type="button" class="btn btn-success btn-s"
				(click)="uploader.uploadAll()"
				[disabled]="!uploader.getNotUploadedItems().length">
				<span class="glyphicon glyphicon-upload"></span> Upload all
			</button>
			<button type="button" class="btn btn-warning btn-s"
				(click)="uploader.cancelAll()" [disabled]="!uploader.isUploading">
				<span class="glyphicon glyphicon-ban-circle"></span> Cancel all
			</button>
			<button type="button" class="btn btn-danger btn-s"
				(click)="uploader.clearQueue()" [disabled]="!uploader.queue.length">
				<span class="glyphicon glyphicon-trash"></span> Remove all
			</button>
		</div>
	</div>
</div>

<h3>Your Images</h3>
<!-- <cl-image public-id="stokvel/bridge" class="thumbnail inline" angle="20"
	format="jpg"> <cl-transformation height="150" width="150"
	crop="fill" radius="20"></cl-transformation> </cl-image>
 -->


<div>
	<p *ngIf="!imageDataArray">
		Add images by dragging and dropping upload region above
	</p>
	
	<cl-image *ngFor="let data of imageDataArray"
		[public-id]="data.public_id" class="imgThumbnail" format="jpg">
	<cl-transformation height="150" width="150" crop="fill" radius="20"></cl-transformation>
	</cl-image>
</div>

now update the content of manager.component.css

.my-drop-zone { border: dotted 3px lightgray; }
.nv-file-over { border: dotted 3px red; } /* Default class applied to drop zones on over */

.imgThumbnail{
    padding: 4px;
}

update the content of manager.component.ts

import { Component, OnInit } from '@angular/core';
import { FileUploader, FileUploaderOptions, ParsedResponseHeaders } from 'ng2-file-upload';
import { Cloudinary } from '@cloudinary/angular-4.x';

import { DB } from '../database';

@Component({
  selector: 'app-manager',
  templateUrl: './manager.component.html',
  styleUrls: ['./manager.component.css']
})
export class ManagerComponent implements OnInit {
  public uploader: FileUploader;
  public hasBaseDropZoneOver = false;
  private title: string;
  public imageDataArray;

  constructor(private cloudinary: Cloudinary) {
    this.title = '';
  }
  ngOnInit() {
    this.loadDB();
    this.loadUploadedImages();
    const uploaderOptions: FileUploaderOptions = {
      url: `https://api.cloudinary.com/v1_1/${this.cloudinary.config().cloud_name}/image/upload`,
      // Upload files automatically upon addition to upload queue
      autoUpload: true,
      // Use xhrTransport in favor of iframeTransport
      isHTML5: true,
      // Calculate progress independently for each uploaded file
      removeAfterUpload: true,
      // XHR request headers
      headers: [
        {
          name: 'X-Requested-With',
          value: 'XMLHttpRequest'
        }
      ]
    };

    const upsertResponse = fileItem => {
      // Check if HTTP request was successful
      if (fileItem.status !== 200) {
        console.log('Upload to cloudinary Failed');
        console.log(fileItem);
        return false;
      }

      let imageCollection = DB.getCollection('imagegallery');
      if (!imageCollection) {
        imageCollection = DB.addCollection('imagegallery')
      }
      imageCollection.insert(fileItem.data);
      const that = this;
      DB.saveDatabase(function(saveErr) {
        if (saveErr) {
          console.log('error : ' + saveErr);
        } else {
          that.loadUploadedImages();
        }
      });
    }

    this.uploader = new FileUploader(uploaderOptions);
    this.uploader.onBuildItemForm = (fileItem: any, form: FormData): any => {
      // Add Cloudinary's unsigned upload preset to the upload form
      form.append('upload_preset', this.cloudinary.config().upload_preset);

      // Add built-in and custom tags for displaying the uploaded photo in the list
      let tags = 'angularimagegallery';
      if (this.title) {
        form.append('context', `photo=${this.title}`);
        tags = `angularimagegallery,${this.title}`;
      }
      form.append('tags', tags);
      form.append('file', fileItem);

      // Use default "withCredentials" value for CORS requests
      fileItem.withCredentials = false;
      return { fileItem, form };
    }

    // Update model on completion of uploading a file
    this.uploader.onCompleteItem = (item: any, response: string, status: number, headers: ParsedResponseHeaders) =>
      upsertResponse(
        {
          file: item.file,
          status,
          data: JSON.parse(response)
        }
      );
  }

  loadDB(): void {
    DB.loadDatabase({}, function(err) {
      if (err) {
        console.log();
      } else {
        console.log('db loaded');
      }
    });
  }

  loadUploadedImages(): void {
    const imageCollection = DB.getCollection('imagegallery');

    if (imageCollection) {
      this.imageDataArray = imageCollection.find();
    }
  }

  public fileOverBase(e: any): void {
    this.hasBaseDropZoneOver = e;
  }

}

Gallery

We update GalleryComponent as follows:
First, update the content o fgallery.component.html

<div class="row">
	<div class="thumbs col-md-3">
		<div *ngIf="!activeImage">Please add some photos to your album in the <a routerLink="/manager">Manager section</a> </div>
		<cl-image *ngFor="let data of imageDataArray; let i=index"
			[public-id]="data.public_id" class="imgThumnail" format="jpg"
			(click)="updateActiveImage(i)"> <cl-transformation
			height="150" width="150" crop="fill" radius="20"></cl-transformation>
		</cl-image>
	</div>
	<div class="col-md-9 screen">
		<cl-image *ngIf="activeImage" [public-id]="activeImage.public_id" class="imgThumbnail"
			format="jpg"> <cl-transformation height="600" crop="fill"
			radius="20"></cl-transformation> </cl-image>
	</div>
</div>

now update gallery.component.css

.thumbs{
    height: 90vh;
    text-align: center;
}

.screen {
    text-align: center;
}

Finally, we update gallery.component.ts

import { Component, OnInit } from '@angular/core';

import { DB } from '../database';

@Component({
  selector: 'app-gallery',
  templateUrl: './gallery.component.html',
  styleUrls: ['./gallery.component.css']
})
export class GalleryComponent implements OnInit {
  public imageDataArray;
  public activeImage;
  constructor() { }

  ngOnInit() {
    this.loadDB();
    this.loadUploadedImages();
  }

  loadDB(): void {
    DB.loadDatabase({}, function(err) {
      if (err) {
        console.log();
      } else {
        console.log('db loaded');
      }
    });
  }

  loadUploadedImages(): void {
    const imageCollection = DB.getCollection('imagegallery');

    if (imageCollection) {
      this.imageDataArray = imageCollection.find();
      if (this.imageDataArray.length > 0) {
        this.activeImage = this.imageDataArray[0];
      }
    }
  }

  updateActiveImage(index): void {
    this.activeImage = this.imageDataArray[index];
  }
}

Our Photo Gallery with Angular is now Ready

Photo Gallery with Angular

Here is the link to the Code source of this project.


Share on social media

//