March 23 2017

MEAN Stack Image Upload from CKEditor

CKEditor is one of the most popular WYSIWYG editors around, it's the editor I use for the MEANie CMS which is a MEAN Stack CMS I built to power this blog.

I just finished adding the ability to upload images from CKEditor in the MEANie CMS, below is a quick run through of how I did it.


Enable Image Upload from CKEditor Directive

The MEAN Stack uses AngularJS for the front end, so the way I integrated CKEditor into the CMS is with a custom directive, this post is focusing more on the file upload functionality rather than the directive itself, if you want to know more about how the directive works you can go to AngularJS CKEditor Directive.

To enable image uploading from CKEditor you just have to set the 'filebrowserImageUploadUrl' property to your upload path in the editor options. Here's a cut down version of the CKEditor directive that shows the upload url property being set to '/admin/upload'.

(function () {
    'use strict';

    angular
        .module('app')
        .directive('wysiwyg', Directive);

    function Directive($rootScope) {
        return {
            require: 'ngModel',
            link: function (scope, element, attr, ngModel) {
                var editorOptions = {
					// enable file uploads from CKEditor
					filebrowserImageUploadUrl: '/admin/upload'
				};

                // enable ckeditor
                var ckeditor = element.ckeditor(editorOptions);

                // update ngModel on change
                ckeditor.editor.on('change', function () {
                    ngModel.$setViewValue(this.getData());
                });
            }
        };
    }
})();


Save Uploaded File with Node / Express

The second piece of the puzzle is handling the file upload on the MEAN Stack server using Node / Express. There's a nice npm package called multer which handles all the low level multipart/formdata stuff, so all that's really needed is to tell it where to save your files.

Below is cut down version of the express admin controller in MEANie that shows the file upload route and multer configuration, you can see the whole file (and the rest of the MEANie project code) here on GitHub.

The getUpload function configures and returns an instance of the multer upload object, also it ensures that the uploaded file doesn't overwrite any existing file on the server by adding a number to the end of the filename if the file already exists (e.g 'file-name-1.jpg').

The upload route returns a javascript function call that's required by CKEditor for it to display the image in the editor after it's been uploaded.

var express = require('express');
var router = express.Router();
var path = require('path');
var multer = require('multer');
var slugify = require('helpers/slugify');
var fileExists = require('helpers/file-exists');

// handle file upload
router.post('/upload', getUpload().single('upload'), upload); 

module.exports = router;

/* ROUTE FUNCTIONS
---------------------------------------*/

function upload(req, res, next) {
    // respond with ckeditor callback
    res.status(200).send(
        '<script>window.parent.CKEDITOR.tools.callFunction(' + req.query.CKEditorFuncNum + ', "/_content/uploads/' + req.file.filename + '");</script>'
    );
}

/* HELPER FUNCTIONS
---------------------------------------*/

function getUpload() {
    // file upload config using multer
    var uploadDir = '../client/blog/_content/uploads';

    var storage = multer.diskStorage({
        destination: uploadDir,
        filename: function (req, file, cb) {
			// slugify file name
            var fileExtension = path.extname(file.originalname);
            var fileBase = path.basename(file.originalname, fileExtension);
            var fileSlug = slugify(fileBase) + fileExtension;

            // ensure file name is unique by adding a counter suffix if the file already exists
            var fileCounter = 0;
            while (fileExists(path.join(uploadDir, fileSlug))) {
				// increment counter until unused filename is found
                fileCounter += 1;
                fileSlug = slugify(fileBase) + '-' + fileCounter + fileExtension;
            }

            cb(null, fileSlug);
        }
    });
    var upload = multer({ storage: storage });

    return upload;
}

Sponsored by