1

How to upload images using angular 5 and node js?

James Z
  • 12,209
  • 10
  • 24
  • 44
jawahar j
  • 183
  • 2
  • 5
  • 13

2 Answers2

0

Depending on what you want to do, you have extra information on the internet.

Your question is based on uploading images, and the images are files, so you should investigate how to upload files with node js and Angular 5 searching the internet.

For example, in the case of Node JS, look at this code

var http = require('http');
var formidable = require('formidable');

http.createServer(function (req, res) {
  if (req.url == '/fileupload') {
    var form = new formidable.IncomingForm();
    form.parse(req, function (err, fields, files) {
      res.write('File uploaded');
      res.end();
    });
  } else {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write('<form action="fileupload" method="post" enctype="multipart/form-data">');
    res.write('<input type="file" name="filetoupload"><br>');
    res.write('<input type="submit">');
    res.write('</form>');
    return res.end();
  }
}).listen(8080);

from https://www.w3schools.com/nodejs/nodejs_uploadfiles.asp

And to Angular 5

<div class="form-group">
    <label for="file">Choose File</label>
    <input type="file"
           id="file"
           (change)="handleFileInput($event.target.files)">
</div>

from Angular-5 File Upload

Héctor M.
  • 2,302
  • 4
  • 17
  • 35
0

HOW TO UPLOAD AN IMAGE FILE USING ANGULAR 5/6 AND NODEJS

For a long time I had this same question but never found a complete answer that could help me. So, after doing lots of researches I finally ended up with something solid and decided to share. This is a simplification of something I built but yet very detailed example working with an Item model, component, service, route and controller to select, upload and store a picture using latest versions of Angular and NodeJS (currently Angular 6 and NodeJS 8.11) but should work in previous versions too.

You will notice I use Reactive Form, among other things. Please do not hesitate to ask if you don't understand something on your own. I'd he happy to explain. Here we go...

item.component.ts:

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, FormArray, Validators } from '@angular/forms';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Subscription } from 'rxjs';
import { mimeType } from './mime-type.validator';

import { ItemService} from '../item.service';

import { Item } from '../item.model';

@Component({
  selector: 'app-item',
  templateUrl: './item.component.html',
  styleUrls: ['./item.component.css']
})
export class ItemComponent implements OnInit {
  item: Item;

  form: FormGroup;

  imagePreview: string;
  private id: string;

  loading = false;

  constructor(public itemService: ItemService, public route: ActivatedRoute) { }

  initForm() {
    this.imagePreview = item.imagePath;

    const item = this.item ? this.item : new Item();

    return new FormGroup({
      id: new FormControl({value: item.id, disabled: true}),
      name: new FormControl(item.name, { validators: [Validators.required, Validators.minLength(3)] }),
      image: new FormControl(item.imagePath, { validators: [Validators.required], asyncValidators: [mimeType] })
    });
  }

  ngOnInit() {
    this.form = this.initForm();

    this.route.paramMap.subscribe((paramMap: ParamMap) => {
      if (paramMap.has('id')) {
        this.id = paramMap.get('id');
        this.loading = true;

        this.itemService.getItem(this.id).subscribe(data => {
          this.item = new Item(
             data._id,
             data.name ? data.name : '',
             data.imagePath ? data.imagePath : '',
          );

          this.form = this.initForm();

          this.loading = false;
        });
      } else {
        this.id = null;
        this.item = this.form.value;
      }
    });
  }

  onImagePicked(event: Event) {
    const file = (event.target as HTMLInputElement).files[0];
    this.form.patchValue({ image: file });
    this.form.get('image').updateValueAndValidity();
    const reader = new FileReader();
    reader.onload = () => {
      this.imagePreview = reader.result;
    };
    reader.readAsDataURL(file);
  }

  onSave() {
    if (this.form.invalid) {
      return;
    }
    this.loading = true;
    if (!this.id) { // creating item...
      const item: Item = {
        id: null,
        name: this.form.value.name,
        imagePath: null
      };
      this.itemService.createItem(item, this.form.value.image);

    } else {  // updating item...
      const item: Item = {
        id: this.id,
        name: this.form.value.name,
        imagePath: null
      };
      this.itemService.updateItem(item, this.form.value.image);
    }

    this.form.reset();
  }

}

The mimeType is a validator to limit the user selecting / loading only an image file from a group of image types...

mimetype.validator:

import { AbstractControl } from '@angular/forms';
import { Observable, Observer, of } from 'rxjs';

export const mimeType = (control: AbstractControl): Promise<{[ key: string ]: any}> | Observable<{[ key: string ]: any}> => {
  if (typeof(control.value) === 'string') {
    return of(null);
  }
  const file = control.value as File;
  const fileReader = new FileReader();
  const frObs = Observable.create((observer: Observer<{[ key: string ]: any}>) => {
    fileReader.addEventListener('loadend', () => {
      const arr = new Uint8Array(fileReader.result).subarray(0, 4);
      let header = '';
      let isValid = false;
      for (let i = 0; i < arr.length; i++) {
        header += arr[i].toString(16);
      }
      switch (header) {
        case '89504e47':
          isValid = true;
          break;
        case '89504e47':    // png
        case '47494638':    // gif
        case 'ffd8ffe0':    // JPEG IMAGE (Extensions: JFIF, JPE, JPEG, JPG)
        case 'ffd8ffe1':    // jpg: Digital camera JPG using Exchangeable Image File Format (EXIF)
        case 'ffd8ffe2':    // jpg: CANNON EOS JPEG FILE
        case 'ffd8ffe3':    // jpg: SAMSUNG D500 JPEG FILE
        case 'ffd8ffe8':    // jpg: Still Picture Interchange File Format (SPIFF)
          isValid = true;
          break;
        default:
          isValid = false;
          break;
      }
      if (isValid) {
        observer.next(null);
      } else {
        observer.next({ invalidMimeType: true });
      }
      observer.complete();
    });
    fileReader.readAsArrayBuffer(file);
  });
  return frObs;
};

item.component.html:

<form [formGroup]="form" (submit)="onSave()" *ngIf="!loading">
  <input type="text" formControlName="name" placeholder="Name" autofocus>
  <span class="error" *ngIf="form.get('name').invalid">Name is required.</span>

  <!-- IMAGE BLOCK -->
  <div class="image">
    <button class="pick-image" type="button" (click)="filePicker.click()">
      Pick Image
    </button>
    <input type="file" #filePicker (change)="onImagePicked($event)">
    
    <div class="image-preview" *ngIf="imagePreview !== '' && imagePreview && form.get('image').valid">
      <img [src]="imagePreview" [alt]="form.value.title">
    </div>
  </div>

  <div id="buttons-bar">
    <button id="submit" type="submit">SAVE</button>
  </div>
</form>

Using some CSS to hide the input type "file" HTML Element because it looks ugly but still necessary to trigger the user browser to open a dialog window to select a file for upload (a nice button is shown instead for a better user experience)...

item.component.css

.pick-image {
  padding: 10px;
  background-color: rgba(150, 220, 255, 0.7);
  width: 100px;
}
.pick-image:hover {
  cursor: pointer;
  background-color: rgba(150, 220, 255, 0.9);
}

input[type="file"] {
  visibility: hidden;
  display: none;
}

.image-preview {
  height: 200px;
  margin: 0;
  padding: 0;
}
.image-preview img {
  height: 100%;
  width: 100%;
  object-fit: contain;
}

item.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';

import { Subject } from 'rxjs';
import { map } from 'rxjs/operators';

import { environment } from '../../environments/environment';
import { Item } from './item.model';

const SERVER_URL = environment.API_URL + '/items/';

@Injectable({ providedIn: 'root' })
export class ItemService {
  private items: Item[] = [];
  private itemsUpdated = new Subject<{items: Item[], count: number}>();

  constructor(private http: HttpClient, private router: Router) {}

  getItems(perPage: number, currentPage: number) {
    const queryParams = `?ps=${perPage}&pg=${currentPage}`;

    this.http.get<{message: string, items: any, total: number}>(SERVER_URL + queryParams)
      .pipe(map((itemData) => {
        const items = [];
        for (let i = 0; i < itemData.items.length; i++) {
          items.push(new Item(
            itemData.items[i]._id
            itemData.items[i].name,
            itemData.items[i].imagePath
          ));
        }
        return {
          items: items,
          count: itemData.total
        };
      }))
      .subscribe((mappedData) => {
        if (mappedData !== undefined) {
          this.items = mappedData.items;
          this.itemsUpdated.next({
            items: [...this.items],
            count: mappedData.count
          });
        }
      }, error => {
        this.itemsUpdated.next({items: [], count: 0});
      });
  }

  getItemUpdatedListener() {
    return this.ItemsUpdated.asObservable();
  }

  getItem(id: string) {
     return this.http.get<{
        _id: string,
        name: string,
        imagePath: string
     }>(SERVER_URL + id);
  }

  createItem(itemToCreate: Item, image: File) {
    const itemData = new FormData();
    itemData.append('name', itemToCreate.name);
    itemData.append('image', image, itemToCreate.name);

    this.http.post<{ message: string, item: Item}>(SERVER_URL, itemData ).subscribe((response) => {
      this.router.navigate(['/']);
    });
  }

  updateItem(itemToUpdate: Item, image: File | string) {
    let itemData: Item | FormData;
    if (typeof(image) === 'object') {
      itemData = new FormData();
      itemData.append('id', itemToUpdate.id);
      itemData.append('name', itemToUpdate.name);
      itemData.append('image', image, itemToUpdate.name);
    } else {
      itemData = {
        id: itemToUpdate.id,
        name: itemToUpdate.name,
        imagePath: image
      };
    }

    this.http.put(SERVER_URL + itemToUpdate.id, itemData).subscribe(
      (response) => {
        this.router.navigate(['/']);
      }
    );
  }

  deleteItem(itemId) {
    return this.http.delete<{ message: string }>(SERVER_URL + itemId);
  }

}

Now, in the backend (NodeJS using ExpressJS), picture you have your app.js where, among other things, you reference your connection with the database (here using MongoDB) and middlewares in the following order...

app.js:

const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');

const itemRoutes = require('./routes/items');

const app = express();

mongoose.connect(
    'mongodb+srv://username:' +
    process.env.MONGO_ATLAS_PW +
    '@cluster0-tmykc.mongodb.net/database-name', { useNewUrlParser: true })
  .then(() => {
    console.log('Mongoose is connected.');
  })
  .catch(() => {
    console.log('Connection failed!');
  });

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.use('/images', express.static(path.join(__dirname, 'images')));
app.use('/', express.static(path.join(__dirname, 'angular')));

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS');
  next();
});

app.use('/api/items', itemRoutes);

app.use((req, res, next) => {
  res.sendFile(path.join(__dirname, 'angular', 'index.html'));
});

module.exports = app;

Your itemRoutes items.js file is in a routes folder...

routes/items.js:

const express = require('express');
const extractFile = require('./../middleware/file');
const router = express.Router();

const ItemController = require('./../controllers/items');

router.get('', ItemController.get_all);

router.get('/:id', ItemController.get_one);

router.post('', extractFile, ItemController.create);

router.put('/:id', extractFile, ItemController.update);

router.delete('/:id', ItemController.delete);

module.exports = router;

Your ItemController items.js file in a controllers folder...

controllers/items.js

const Item = require('./../models/item');

// ...

exports.create = (req, res, next) => {
  const url = req.protocol + '://' + req.get('host');

  const item = req.body; // item = item to create

  const itemToCreate = new Item({
    name:         item.name,
    imagePath:    url + "/images/" + req.file.filename,
  });

  itemToCreate.save().then((newItem) => {
    res.status(201).json({
      message: 'Item created.',
      item: {
        ...newItem,
        id: newItem._id
      }
    });
  })
  .catch(error => {
    console.log('Error while creating item: ', error);
    res.status(500).json({
      message: 'Creating item failed!',
      error: error
    });
  });
};

// ...

And finally, the file.js middleware:

const multer = require('multer');

const MIME_TYPE_MAP = {
  'image/png': 'png',
  'image/jpeg': 'jpg',
  'image/jpg': 'jpg'
};

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const isValid = MIME_TYPE_MAP[file.mimetype];
    let error = isValid ? null : new Error('Invalid mime type');
    cb(error, 'images');
  },
  filename: (req, file, cb) => {
    const name = file.originalname.toLowerCase().split(' ').join('-');

    const ext = MIME_TYPE_MAP[file.mimetype];
    cb(null, name + '-' + Date.now() + '.' + ext);
  }
});

module.exports = multer({storage: storage}).single('image');

I hope this helps.

Fabricio
  • 828
  • 8
  • 13