MEAN Stack (Angular 10) Tutorial: Upload Image File

by Didin J. on Jul 08, 2020 MEAN Stack (Angular 10) Tutorial: Upload Image File

The comprehensive step by step MEAN (MongoDB, Express.js, Angular 10, Node.js) Stack tutorial on upload image file using Multer

In this tutorial, we will show you how to upload an image file in MEAN (MongoDB, Express.js, Angular 10, Node.js) stack app using Multer. We will use the previous tutorial on the REST API image upload with the latest version and dependencies for this tutorial. 

This tutorial divided into several steps:

The following tools, frameworks, modules, and libraries are required for this tutorial:

  1. Node.js
  2. MongoDB
  3. Angular 10
  4. Angular CLI
  5. Express.js
  6. Mongoose.js
  7. Multer.js
  8. Angular Material Input File
  9. Terminal or Command Line
  10. IDE or Text Editor (we are using VSCode)

Before the move to the main steps of this tutorial, make sure that you have installed Node.js and MongoDB on your machine. You can check the Node.js version after installing it from the terminal or Node.js command line.

node -v
v12.18.0
npm -v
6.14.5

You can watch the video tutorial from our YouTube channel here. If you like it, please share, comment, and subscribe to this channel.

Let's get started with the main steps!


Step #1: Create a New Node Express.js App

Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. To create the Express.js app, we will be using the Express generator. Type this command to install it.

sudo npm install -g express-generator

Next, create an Express.js app by typing this command.

express mean-uploader --no-view

Go to the newly created mean-uploader folder then install all NPM modules.

cd ./mean-uploader
npm install

Open this Express.js project with your IDE or Text Editor. To use Visual Studio Code, type this command.

code .

Now, we have this Express.js app structure for the mean-uploader app.

.
|-- app.js
|-- bin
|   `-- www
|-- node_modules
|-- package-lock.json
|-- package.json
|-- public
|   |-- images
|   |-- index.html
|   |-- javascripts
|   `-- stylesheets
|       `-- style.css
`-- routes
    |-- index.js
    `-- users.js

To check and sanitize the Express.js app, run this app for the first time.

nodemon

or

npm start

Then you will see this page when open the browser and go to `localhost:3000`.

MEAN Stack (Angular 10) Tutorial: Upload Image File - express welcome

To make this Express.js server accessible from the different port or domain. Enable the CORS by adding this module.

npm i --save cors

Next, add this import to `app.js` after other `require`.

var cors = require('cors');

Then add this line to `app.js` before other `app.use`.

app.use(cors());


Step #2: Install the required modules and dependencies

We will use Mongoose as the ODM for MongoDB. For the upload file to the Node server, we will use Multer.js. To install those required modules, type this command.

npm install --save mongoose multer

Back to `app.js` then call the Mongoose module.

var mongoose = require('mongoose');

Create a connection to the MongoDB server using these lines of codes.

mongoose.connect('mongodb://localhost/blog-cms', {
    promiseLibrary: require('bluebird'),
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useCreateIndex: true
}).then(() =>  console.log('connection successful'))
  .catch((err) => console.error(err));

Now, if you re-run again Express.js server after running MongoDB server or daemon, you will see this information in the console.

[nodemon] 2.0.4
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./bin/www`
connection successful

That's mean, the connection to the MongoDB is successful. There's no additional configuration for Multer.js for this tutorial.


Step #3: Add Mongoose Models or Schemas

The uploaded image will not save to the MongoDB collection, but it will save to the server directory that accessible from the Angular 10 client. So, there's only an image URL that will be saved in the MongoDB collection as a string field. We need to create new Mongoose models or schemas for it. First, create a new folder in the root of the project folder that holds the Mongoose models or schemas files then add this model file.

mkdir models
touch models/Gallery.js

Open and edit `models/Gallery.js` then add these lines of Mongoose schema.

var mongoose = require('mongoose');

var GallerySchema = new mongoose.Schema({
  id: String,
  imageUrl: String,
  imageTitle: String,
  imageDesc: String,
  uploaded: { type: Date, default: Date.now },
});

module.exports = mongoose.model('Gallery', GallerySchema);


Step #4: Add Express.js REST API Route for Image Upload

We will implement this image upload in a Form. So, we will create a route for REST API that submits or POST a form request. For that, create a new Javascript file for the router inside the routes folder.

touch routes/gallery.js

Open and edit that file then declare all required modules.

var express = require('express');
var router = express.Router();
var multer  = require('multer');
var Gallery = require('../models/Gallery.js');

Declare a function that saves the uploaded file to the server storage. We have put the file to the public/images folder.

var storage = multer.diskStorage({
    destination: (req, file, cb) => {
      cb(null, './public/images');
    },
    filename: (req, file, cb) => {
      console.log(file);
      var filetype = '';
      if(file.mimetype === 'image/gif') {
        filetype = 'gif';
      }
      if(file.mimetype === 'image/png') {
        filetype = 'png';
      }
      if(file.mimetype === 'image/jpeg') {
        filetype = 'jpg';
      }
      cb(null, 'image-' + Date.now() + '.' + filetype);
    }
});

var upload = multer({storage: storage});

Add a route to POST data including file. The file will save to the server directly and the request body saves to the database if the file exists in the request.

router.post('/', upload.single('file'), function(req, res, next) {
    if(!req.file) {
        return res.status(500).send({ message: 'Upload fail'});
    } else {
        req.body.imageUrl = 'http://192.168.0.7:3000/images/' + req.file.filename;
        Gallery.create(req.body, function (err, gallery) {
            if (err) {
                console.log(err);
                return next(err);
            }
            res.json(gallery);
        });
    }
});

Export this router as a module.

module.exports = router;

Next, back to `app.js` then add this route that previously created.

var galleryRouter = require('./routes/gallery');

Add URL mapping for this route after another URL mappings.

app.use('/gallery', galleryRouter);


Step #5: Create a New Angular 10 App

We will use Angular 10 CLI to install the Angular 10 app. Just type this command to install or update the Angular 10 CLI.

sudo npm install -g @angular/cli

Still in this project folder, create a new Angular 10 app by running this command.

ng new client

If you get the question like below, choose `Yes` and `SCSS` (or whatever you like to choose).

? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS   [ https://sass-lang.com/
documentation/syntax#scss  
              ]

Next, go to the newly created Angular 10 project folder.

cd client

Type this command to run the Angular 10 app for the first time.

ng serve --open

Using the "--open" parameter will automatically open this Angular 10 app in the default browser. Now, the Angular initial app looks like this.

MEAN Stack (Angular 10) Tutorial: Upload Image File - Angular welcome


Step #6: Add Angular 10 Routing and Navigation

We will use only 2 pages in our Angular 10 frontend. An image upload form and a details page that shows up after successful image upload. Type these commands to generate it.

ng g component gallery
ng g component gallery-details

We don't need to add or register those components to the app.module.ts because it already added automatically. Next, open and edit `src/app/app-routing.module.ts` then add these imports.

import { GalleryDetailsComponent } from './gallery-details/gallery-details.component';
import { GalleryComponent } from './gallery/gallery.component';

Add these arrays to the existing routes constant that contain route for above-added components.

const routes: Routes = [
  {
    path: 'gallery',
    component: GalleryComponent,
    data: { title: 'List of Sales' }
  },
  {
    path: 'gallery-details/:id',
    component: GalleryDetailsComponent,
    data: { title: 'Sales Details' }
  },
  { path: '',
    redirectTo: '/gallery',
    pathMatch: 'full'
  }
];

Open and edit `src/app/app.component.html` and you will see the existing router outlet. Next, modify this HTML page to fit the CRUD page.

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

Open and edit `src/app/app.component.scss` then replace all SASS codes with this.

.container {
  padding: 20px;
}


Step #7: Add Angular 10 Service

The uploading to the Node-Express server handle in the Angular 10 service. The response from the Node-Express emitted by Observable that can subscribe and read from the Components. Before creating a service for REST API access, first, we have to install or register `HttpClientModule`. Open and edit `src/app/app.module.ts` then add these imports of FormsModule, ReactiveFormsModule (@angular/forms) and HttpClientModule (@angular/common/http).

import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

Add it to `@NgModule` imports after `BrowserModule`.

  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    HttpClientModule,
    AppRoutingModule
  ],

We will use the type specifier to get a typed result object. For that, create a new Typescript file `src/app/gallery.ts` then add these lines of Typescript codes.

export class Gallery {
  _id: string;
  imageUrl: string;
  imageTitle: string;
  imageDesc: string;
  uploaded: Date;
}

Next, generate an Angular 10 service by typing this command.

ng g service api

Next, open and edit `src/app/api.service.ts` then add these imports.

import { Observable, throwError } from 'rxjs';
import { HttpClient, HttpHeaders, HttpErrorResponse, HttpParams, HttpRequest } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { Gallery } from './gallery';

Add this constant before the `@Injectable`.

const apiUrl = 'http://localhost:3000/gallery';

Inject the `HttpClient` module to the constructor.

  constructor(private http: HttpClient) { }

Add the error handler function that returns as an Observable.

  private handleError(error: HttpErrorResponse): any {
    if (error.error instanceof ErrorEvent) {
      console.error('An error occurred:', error.error.message);
    } else {
      console.error(
        `Backend returned code ${error.status}, ` +
        `body was: ${error.error}`);
    }
    return throwError(
      'Something bad happened; please try again later.');
  }

Add the functions for all POST and GET gallery data with image upload. 

  getGalleryById(id: string): Observable<any> {
    const url = `${apiUrl}/${id}`;
    return this.http.get<Gallery>(url).pipe(
      catchError(this.handleError)
    );
  }

  addGallery(gallery: Gallery, file: File): Observable<any> {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('imageTitle', gallery.imageTitle);
    formData.append('imageDesc', gallery.imageDesc);
    const header = new HttpHeaders();
    const params = new HttpParams();

    const options = {
      params,
      reportProgress: true,
      headers: header
    };
    const req = new HttpRequest('POST', apiUrl, formData, options);
    return this.http.request(req);
  }

You can find more examples of Angular Observable and RXJS here.


Step #8: Create Angular Material Upload Image Form

We will use Angular Material for the upload image form and gallery details page. For that, type this command to add Angular Material using Angular Schematics.

ng add @angular/material

If there are questions like below, just use the default and "Yes" answer.

? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink        [ Preview: http
s://material.angular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? Yes
? Set up browser animations for Angular Material? Yes

For the input file component, we will use ngx-material-file-input as an additional Angular Material Input component. For that, type this command to add it.

npm i ngx-material-file-input

We will register all required Angular Material components or modules and ngx-material-file-input to `src/app/app.module.ts`. Open and edit that file then add these imports of required Angular Material Components.

import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MaterialFileInputModule } from 'ngx-material-file-input';

Register the above modules to `@NgModule` imports.

  imports: [
    ...
    MatInputModule,
    MatProgressSpinnerModule,
    MatIconModule,
    MatButtonModule,
    MatCardModule,
    MatFormFieldModule,
    MaterialFileInputModule
  ],

Next, we will implement the Angular upload file/image to the Angular component. Open and edit `src/app/gallery/gallery.component.ts` then add these imports.

import { ApiService } from '../api.service';
import { Router } from '@angular/router';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';

Add a class that implements the ErrorStateMatcher module before the main class or @Component.

/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}

Declare all required variables to build upload image form and declare the ErrorStateMatcher class.

  galleryForm: FormGroup;
  imageFile: File = null;
  imageTitle = '';
  imageDesc = '';
  isLoadingResults = false;
  matcher = new MyErrorStateMatcher();

Inject those imports to the constructor.

  constructor(
    private api: ApiService,
    private formBuilder: FormBuilder,
    private router: Router) { }

Initialize the Angular FormGroup inside ngOnInit() function.

  ngOnInit(): void {
    this.galleryForm = this.formBuilder.group({
      imageFile : [null, Validators.required],
      imageTitle : [null, Validators.required],
      imageDesc : [null, Validators.required]
    });
  }

Add a function to submit the form and call the POST function of the API service.

  onFormSubmit(): void {
    this.isLoadingResults = true;
    this.api.addGallery(this.galleryForm.value, this.galleryForm.get('imageFile').value._files[0])
      .subscribe((res: any) => {
        this.isLoadingResults = false;
        if (res.body) {
          this.router.navigate(['/gallery-details', res.body._id]);
        }
      }, (err: any) => {
        console.log(err);
        this.isLoadingResults = false;
      });
  }

Next, open and edit `src/app/gallery/gallery.component.html` then replace all HTML tags with these HTML tags of image upload form implementation.

<div class="example-container mat-elevation-z8">
  <h2>Add Gallery</h2>
  <div class="example-loading-shade"
        *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="galleryForm" (ngSubmit)="onFormSubmit()">
      <mat-form-field class="example-full-width">
        <mat-label>Gallery Image</mat-label>
        <ngx-mat-file-input formControlName="imageFile" placeholder="Select Image" valuePlaceholder="No image file selected"
          [errorStateMatcher]="matcher"></ngx-mat-file-input>
        <mat-icon matSuffix>folder</mat-icon>
        <mat-error>
          <span *ngIf="!galleryForm.get('imageFile').valid">Please select image file</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <mat-label>Image Name</mat-label>
        <input matInput placeholder="Image Name" formControlName="imageTitle"
          [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!galleryForm.get('imageTitle').valid && galleryForm.get('imageTitle').touched">Please enter Image Name</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <mat-label>Image Description</mat-label>
        <input matInput placeholder="Image Description" formControlName="imageDesc"
          [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!galleryForm.get('imageDesc').valid && galleryForm.get('imageDesc').touched">Please enter Image Description</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" mat-flat-button color="primary"><mat-icon>upgrade</mat-icon></button>
      </div>
    </form>
  </mat-card>
</div>

Next, open and edit `src/app/gallery/gallery.componen.scss` then add these lines of SCSS codes.

/* Structure */
.example-container {
  position: relative;
  padding: 5px;
}

.example-form {
  min-width: 150px;
  max-width: 500px;
  width: 100%;
}

.example-full-width {
  width: 100%;
}

.example-full-width:nth-last-child(0) {
  margin-bottom: 10px;
}

.button-row {
  margin: 10px 0;
}

.mat-flat-button {
  margin: 5px;
}

Additionally, we will add a redirect page after a successful image upload that shows the gallery details of the uploaded image. Open and edit `src/app/gallery-details/gallery-details.component.ts` then add these imports.

import { ActivatedRoute } from '@angular/router';
import { ApiService } from '../api.service';
import { Gallery } from './../gallery';

Declare all required variables to show gallery details.

  gallery: Gallery = { _id: '', imageUrl: '', imageTitle: '', imageDesc: '', uploaded: null };
  isLoadingResults = true;

Inject the previously imported modules to the constructor.

  constructor(
    private route: ActivatedRoute,
    private api: ApiService
  ) { }

Add a function to load gallery details from the API service.

  getGalleryDetails(id: string): void {
    this.api.getGalleryById(id)
      .subscribe((data: any) => {
        this.gallery = data;
        console.log(this.gallery);
        this.isLoadingResults = false;
      });
  }

Call that function from the ngOnInit() function.

  ngOnInit(): void {
    this.getGalleryDetails(this.route.snapshot.paramMap.get('id'));
  }

Next, open and edit `src/app/gallery-details/gallery-details.component.html` then replace all HTML tags with this.

<a [routerLink]="['/gallery']"><h3>Upload again!</h3></a>
<mat-card class="example-card">
  <mat-card-header>
    <mat-card-title>{{gallery.imageTitle}}</mat-card-title>
  </mat-card-header>
  <img mat-card-image src="{{gallery.imageUrl}}" alt="Photo of a Shiba Inu">
  <mat-card-content>
    <p>{{gallery.imageDesc}}</p>
  </mat-card-content>
</mat-card>

Finally, add style to `src/app/gallery-details/gallery-details.component.scss` to adjust the view of this component.

.example-card {
  max-width: 600px;
}


Step #9: Run and Test a Complete MEAN Stack (Angular 10) App

To run the complete MEAN stack (with Angular 10) application, first, run the MongoDB daemon or server in another Terminal tab.

mongod

In the current terminal tab, run the Node-Express.js server.

nodemon

Open the new Terminal tab then run the Angular 10 application.

ng serve --open

And here it is the complete MEAN Stack (Angular 10) image upload application looks like.

MEAN Stack (Angular 10) Tutorial: Upload Image File - demo 1
MEAN Stack (Angular 10) Tutorial: Upload Image File - demo 2
MEAN Stack (Angular 10) Tutorial: Upload Image File - demo 3

That it's, the MEAN Stack (Angular 10) Tutorial: Upload Image File. You can get the full working source code from our GitHub.

If you don’t want to waste your time design your own front-end or your budget to spend by hiring a web designer then Angular Templates is the best place to go. So, speed up your front-end web development with premium Angular templates. Choose your template for your front-end project here.

That just the basic. If you need more deep learning about MEAN Stack, Angular, and Node.js, you can take the following cheap course:

Thanks!

Loading…