Ionic Image Upload and Management with Node.js - Part 2: Ionic App Last update: 2017-10-17

Ionic Image Upload and Management with Node.js - Part 2: Ionic App

This is the second post of our Ionic Image upload series! Last time we were building the Node.js backend and API routes, now it’s time to actually use them from a real Ionic app to upload images, display a list of images and delete single images!

This means we will implement a few views and make HTTP calls to our API, capture images from the camera or library and send those images with the FileTransfer plugin to our backend including an optional description for each image.

If you haven’t followed the first part, I recommend you check it out now as you otherwise don’t have a working backend.

Part 1: Ionic Image Upload and Management with Node.js - Server

Once we are done, our final app will look like in the animation below, which means we can upload the image, see a nice list of images and even open a bigger view for each image. So this article contains everything you need to build your own Ionic image sharing app!

nodejs-image-upload-app

If you feel lazy, you can also grab the code for the frontend app directly below.

Setting up a new Ionic App

Like always we start with a blank new Ionic app so everyone can follow all steps. We need a provider for our backend interaction and a few additional pages. Also, we install the camera plugin and the file transfer plugin to easily upload images with their Cordova and NPM packages. Go ahead and run:

ionic start devdacticImageUpload blank
cd devdacticImageUpload
ionic g provider images
ionic g page home
ionic g page uploadModal
ionic g page previewModal
ionic cordova plugin add cordova-plugin-camera
ionic cordova plugin add cordova-plugin-file-transfer
npm install --save @ionic-native/camera @ionic-native/file-transfer

Initially I tried the new HttpClient of Angular 4.3 but as it’s not yet (at the time writing this) a dependency of Ionic we would have to change a lot so we will stick with this solution for now. If you come from the future and want to see that way, just leave a comment below!

To use all of our stuff we need to import it to our module so change your src/app/app.module.ts to:

import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';

import { MyApp } from './app.component';

import { ImagesProvider } from '../providers/images/images';
import { HttpModule } from '@angular/http';
import { Camera } from '@ionic-native/camera';
import { FileTransfer } from '@ionic-native/file-transfer';

@NgModule({
  declarations: [
    MyApp
  ],
  imports: [
    BrowserModule,
    HttpModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp
  ],
  providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    ImagesProvider,
    Camera,
    FileTransfer
  ]
})

export class AppModule {}

That’s all for the setup process, we are now ready to make our HTTP calls to our backend with our provider and later use the Cordova plugins.

Provider for Backend Interaction

It’s always recommended to put stuff like backend interaction into a provider so we created a new one for all of our image actions.

Inside this ImagesProvider we have 3 functions to make use of the backend API routes. Getting a list of images and deleting images is just one line as it’s a simple GET or DELETE request using the Http provider of Angular.

Uploading an image file is a bit more complex, but just because we need a few more information at this point. To upload our file using the fileTransfer we need the path to the image, the backend URL and some optional options. In this case we specify the fileKey to be ’image’ as this is what the Multer plugin on the Node.js side expects (as we have told it so).

We also wanted to pass some information along which is the params object of the options where we set the desc key to the value the user entered before. Once all of this is done, it’s just calling upload() and the file will be send to our server!

Now open your src/providers/images/images.ts and change it to:

import { Injectable } from '@angular/core';
import 'rxjs/add/operator/map';
import { Http } from '@angular/http';
import { FileTransfer, FileUploadOptions, FileTransferObject } from '@ionic-native/file-transfer';

@Injectable()
export class ImagesProvider {
  apiURL = 'http://localhost:3000/';

  constructor(public http: Http, private transfer: FileTransfer) { }

  getImages() {
    return this.http.get(this.apiURL + 'images').map(res => res.json());
  }

  deleteImage(img) {
    return this.http.delete(this.apiURL + 'images/' + img._id);
  }

  uploadImage(img, desc) {

    // Destination URL
    let url = this.apiURL + 'images';

    // File for Upload
    var targetPath = img;

    var options: FileUploadOptions = {
      fileKey: 'image',
      chunkedMode: false,
      mimeType: 'multipart/form-data',
      params: { 'desc': desc }
    };

    const fileTransfer: FileTransferObject = this.transfer.create();

    // Use the FileTransfer to upload the image
    return fileTransfer.upload(targetPath, url, options);
  }

}

Our API and provider is now ready, so let’s put all of this into action!

Loading Images from the Backend

The first view of our app will display a list of images which we received from the backend side. We just need to call getImages() on our provider and we will get the data back inside the completion block, which we will then use inside the view in the next step.

Additional our first view has the open to delete an image (again just one call) and if we want to open an image for a bigger view of the actual image, we will open a Modal Controller with the PreviewModalPage which we implement later.

Finally the image upload will start with the presentActionSheet() so the user can decide if he wants to use an image from the library or capture a new one. After selecting, the takePicture() uses the Cordova plugin with the correct options to receive a URL to the file on the device.

At this point we could already upload the file, but we want to add an additional caption and perhaps if you want add options to change the image (just like you might know from Instagram) so we show the UploadModalPage which will then take care of calling the final upload function.

Also, once this modal is dismissed we reload our list of images if a special parameter reload was passed back from the modal, which means the upload finished and a new image should be shown inside the list!

Go ahead and change your src/pages/home/home.ts to:

import { ImagesProvider } from './../../providers/images/images';
import { Component } from '@angular/core';
import { IonicPage, NavController, ModalController, ActionSheetController } from 'ionic-angular';
import { Camera, CameraOptions } from '@ionic-native/camera';

@IonicPage()
@Component({
  selector: 'page-home',
  templateUrl: 'home.html',
})
export class HomePage {
  images: any = [];

  constructor(public navCtrl: NavController, private imagesProvider: ImagesProvider, private camera: Camera, private actionSheetCtrl: ActionSheetController, private modalCtrl: ModalController) {
    this.reloadImages();
  }

  reloadImages() {
    this.imagesProvider.getImages().subscribe(data => {
      this.images = data;
    });
  }

  deleteImage(img) {
    this.imagesProvider.deleteImage(img).subscribe(data => {
      this.reloadImages();
    });
  }

  openImage(img) {
    let modal = this.modalCtrl.create('PreviewModalPage', { img: img });
    modal.present();
  }

  presentActionSheet() {
    let actionSheet = this.actionSheetCtrl.create({
      title: 'Select Image Source',
      buttons: [
        {
          text: 'Load from Library',
          handler: () => {
            this.takePicture(this.camera.PictureSourceType.PHOTOLIBRARY);
          }
        },
        {
          text: 'Use Camera',
          handler: () => {
            this.takePicture(this.camera.PictureSourceType.CAMERA);
          }
        },
        {
          text: 'Cancel',
          role: 'cancel'
        }
      ]
    });
    actionSheet.present();
  }

  public takePicture(sourceType) {
    // Create options for the Camera Dialog
    var options = {
      quality: 100,
      destinationType: this.camera.DestinationType.FILE_URI,
      sourceType: sourceType,
      saveToPhotoAlbum: false,
      correctOrientation: true
    };

    // Get the data of an image
    this.camera.getPicture(options).then((imagePath) => {
      let modal = this.modalCtrl.create('UploadModalPage', { data: imagePath });
      modal.present();
      modal.onDidDismiss(data => {
        if (data && data.reload) {
          this.reloadImages();
        }
      });
    }, (err) => {
      console.log('Error: ', err);
    });
  }
}

When we get the initial list of images it will be empty, but later after uploading some images the response will look somehow like this:

[
    {
        "_id": "59a409348068343d9b187d6b",
        "desc": "my description",
        "originalName": "ionic-roadmap-icon.webp",
        "filename": "image-1503922484703",
        "created": "2017-08-28T12:14:44.709Z",
        "url": "http://localhost:3000/images/59a409348068343d9b187d6b"
    },
    {
        "_id": "59a4093c8068343d9b187d6c",
        "desc": "Amrum!",
        "originalName": "18645529_1896463043974492_8029820227527114752_n.jpg",
        "filename": "image-1503922492005",
        "created": "2017-08-28T12:14:52.009Z",
        "url": "http://localhost:3000/images/59a4093c8068343d9b187d6c"
    }
]

We now build the view around this response by creating items for each entry and directly using the image URL of the JSON object. Remember, this is the URL we created inside the backend after receiving the entries, so this URL is not stored inside the database!

You might change URLs or backends so it wouldn’t be a good idea to store a absolute URL inside the database, but now that we get it we are happy on the frontend side as there is no additional work required to show images.

We can click each item to open the full screen preview, and sliding the items allows us to use the delete button for each image.

To upload a new image we add an ion-fab button which will float above our content and open the action sheet with the options for library or camera.

Now change your src/pages/home/home.html to:

<ion-header>
  <ion-navbar color="primary">
    <ion-title>Images</ion-title>
  </ion-navbar>
</ion-header>

<ion-content>
  <h3 [hidden]="images.length !== 0" text-center>No Images found!</h3>

  <ion-list>
    <ion-item-sliding *ngFor="let img of images">

      <ion-item tappable (click)="openImage(img)">
        <ion-thumbnail item-start>
          <img [src]="img.url">
        </ion-thumbnail>
        <h2>{{ img.desc }}</h2>
        <button ion-button clear icon-only item-end> <ion-icon name="arrow-forward"></ion-icon></button>
      </ion-item>

      <ion-item-options side="right">
        <button ion-button icon-only color="danger" (click)="deleteImage(img)">
        <ion-icon name="trash"></ion-icon>
      </button>
      </ion-item-options>

    </ion-item-sliding>
  </ion-list>

  <ion-fab right bottom>
    <button ion-fab (click)="presentActionSheet()"><ion-icon name="camera"></ion-icon></button>
  </ion-fab>

</ion-content>

You can now run your app already but as you might have not uploaded anything (you could already upload files with Postman!) you will see a blank screen, so let’s work on the upload page now.

Capture & Upload Image to Node.js Server

Most of the logic is already implemented so we just need some more simple views.

The image upload view appears after the user has taken or selected an image. Now we use the data that was passed to our modal page and implement a simple dismiss()function to close the preview and the saveImage() function which will upload the actual image with the caption which the user can also enter on this page. Once the upload is finished, we dismiss the page but pass the reload parameter so our HomePage knows the image list needs to be reloaded.

Open your src/pages/upload-modal/upload-modal.ts and change it to:

import { ImagesProvider } from './../../providers/images/images';
import { Component } from '@angular/core';
import { IonicPage, NavController, NavParams, ViewController } from 'ionic-angular';

@IonicPage()
@Component({
  selector: 'page-upload-modal',
  templateUrl: 'upload-modal.html',
})
export class UploadModalPage {
  imageData: any;
  desc: string;

  constructor(public navCtrl: NavController, private navParams: NavParams, private viewCtrl: ViewController, private imagesProvider: ImagesProvider) {
    this.imageData = this.navParams.get('data');
  }

  saveImage() {
    this.imagesProvider.uploadImage(this.imageData, this.desc).then(res => {
      this.viewCtrl.dismiss({reload: true});
    }, err => {
      this.dismiss();
    });
  }

  dismiss() {
    this.viewCtrl.dismiss();
  }

}

Inside the view for this page we only need the image and the inout field for the description, so really nothing special here. Go ahead and change your src/pages/upload-modal/upload-modal.html to:

<ion-header>
  <ion-navbar color="primary">
    <ion-buttons start>
      <button ion-button icon-only (click)="dismiss()"><ion-icon name="close"></ion-icon></button>
    </ion-buttons>
    <ion-title>Upload Image</ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <img [src]="imageData" style="width: 100%">
  <ion-item>
    <ion-input placeholder="My Awesome Image" [(ngModel)]="desc"></ion-input>
  </ion-item>

  <button ion-button full icon-left (click)="saveImage()">
          <ion-icon name="checkmark"></ion-icon>
          Save & Upload Image
  </button>
</ion-content>

You are now ready to upload images and should see the new images inside your list! Let’s finish this up with the special image preview.

Opening Images Fullscreen

Of course the list image is a bit small so we want to be able to show the full image with the description and perhaps also the creation date (which is added by the server automatically).

The class of this view is just making use of the passed image and has a dismiss function, so change your src/pages/preview-modal/preview-modal.ts now to:

import { Component } from '@angular/core';
import { IonicPage, NavController, NavParams, ViewController } from 'ionic-angular';

@IonicPage()
@Component({
  selector: 'page-preview-modal',
  templateUrl: 'preview-modal.html',
})
export class PreviewModalPage {
  img: any;

  constructor(public navCtrl: NavController, public navParams: NavParams, private viewCtrl: ViewController) {
    this.img = this.navParams.get('img');
  }

  close() {
    this.viewCtrl.dismiss();
  }

}

For the view we need to get a bit tricky as we want a slightly transparent background which we will achieve with some CSS classes. Also, whenever the user clicks on the background we want to catch that event and hide the full screen image again.

Below the image we also display a card with the description and date, everything is directly retrieved from the JSON object of that image.

Now change your src/pages/preview-modal/preview-modal.html to:

<ion-content>
  <div class="image-modal transparent">
    <ion-item class="close-fake" no-lines (click)="close()">
      <ion-icon name="close"></ion-icon>
    </ion-item>
    <img [src]="img.url" class="fullscreen-image transparent" (click)="close()">
    <ion-card>
      <ion-card-content class="img-info" *ngIf="img.desc">
        <div class="desc-text">"{{ img.desc }}"</div>
        <p>
          <ion-icon name="calendar" item-left *ngIf="img.created"></ion-icon>
          {{ img.created | date: 'short' }}
        </p>
      </ion-card-content>
    </ion-card>
  </div>
</ion-content>

The last missing piece is the CSS to make the page look transparent, so open the src/pages/preview-modal/preview-modal.scss and change it to:

page-preview-modal {
    .img-info {
        padding-left: 20px;
        background: #fff;
    }
    .desc-text {
         font-weight: 500;
         font-size: larger;
         padding-bottom: 15px;
         color: color($colors, dark);
    }

  .content-ios {
    background: rgba(44, 39, 45, 0.84) !important;
  }

  .content-md {
    background: rgba(44, 39, 45, 0.84) !important;
  }

  .close-fake {
      background: transparent;
      color: color($colors, light);
      font-size: 3rem;
  }
}

Woah, we are done! Finally..

To completely use your app you need to deploy it to a device as we are using Cordova plugins to capture images and for the upload process. If you already have data inside your database you should anyway be able to run the app inside your browser to see the list of images and also open the full screen preview!

Conclusion

Inside this 2 part series you have learnt to build a Node.js backend with MongoDB and also an Ionic image sharing App which makes use of our own API and uploads media files to a server.

You are now more or less ready to build the next Instagram or simply an image sharing app between you and your friends. Whatever it is that you build, let us know in the comments (and also if you enjoyed this series style with backend coding!).

You can also find a video version of this article below.

Happy Coding, Simon

https://youtu.be/JtSTW2g1sZU