Let's create the controller and view model for the image page. The controller for the image will be a little more complex, as we'll write the logic to handle uploading and saving of the image files via the form on the homepage.
The index
function in the image controller will look almost identical to the index
function from the home controller. The only difference is that instead of generating an array of images, we will build a view model for a single image. However, the view model for this image will have a little more information than the one from the homepage, since we are building a page that renders a more detailed view of an image (versus the thumbnail collection on the homepage). The most noteworthy inclusion is that of a comments
array for the image.
Taking another look at the original index
function in our controllers/image.js
file, we can see the simple existing res.render
line of code:
res.render('image'),
We want to replace this line with a view model and an updated res.render
statement using the following code:
var viewModel = { image: { uniqueId: 1, title: 'Sample Image 1', description: 'This is a sample.', filename: 'sample1.jpg', views: 0, likes: 0, timestamp: Date.now }, comments: [ { image_id: 1, email: '[email protected]', name: 'Test Tester', gravatar: 'http://lorempixel.com/75/75/animals/1', comment: 'This is a test comment...', timestamp: Date.now() },{ image_id: 1, email: '[email protected]', name: 'Test Tester', gravatar: 'http://lorempixel.com/75/75/animals/2', comment: 'Another followup comment!', timestamp: Date.now() } ] }; res.render('image', viewModel);
Here we are declaring a new viewModel
variable again, this time with an image
property that contains the properties for the single image. In addition to the image
property, there is also a comments
property, which is an array of comment
objects. You can see that each comment has various properties specific to a comment for each image. This JavaScript object is actually a pretty good preview of what our real data will wind up looking like once we include logic to connect our app to MongoDB!
After we build our sample image object and its collection of comments, we pass that along to our res.render
call, thus sending this new viewModel
directly to our image's Handlebars template. Again, if you review the HTML code in the image.handlebars
file, you can see where each property of the viewModel
is being displayed.
Again, let's run the application and make sure our image page is appearing properly:
$ node server.js
Once the app is running and you've launched it in your browser, click on any of the images that are listed in the Newest Images section of the homepage. This should take you to an individual image page where you will see something like the page shown in the following screenshot:
Notice that the title, description, the likes and views count, and timestamp are all now appearing on the page. In addition, you can see a few comments listed below the image as well!
The next feature we need to implement in our image
controller is the logic to handle when a user submits an image via the Image Upload form on the homepage. Even though the form is on the homepage of our app, we decided to house the logic to handle uploading within our image
controller because logically, this makes the most sense (since this feature has primarily to do with images and not the homepage per se). This was purely a personal decision, and you can house the logic wherever you please.
You should note that the HTML for the form on the homepage has its action
set to /images
and its method
is post
. This matches perfectly with the route we set up previously, where we are listening for a post to the /images
route and calling the image controller's create
function.
The create
function in our image controller will have a few key responsibilities:
As we are going to be working with the filesystem in this function, we are going to need to include a few modules from the Node.js core set of modules, specifically the File System (fs
) and the Path (path
) modules.
Let's begin by first editing the controllers/image.js
file and inserting the two new require
statements at the very top of the file:
var fs = require('fs'), path = require('path'),
Next, take the create
function's original code:
res.send('The image:create POST controller'), res.redirect('/images/1'),
Replace this original code with the following code:
var saveImage = function() { // to do... }; saveImage();
Here, we created a function called saveImage
, and we executed it immediately after we declared it. This might look a little odd, but the reason for this will become clearer when we implement database calls in the following chapter. The main reason is that we are going to call saveImage
repeatedly to ensure that the unique identifier we generated is in fact unique and doesn't already exist in the database (as a previously saved image's identifier).
Let's review a breakdown of the code that will be inserted inside the saveImage
function (replacing the // to do...
comment). I will cover each line of code for this function and then give you the entire block of code at the end:
var possible = 'abcdefghijklmnopqrstuvwxyz0123456789', imgUrl = '';
We need to generate a random six-digit alphanumeric string to represent a unique identifier for an image. This identifier will work similar to other websites that provide tiny URLs for unique links (that is, bit.ly
). To do this, we first provide a string of possible characters that can be used while generating the random string:
for(var i=0; i < 6; i+=1) { imgUrl += possible.charAt(Math.floor(Math.random() * possible.length)); }
Then, loop six times and randomly pull out a single character from our string of possible characters, appending it in each cycle. By the end of this for
loop, we should have a string that consists of six random letters and/or numbers, for example 'a8bd73'
:
var tempPath = req.files.file.path, ext = path.extname(req.files.file.name).toLowerCase(), targetPath = path.resolve('./public/upload/' + imgUrl + ext);
Here we declare three variables; where our uploaded files will be stored temporarily, the file extension of the file that was uploaded (that is '.png', '.jpg', and so on), and a destination where the uploaded image should ultimately reside. For both the latter variables, we use the Path
node module, which works great while dealing with file names and paths and getting information from a file (such as a file extension). Next we move the image from its temporary upload path to its final destination:
if (ext === '.png' || ext === '.jpg' || ext === '.jpeg' || ext === '.gif') { fs.rename(tempPath, targetPath, function(err) { if (err) throw err; res.redirect('/images/99'), }); } else { fs.unlink(tempPath, function () { if (err) throw err; res.json(500, {error: 'Only image files are allowed.'}); }); }
This code performs some validation. Specifically, it conducts checks to make sure that the uploaded file extension matches a list of allowable extensions—namely, known image file types. If a valid image file was uploaded, it is moved from the temp
folder via the filesystem's rename
function. Notice how the filesystem (fs) rename
function takes three parameters: the original file, the new file, and a callback function. The callback function is executed once the rename is complete. If node didn't work this way (always relying on callback functions), it's quite likely your code will execute immediately following the execution of the rename
function and try to work against a file that doesn't exist yet (that is, the rename
function didn't even finish doing its work). By using a callback function, we are effectively telling node that "once the rename of the file is finished and the file is ready and where it should be, then execute the following code."
The else
condition that follows handles the situation when the uploaded file was invalid (that is, not an image), so we call the unlink
function of the filesystem module, which will delete the original file (from the temp
directory it was uploaded to) and then send a simple JSON 500 with an error message.
Here is the complete saveImage
function (again, the following code will replace // to do...
from earlier):
var possible = 'abcdefghijklmnopqrstuvwxyz0123456789', imgUrl = ''; for(var i=0; i < 6; i+=1) { imgUrl += possible.charAt(Math.floor(Math.random() * possible.length)); } var tempPath = req.files.file.path, ext = path.extname(req.files.file.name).toLowerCase(), targetPath = path.resolve('./public/upload/' + imgUrl + ext); if (ext === '.png' || ext === '.jpg' || ext === '.jpeg' || ext === '.gif') { fs.rename(tempPath, targetPath, function(err) { if (err) throw err; res.redirect('/images/' + imgUrl); }); } else { fs.unlink(tempPath, function () { if (err) throw err; res.json(500, {error: 'Only image files are allowed.'}); }); }
With this code in place, we can now successfully upload an image file via the form on the homepage. Give it a try by launching the app and opening it in a browser. Once there, click on the Browse button in the main form, and select an image file from your computer. If successful, the image file should exist within the public/upload
folder of your project with a new random filename.
After the upload form completes and the create
controller function does its work, it will redirect to the individual image page for the image that was uploaded.
18.189.171.52