Image upload functionality using plain client-side JS and NodeJS
#
Intention / WhyHave you ever thought about how images are actually uploaded to a server, from the UI perspective? There could be many ways. You might have seen Image hosting cloud platforms like Cloudinary, Bynder, etc. Tools like these also provide edge-cached dynamic endpoints for various image qualities etc. But if you take a step back, the first thing all these tools need to do is allow you to upload the image which then gets processed it in a certain way. In this article, we will dive into one simple example of uploading images.
This example also covers the way you can drag-drop the image and preview it before uploading.
Lets dive in.
Here is the Youtube playlist if you want to watch me do it. Check the project code here.
#
Some common use-cases:- Profile picture update.
- Image can be dragged and dropped into the placeholder and it shows a preview.
- Some custom validations to restrict users from uploading bigger size images
- Restrict users from uploading unwanted file-types.
- Showing error message and success message.
- Using Ajax to do all the heavy-lifting and avoid page reload for uploading the image.
- Have a simple back-end using express-js to simulate a real world upload scenario.
- Clear the selected file before upload, no reload needed.
#
Technologies usedHTML
JS
jQuery (not mandatory)
Bootstrap (not mandatory)
Express.js (endpoint to save image)
Node.js (obviously for server)
Bower (frontend deps - not mandatory)
Nodemon (continous development)
#
How to installYou should have NodeJS and Git installed in your machine. You can manage without Git, but I highly recommend you use it.
#
Do a git clone#
installGo to that folder where you did the git clone
.
#
Install front-end dependenciesAs we installed bower as an npm dependency, we can use the below function to install the bower front-end dependencies.
#
Run the appThat is all required to get up and running.
#
UI looks like thisOpen http://localhost:3000.
#
Front-end UILets talk about the user interface.
#
MarkupThe index.html
file has the normal html5 boilerplate structure. The Bootstrap 4 related files such as bootstrap.min.css
, bootstrap.bundle.min.js
(which comes along with PopperJS included) and jquery.min.js
. These are required for our fancy UI to use the Bootstrap 4 magic. Since we are using jQuery for our Ajax functionality, it is good that we included this over here.
There is a form which has a hidden input field with type="file"
.
Notice the label surrounding the input:
The reason for wrapping the input inside the label and hiding it is this:
- We want to stylize the label to look like a circular section where user can drop the image.
- We also want the same circular section to be clicked upon and the native file explorer window should pop-up. The labelās for attribute takes care of this.
- We need to hide the normal file input field, because it looks ugly for our purposes and it looks different in each browser, so for the sake of consistency, we need to hide it.
The label's for
attribute points to the input which allows us to action upon the input field by clicking on the label (which is the case for all types of input fields). So, when we click on the circular section, we are actually clicking on the label and that in turn invokes the click action upon the hidden input field.
Notice that there is this attribute accept=āimage/png, image/jpegā
that makes sure that the user can only upload the images of type JPEG or PNG.
Notice, there is no attribute called
multiple
so the user can only upload one image file.
There are empty paragraphs to show error and success messages. There is an Upload button and a CTA to clear the selected image (labelled clear
).
#
Custom stylingNow, since we are using Bootstrap 4 framework, there are classes we can use stylize our app. But just for the fun of learning some CSS, lets add custom styles.
Even a superhero needs a haircut.
Iām using flex for centering the form. And the image container (label) is having the bulk of the styling.
To center the form:
The circular thingy:
The error and success messages:
To show the image preview, we use an image tag inside the circular label. But we need to make sure the image centers in that circle and the overflow should be hidden (taken care in the label related style).
Notice, we are using the traditional way of centering things using left and transform translateX stuff there.
We are also giving a special class called .dragging
that gets added to the circular section when the image is being dragged onto it. Just extra border width and low opacity to the contained text.
That is it about the UI. The only place where we are using Bootstrap is when we style the button and to use a container (with a fixed width). And of course the fonts that come with Bootstrap look awesome as always.
#
Front-end JavaScriptOoohh yes, JSā¦
Remember, there are no giant libraries to back us up with the heavy lifting. Its just plain JavaScript.
We just start with creating variables to all the sections:
All the pointers that we need:
#
The parallel thoughtsThere is a lot going on here.
We have to make sure we:
- allow user to drag and drop into the circular section
- allow user to click on the circular section and select the image
- allow user to clear the selected image
- allow user to submit the selected image
- have to handle drag and drop events to check for file type and size
- have to change style of the circular section when the image is being dragged
#
The process of draggingThis is obviously the most important and intuitive part of this app. But it is equally tricky to make sure you donāt do errors while handling the dragging scenario.
First, you have to prevent the default thing that happens when you drag a file onto a browser which is to load the file as a static file in the window.
For that you have to add a special listener that cancels out such static loading as soon as that event occurs. And this has to be listening to all the drag and drop related events.
Preventing the default actionā¦:
Then comes the hover effect that comes when the user is dragging the file onto the circular section, we have to add the .dragging
class. We do that like this:
The dragenter
and dragover
events happen when the file is being dragged over to the circular section. The dragleave
and dragend
events happen when the file is being pulled away from the section. The drop
event obviously happens when the file is let go on the section.
UI changes while dragging:
The hover effect:
And then finally when the user drops the file, we need to make sure the file goes through a size-check and file-type check.
There is another listener to drop
event (remember? we can assign multiple listeners to DOM events). The checkFileProperties
function takes care of the file-size and file-type checks. If there is an issue, it will gracefully complain using the #errorMessage
paragraph and end the drop event handler. Also, we are restricting the dragged files to be only one.
Max file size is 500KB:
We are restricting the file size to be less than or equal to 500KB and of type JPEG or PNG. The file has type and size properties that give us this info.
#
Selecting the file manuallyBut what if the user manually clicks on the circular section and manually selects the image instead of dragging. For that we will get a change event being triggered on the hidden input field. We need to handle that as well like this:
The event.target.files
property gives the list of files the user has selected.
The same functionality for both dragging and selecting manually:
#
Image preview inside circular sectionThe handleUploadedFile
function takes care of previewing the uploaded image. This is done using a FileReader constructor which is coming from JavaScriptās DOM api.
Previewing the image:
This FileReader
reads the file and triggers a load
event when it is done reading. We are setting the image source to be the event.target.result
.
This takes care of previewing the image.
#
Submitting the image to the back-endWhen user clicks on the button, the uploaded image gets sent to the backed in a base64 string format.
This is important to note, because there are many ways that we can send files to the back-end but for images, I think base64 formatted string is a quick and easy way.
The Ajax call also sends the name of the file which we are getting from the handleUploadedFile
method. The base64 string is sent in the property named ātheFileā.
Once the upload is successful, we show the success message.
#
ClearingIf the selected image has to be cleared for some reason, user can click on the Clear link below the button.
The function looks like this:
We remove the manually added image so that the background text shows up again.
#
Back-end JavaScriptThe history of backend can be easily marked as āBefore NodeJSā and āAfter NodeJSā. Of course there is still a lot of competition out there from the likes of Python, Java and PHP but in its relatively short life-time, NodeJS has touched every average developerās life in some way or the other. (Trying not to get emotional here š).
We use NodeJS for this example too. We use it for our packaging and server support. Packaging allows us to use many beautiful applications built on NodeJS such as bower, expressJs and the likes.
#
What do we use:- NodeJS for sure.
- ExpressJS for routing and body-parser to parse incoming Ajax requests
- Bower for front-end dependency management
- Nodemon ā to watch our files and auto-restart Node.
All the imports:
#
What is routingWhen we talk about the back-end, there is always a good chance that we are trying to build a micro-service or some sort of RESTful API that the front-end is going to use, right!
ExpressJS and its routing features allow us to build seamless API with its middleware functions. We can target various request types and RESTful paths. Also we can target route-params (though we are not covering them here).
#
Building the back-endThere is only one single file that caters all of our needs here, since this is a very simple demo as far as back-end is concerned.
In that file, we
- Build our API route to handle the upload scenario.
- Bring up the server to host the front-end as well as the back-end.
- Handle the uploaded file by saving it in the āimagesā folder.
#
API route for upladingWe import the express module and build our app
. Then we bring up the server using app.listen
function.
The app listens on port 3000 or the PORT variable on the environment.
Then we create the required routing using the app.post method of the ExpressJS API.
A lot is going on here, lets just take a step back and analize.
- The API URL is specified as the first parameter to the
.post
method. It is/upload
. - The second argument is the function that gets called back when this particular route is targeted through a
post
call. - When the function gets executed, the first two arguments are request object (
req
) and response object (res
) respectively. - The request object as usual contains the body object that contains all the data that is being sent from the front-end.
- In that body object, we can find the
theFile
property that contains our base64 encoded string of image data. - We check for that propertyās existence and get both the encoded string and the title of the image to save it inside the file that we are going to save it as.
- Then we writ the file to the āimagesā folder, using the NodeJSās built-in
fs.writeFileSync
method. - Notice that in this method we are passing the encoding also in the options object. This is to ensure that the file is being written in the proper format.
- At last, we use the response object to send a simple āUPLOADEDā string to the front-end to notify that the upload worked fine.
#
Static loadingIn order to serve the front-end files, we are putting them inside a folder called public
. To make the NodeJS app server serve those files as they are, we use the express.static
method like below.
We are also serving the bower_components in the /libs
route.
#
Limiting file-size and encoding incoming requestsWe are using body-parser
in order to do some middleware changes.
- We can call the imported
bodyParser
function with an options object and the main option here islimit
which we are setting to1mb
. - Also, we are converting all incoming request data to JSON format using
bodyParser.json
method. - We are also using the
bodyParser.urlencoded
method to read the encoded data that sometimes comes through.
#
The conclusionI hope you liked this post and gained some knowledge out of it. I have created a YouTube series demonstrating this illustrated example demo.
Please subscribe to my YouTube channel and follow me here as well.
See you soon with more tutorials and tips.
Originally posted in Medium here.