From the Archives: 2016: Multi-File Upload for MeteorJS
Multiple file uploads form submission with Meteor
I spent a few hours of my life searching through the minimal amounts of documentation to figure out file uploading with meteor. Specifically I was interested in uploading two files in the same form submission. Here’s a few things I’ve learned that I felt obligated to share with the world.
The package I used seems to be the most actively maintained, called CollectionFS. You can view the project here.
https://github.com/CollectionFS/Meteor-CollectionFS
Here is my function to handle the form submission.
//Form Submission Handler for uploading/creating a new contest Template.addcontest.events({ "submit form" : function(event, template){ if (!userCanCreateContest()) { //Call helper function to make sure user can create //Send message notifying user if they can't create contest FlashMessages.sendWarning("Contest Could not be added, you must vote in 5 contests before creating your own!", {autoHide: false}); return false; } event.preventDefault(); var firstImage = true; var contestId = Random.id(25); //generate a random id for the contest var curUser = Meteor.userId(); //get current user ID var currentUserId = Meteor.user().username; //get current user name //pull text fields from form and stuff into vars var subContestName = event.target.contestName.value; var subContestDescrip = event.target.descrip.value; var file = template.findAll('input:file'); var errorOccurred = false; //Loop through each submitted file and try to upload it for (x=0; x < file.length; x++) { console.log("x = " + x); //CFS file package insert call here Images.insert(file[x].files[0], function (err, fileObj) { if (err){ console.log("error on file upload! " + err); FlashMessages.sendError("Error on upload: Image #"+ (x+1)); errorOccurred = true; //handle file upload error here } else { //file upload was a success!! if(firstImage) //check if we are in first or second loop { var event1 = event.target.event1.value; var imagesURL = fileObj._id; console.log("updating image1"); Contests.update(contestId, {$set: { entry1: imagesURL, contestId: contestId, contestName: subContestName, contestDescription: subContestDescrip, userId: curUser, isActive: true, userName: currentUserId}}, {upsert: true} ); firstImage = false; } else { console.log("in else clause so second image"); var event2 = event.target.event1.value; var imagesURL = fileObj._id; Contests.update(contestId, {$set: { entry2: imagesURL, contestId: contestId, userId: curUser, isActive: true, userName: currentUserId} }, {upsert: true}); } //end else block // end for loop } });// end insert function }//end for loop //}); if(!errorOccurred) //Error handling, send notification. { FlashMessages.sendWarning("Contest Added!"); } else { FlashMessages.sendWarning("An error has occured uploading one or both images, please double check your files or your contest may not display properly.")} }}); //close addcontest.events
Ok, now obviously this has a lot of stuff you aren’t particularly interested in, or maybe it does.
First let me draw your attention to setting up CollectionFS. At the top of your js file, make sure you initialize the object that will represent your File Store.
ImageStore = new FS.Store.FileSystem("images", { }); //Definition of images collection Images = new FS.Collection("images", { filter: { maxSize: 1048576, allow: { //Restrictions for file uploads are here contentTypes: ['image/*'], extensions: ['png', 'jpg'] } }, stores: [ImageStore] });
This declaration creates a new FS.Collection, sets a few restrictions, and specifies an image store. You can read up on the different types of image stores FS has to offer if you’d like. FOr my purposes I wanted to store the whole file somewhere on a publicly accessible directory (to be displayed dynamically later on).
Now, let’s get into the file upload handler. This gets called when the user click’s the ‘Submit’ button. The cool thing about meteor here, is that we aren’t actually doing a page refresh/POST, but instead just calling this event handler to run in browser live. Pretty sweet.
//Loop through each submitted file and try to upload it for (x=0; x < file.length; x++) {
This tells us to loop through the total amount of files that were submitted. CollectionFS by default tries to upload the image as soon as the user selects it, so once the dialogue box for file selection closes the upload starts asynchronously. I didn’t want this behavior at all, and think users would much prefer the traditional ‘Click Submit’ approach.
//CFS file package insert call here Images.insert(file[x].files[0], function (err, fileObj) { if (err){ console.log("error on file upload! " + err); FlashMessages.sendError("Error on upload: Image #"+ (x+1)); errorOccurred = true; //handle file upload error here } else { //file upload was a success!!
Ok this part is the actual insert. So we call the Images collection defined at the very top of our JS file here, and tell it to insert the ‘x’ number file that was submitted. This one took me awhile to figure out because there isn’t exactly good documentation on how the indexing of the file array works on form submission.
Then we have the error handling, just write to console while you’re testing.
If the upload succeeds, you’re done! Do any additional handling you’d like.
You may be asking yourself, “Great, that makes it so simple to upload files, but what if I want to reference and use that file again?!” Excellent question sir/madam; let’s keep going.
var imagesURL = fileObj._id;
That’s it! That will return the file ID of the image that was just uploaded. As you can see I put a conditional in to determine whether the loop was on the first or second image, because I needed to add them differently to the database. This may not be the case for you, but feel free to come up with better logic or just use mine.
The final part of this is getting the image to show up in a Blaze template. So we need three things:
We need an event handler to query and grab those file references
We need to know where the heck the files are being stored on our server
We need to reference them in an IMG tag within a Blaze template
Here is event handler to grab all the file references from our database:
Really straightforward, it’s simply a mongodb find with the specification that the ‘Contest’ is active. A boolean called isActive is in each document in the database to keep track of whether or not a user has enabled/disabled the contest.
Template.contests.helpers({ contests: function () { return Contests.find({isActive: true}) ; } });
Here is the Blaze template:
<template name="userContest"> <h3><div class="user-contest-status-value">{{contestName}}</div></h3> <div class="contest-row" > <!-- <div class="userContestPreview"> --> <div class="entry"> <img class="entry" src="/cfs/files/images/{{entry1}}" style="width:60%;"> <div style="clear:both;"></div> <span class="btn"> <button type="button" class="vote1 btn">Vote Count ({{vote1_count}})</button> </span> </div> <div class="entry"> <img class="entry" src="/cfs/files/images/{{entry2}}" style="width:60%;"> <div style="clear:both;"></div> <button type="button" class="btn vote2 button-clear">Vote Count ({{vote2_count}})</button> </div> <!-- </div> --> <div class="userContestControls"> <ul> <li><button type="button" class="userContestControlButton_Pause"> Pause </button> </li> <li><button type="button" class="userContestControlButton_Delete"> Delete </button></li> <div class="userContestInfo"> <li> <div><b>Contest Name:</b></div> <div class="user-contest-status-value">{{contestName}}</div> </li> <li> <div><b>Description:</b></div> <div class="user-contest-status-value">{{contestDescription}} </div> </li> </div> </ul> <div class="userContestStatus"> {{#if isActive}} <b>Status:</b><span class="user-contest-status-value"> Active</span> {{else}} <b>Status:</b><span class="user-contest-status-value"> Paused</span> {{/if}} </div> </div> </div> <div></div> </template>