Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds

Using Client Methods

Save for later
  • 14 min read
  • 26 May 2015

article-image

In this article by Isaac Strack, author of the book Meteor Cookbook, we will cover the following recipe:

  • Using the HTML FileReader to upload images

(For more resources related to this topic, see here.)

Using the HTML FileReader to upload images

Adding files via a web application is a pretty standard functionality nowadays. That doesn't mean that it's easy to do, programmatically. New browsers support Web APIs to make our job easier, and a lot of quality libraries/packages exist to help us navigate the file reading/uploading forests, but, being the coding lumberjacks that we are, we like to know how to roll our own! In this recipe, you will learn how to read and upload image files to a Meteor server.

Getting ready

We will be using a default project installation, with client, server, and both folders, and with the addition of a special folder for storing images. In a terminal window, navigate to where you would like your project to reside, and execute the following commands:

$ meteor create imageupload
$ cd imageupload
$ rm imageupload.*
$ mkdir client
$ mkdir server
$ mkdir both
$ mkdir .images

Note the dot in the .images folder. This is really important because we don't want the Meteor application to automatically refresh every time we add an image to the server! By creating the images folder as .images, we are hiding it from the eye-of-Sauron-like monitoring system built into Meteor, because folders starting with a period are "invisible" to Linux or Unix.

Let's also take care of the additional Atmosphere packages we'll need. In the same terminal window, execute the following commands:

$ meteor add twbs:bootstrap
$ meteor add voodoohop:masonrify

We're now ready to get started on building our image upload application.

How to do it…

We want to display the images we upload, so we'll be using a layout package (voodoohop:masonrify) for display purposes. We will also initiate uploads via drag and drop, to cut down on UI components. Lastly, we'll be relying on an npm module to make the file upload much easier. Let's break this down into a few steps, starting with the user interface.

  1. In the [project root]/client folder, create a file called imageupload.html and add the following templates and template inclusions:
    <body>
    <h1>Images!</h1>
    {{> display}}
    {{> dropzone}}
    </body>
     
    <template name="display">
    {{#masonryContainer
       columnWidth=50
       transitionDuration="0.2s"
       id="MasonryContainer"
    }}
    {{#each imgs}}
    {{> img}}
    {{/each}}
    {{/masonryContainer}}
    </template>
     
    <template name="dropzone">
    <div id="dropzone" class="{{dropcloth}}">Drag images here...</div>
    </template>
     
    <template name="img">
    {{#masonryElement "MasonryContainer"}}
    <img src="{{src}}"
       class="display-image"
       style="width:{{calcWidth}}"/>
    {{/masonryElement}}
    </template>

  2. We want to add just a little bit of styling, including an "active" state for our drop zone, so that we know when we are safe to drop files onto the page. In your [project root]/client/ folder, create a new style.css file and enter the following CSS style directives:
    body {
    background-color: #f5f0e5;
    font-size: 2rem;
     
    }
     
    div#dropzone {
    position: fixed;
    bottom:5px;
    left:2%;
    width:96%;
    height:100px;
    margin: auto auto;
    line-height: 100px;
    text-align: center;
    border: 3px dashed #7f898d;
    color: #7f8c8d;
    background-color: rgba(210,200,200,0.5);
    }
     
    div#dropzone.active {
    border-color: #27ae60;
    color: #27ae60;
    background-color: rgba(39, 174, 96,0.3);
    }
     
    img.display-image {
    max-width: 400px;
    }

  3. We now want to create an Images collection to store references to our uploaded image files. To do this, we will be relying on EJSON. EJSON is Meteor's extended version of JSON, which allows us to quickly transfer binary files from the client to the server. In your [project root]/both/ folder, create a file called imgFile.js and add the MongoDB collection by adding the following line:
    Images = new Mongo.Collection('images');

  4. We will now create the imgFile object, and declare an EJSON type of imgFile to be used on both the client and the server. After the preceding Images declaration, enter the following code:
    imgFile = function (d) {
    d = d || {};
    this.name = d.name;
    this.type = d.type;
    this.source = d.source;
    this.size = d.size;
    };

  5. To properly initialize imgFile as an EJSON type, we need to implement the fromJSONValue(), prototype(), and toJSONValue() methods. We will then declare imgFile as an EJSON type using the EJSON.addType() method. Add the following code just below the imgFile function declaration:
    imgFile.fromJSONValue = function (d) {
    return new imgFile({
       name: d.name,
       type: d.type,
       source: EJSON.fromJSONValue(d.source),
       size: d.size
    });
    };
     
    imgFile.prototype = {
    constructor: imgFile,
     
    typeName: function () {
       return 'imgFile'
    },
    equals: function (comp) {
       return (this.name == comp.name &&
       this.size == comp.size);
    },
    clone: function () {
       return new imgFile({
         name: this.name,
         type: this.type,
         source: this.source,
         size: this.size
       });
    },
    toJSONValue: function () {
       return {
         name: this.name,
         type: this.type,
         source: EJSON.toJSONValue(this.source),
         size: this.size
       };
    }
    };
     
    EJSON.addType('imgFile', imgFile.fromJSONValue);

    The EJSON code used in this recipe is heavily inspired by Chris Mather's Evented Mind file upload tutorials. We recommend checking out his site and learning even more about file uploading at https://www.eventedmind.com.

  6. Even though it's usually cleaner to put client-specific and server-specific code in separate files, because the code is related to the imgFile code we just entered, we are going to put it all in the same file. Just below the EJSON.addType() function call in the preceding step, add the following Meteor.isClient and Meteor.isServer code:
    if (Meteor.isClient){
    _.extend(imgFile.prototype, {
       read: function (f, callback) {
     
         var fReader = new FileReader;
         var self = this;
         callback = callback || function () {};
     
         fReader.onload = function() {
           self.source = new Uint8Array(fReader.result);
           callback(null,self);
         };
     
         fReader.onerror = function() {
           callback(fReader.error);
         };
     
         fReader.readAsArrayBuffer(f);
       }
    });
    _.extend (imgFile, {
       read: function (f, callback){
         return new imgFile(f).read(f,callback);
       }
    });
    };
     
    if (Meteor.isServer){
    var fs = Npm.require('fs');
    var path = Npm.require('path');
    _.extend(imgFile.prototype, {
       save: function(dirPath, options){
         var fPath = path.join(process.env.PWD,dirPath,this.name);
         var imgBuffer = new Buffer(this.source);
         fs.writeFileSync(fPath, imgBuffer, options);
       }
    });
    };

  7. Next, we will add some Images collection insert helpers. We will provide the ability to add either references (URIs) to images, or to upload files into our .images folder on the server. To do this, we need some Meteor.methods. In the [project root]/server/ folder, create an imageupload-server.js file, and enter the following code:
    Meteor.methods({
    addURL : function(uri){
       Images.insert({src:uri});
    },
    uploadIMG : function(iFile){
       iFile.save('.images',{});
       Images.insert({src:'images/'     +iFile.name});
    }
    });

  8. We now need to establish the code to process/serve images from the .images folder. We need to circumvent Meteor's normal asset serving capabilities for anything found in the (hidden) .images folder. To do this, we will use the fs npm module, and redirect any content requests accessing the Images/ folder address to the actual .images folder found on the server. Just after the Meteor.methods block entered in the preceding step, add the following WebApp.connectHandlers.use() function code:
    var fs = Npm.require('fs');
    WebApp.connectHandlers.use(function(req, res, next) {
    var re = /^/images/(.*)$/.exec(req.url);
    if (re !== null) {
       var filePath = process.env.PWD     + '/.images/'+ re[1];
       var data = fs.readFileSync(filePath, data);
       res.writeHead(200, {
         'Content-Type': 'image'
       });
       res.write(data);
       res.end();
    } else {
       next();
    }
    });

  9. Our images display template is entirely dependent on the Images collection, so we need to add the appropriate reactive Template.helpers function on the client side. In your [project root]/client/ folder, create an imageupload-client.js file, and add the following code:
    Template.display.helpers({
    imgs: function () {
       return Images.find();
    }
    });

  10. If we add pictures we don't like and want to remove them quickly, the easiest way to do that is by double clicking on a picture. So, let's add the code for doing that just below the Template.helpers method in the same file:
    Template.display.events({
    'dblclick .display-image': function (e) {
       Images.remove({
         _id: this._id
       });
    }
    });

  11. Now for the fun stuff. We're going to add drag and drop visual feedback cues, so that whenever we drag anything over our drop zone, the drop zone will provide visual feedback to the user. Likewise, once we move away from the zone, or actually drop items, the drop zone should return to normal. We will accomplish this through a Session variable, which modifies the CSS class in the div.dropzone element, whenever it is changed. At the bottom of the imageupload-client.js file, add the following Template.helpers and Template.events code blocks:
    Template.dropzone.helpers({
    dropcloth: function () {
       return Session.get('dropcloth');
    }
    });
     
    Template.dropzone.events({
    'dragover #dropzone': function (e) {
       e.preventDefault();
       Session.set('dropcloth', 'active');
    },
    'dragleave #dropzone': function (e) {
       e.preventDefault();
       Session.set('dropcloth');
     
    }
    });

  12. Unlock access to the largest independent learning library in Tech for FREE!
    Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
    Renews at £15.99/month. Cancel anytime
  13. The last task is to evaluate what has been dropped in to our page drop zone. If what's been dropped is simply a URI, we will add it to the Images collection as is. If it's a file, we will store it, create a URI to it, and then append it to the Images collection. In the imageupload-client.js file, just before the final closing curly bracket inside the Template.dropzone.events code block, add the following event handler logic:
    'dragleave #dropzone': function (e) {
       ...
    },
    'drop #dropzone': function (e) {
       e.preventDefault();
       Session.set('dropcloth');
     
       var files = e.originalEvent.dataTransfer.files;
       var images =
    $(e.originalEvent.dataTransfer.getData('text/html')).find('img');
       var fragment = _.findWhere(e.originalEvent.dataTransfer.items, {
         type: 'text/html'
       });
       if (files.length) {
         _.each(files, function (e, i, l) {
           imgFile.read(e, function (error, imgfile) {
             Meteor.call('uploadIMG', imgfile, function (e) {
               if (e) {
                 console.log(e.message);
               }
             });
           })
         });
       } else if (images.length) {
         _.each(images, function (e, i, l) {
           Meteor.call('addURL', $(e).attr('src'));
         });
       } else if (fragment) {
         fragment.getAsString(function (e) {
           var frags = $(e);
           var img = _.find(frags, function (e) {
             return e.hasAttribute('src');
           });
           if (img) Meteor.call('addURL', img.src);
         });
     
     }
     
    }
    });

  14. Save all your changes and open a browser to http://localhost:3000. Find some pictures from any web site, and drag and drop them in to the drop zone. As you drag and drop the images, the images will appear immediately on your web page, as shown in the following screenshot:

    using-client-methods-img-0

    As you drag and drop the dinosaur images in to the drop zone, they will be uploaded as shown in the following screenshot:

    using-client-methods-img-1

    Similarly, dragging and dropping actual files will just as quickly upload and then display images, as shown in the following screenshot:

    using-client-methods-img-2

  15. As the files are dropped, they are uploaded and saved in the .images/ folder:

    using-client-methods-img-3

How it works…

There are a lot of moving parts to the code we just created, but we can refine it down to four areas.

First, we created a new imgFile object, complete with the internal functions added via the Object.prototype = {…} declaration. The functions added here ( typeName, equals, clone, toJSONValue and fromJSONValue) are primarily used to allow the imgFile object to be serialized and deserialized properly on the client and the server. Normally, this isn't needed, as we can just insert into Mongo Collections directly, but in this case it is needed because we want to use the FileReader and Node fs packages on the client and server respectively to directly load and save image files, rather than write them to a collection.

Second, the underscore _.extend() method is used on the client side to create the read() function, and on the server side to create the save() function. read takes the file(s) that were dropped, reads the file into an ArrayBuffer, and then calls the included callback, which uploads the file to the server. The save function on the server side reads the ArrayBuffer, and writes the subsequent image file to a specified location on the server (in our case, the .images folder).

Third, we created an ondropped event handler, using the 'drop #dropzone' event. This handler determines whether an actual file was dragged and dropped, or if it was simply an HTML <img> element, which contains a URI link in the src property. In the case of a file (determined by files.length), we call the imgFile.read command, and pass a callback with an immediate Meteor.call('uploadIMG'…) method. In the case of an <img> tag, we parse the URI from the src attribute, and use Meteor.call('addURL') to update the Images collection.

Fourth, we have our helper functions for updating the UI. These include Template.helpers functions, Template.events functions, and the WebApp.connectedHandlers.use() function, used to properly serve uploaded images without having to update the UI each time a file is uploaded. Remember, Meteor will update the UI automatically on any file change. This unfortunately includes static files, such as images. To work around this, we store our images in a file invisible to Meteor (using .images). To redirect the traffic to that hidden folder, we implement the .use() method to listen for any traffic meant to hit the '/images/' folder, and redirect it accordingly.

As with any complex recipe, there are other parts to the code, but this should cover the major aspects of file uploading (the four areas mentioned in the preceding section).

There's more…

The next logical step is to not simply copy the URIs from remote image files, but rather to download, save, and serve local copies of those remote images. This can also be done using the FileReader and Node fs libraries, and can be done either through the existing client code mentioned in the preceding section, or directly on the server, as a type of cron job.

For more information on FileReader, please see the MDN FileReader article, located at https://developer.mozilla.org/en-US/docs/Web/API/FileReader.

Summary

In this article, you have learned the basic steps to upload images using the HTML FileReader.

Resources for Article:


Further resources on this subject: