Add a Progress Bar in the Phoenix File Upload app
May 29, 2019
X Follow Button
- Part 1: Step-by-Step Tutorial to Build a Phoenix App that Supports User Upload
- Part 2: Creating Thumbnails of uploaded Images and PDF in Phoenix
- Part 3: this article
In the previous two parts we’ve seen how to create a Phoenix application with a multipart upload form, that creates thumbnails of images and PDF.
We select a file to upload and once uploaded we see it in the upload list with a thumbnail (if image or PDF). Since we are using it in localhost the upload is fast, even with large files.
But in a real world scenario the upload could take minutes (or even hours). We have to show a proper upload interface with a nice process bar.
To better follow this article, you can download the part-2 code at poeticoding/phoenix_uploads_articles:part-2.
Once we have started a Postgres server and installed Imagemagick (for thumbnails), we have an upload form that looks like this
Current Upload Form
Simulate slow connections with Chrome
With Chrome is possible to simulate a real case internet connection using throttling. You just need to open the Inspector, open the Network conditions panel
Chrome Network conditions
and choose the throttling option you want, like Slow 3G. You can even add your custom throttling profile.
Slow 3G Throttling
If you try to upload a file bigger than one megabyte, you’ll see that the interface is stuck, you wait … without knowing for how long.
At least the browser shows, on the bottom left, a small bar displaying the progress, but obviously it’s not enough for us and our app.
Chrome progress bar
To make a nice upload page we need to start playing a little bit with JavaScript and CSS.
jQuery in Phoenix
There are great libraries out there that could do everything for us, like for example DropzoneJS. But in this article I want to show you how, with JavaScript and the jQuery, we can take control of upload form events and build our progress bar.
So let’s start by adding jQuery in our Phoenix app. The easiest way is to add jquery under dependencies in assets/package.json
"dependencies": {
...
"jquery": "^3.4.1"
}
JSON
Copy
I’ve started recently to use Visual Studio Code (before I was using Sublime Text), which supports Elixir really well thanks to ElixirLS. It has also a nice feature I’ve noticed while editing the package.json configuration file: when adding jquery VS Code lists the packages available on npmjs and once selected jquery it suggests the latest stable version. it’s a simple feature but a really helpful one.
Visual Studio Code – package.json
After adding this new dependency, we run npm install in the assets/ directory, installing the missing packages which will be saved in the assets/node_modules folder.
Now, we create a new javascript file, assets/js/upload.js and we import it into assets/js/app.js. In this new upload file we will write our JavaScript code that refers to the upload form.
// assets/js/app.js
import "phoenix_html"
import "./upload"
JavaScript
Copy
In this way webpack, which starts automatically in development with mix phx.server, includes the upload.js script in the app.js file served by Phoenix.
<body>
...
<script type="text/javascript" src="/js/app.js"></script>
</body>
HTML
Copy
Upload a file using jQuery and Ajax
Let’s now see how to submit the multipart form using jQuery, so we can monitor the upload’s progress with JavaScript.
At first we give an id to the upload form to be able to easily refer to it with a jQuery selector. We find the form in lib/poetic_web/templates/upload/new.html.eex
<%= form_for @conn, Routes.upload_path(@conn, :create),
[multipart: true, id: "upload_form"], fn f-> %>
...
<% end %>
HTML
Copy
And then we focus on the upload.js file.
// assets/js/upload.js
import jQuery from "jquery"
jQuery(document).ready(function($){
let $form = $("#upload_form");
$form.submit(function(event){
let formData = new FormData(this);
startUpload(formData, $form);
event.preventDefault();
})
})
JavaScript
Copy
We start by importing jQuery and, when the document is loaded, we catch the form submit event by passing a handler to the submit function.
Since we want to upload the file ourself using jQuery
- we create a new FormData, which we’ll use later
- we pass
formDatato thestartUploadfunction we are going to implement in a second - we also pass the
$formjQuery object tostartUpload– it will be useful later - at the end, we stop the form submission event with
event.preventDefault(), so we can submit it ourself in thestartUploadfunction.
Let’s see now the startUpload function
// assets/js/upload.js
function startUpload(formData, $form) {
jQuery.ajax({
type: 'POST',
url: '/uploads',
data: formData,
processData: false, //IMPORTANT!
cache: false,
contentType: false,
xhr: function () {
let xhr = jQuery.ajaxSettings.xhr();
if (xhr.upload) {
xhr.upload.addEventListener(
'progress',handleProgressEvent, false
);
}
return xhr;
},
success: function (data) {
console.log("SUCCESS", data)
},
error: function (data) {
console.error(data);
}
})
}
JavaScript
Copy
We use the the ajax function to submit the form, making a POST request to /uploads path, sending formData to the server. To avoid any data transformation from jQuery it’s important to set processData to false.
The xhr parameter expects a callback function which creates and returns a XMLHttpRequest (XHR) object, used to make an HTTP request to the server. We pass a callback function which creates xhr, a XMLHttpRequest object, and with xhr.upload.addEventListener(...) we start listening to progress events, which are handled by handleProgressEvent function.
Almost there… last part before we are able to test this out. Let’s write a handleProgressEvent function that just prints the event.
// assets/js/upload.js
function handleProgressEvent(progressEvent) {
console.log(progressEvent);
}
JavaScript
Copy
Let’s see what it’s printed when we upload a file. Remember to enable the throttling – with a slow connection you can see many more events printed on the console.
ProgressEvent
Calculate and show the progress
The ProgressEvent object, passed to handleProgressEvent(), has everything we need to calculate the upload progress percentage.
ProgressEvent {
total: 3698228,
loaded: 49152
...
}
JavaScript
Copy
total is the total file size (in byte) and loaded is the current uploaded size.
Let’s start with something simple showing in the upload page an HTML label with the progress percentage.
First, we need to add a label in the upload form in new.html.eex file
<%= form_for @conn, Routes.upload_path(@conn, :create),
[multipart: true, id: "upload_form"], fn f-> %>
<%= file_input f, :upload, class: "form-control" %>
<%= submit "Upload", class: "btn btn-primary" %>
<div class="upload-progress">
<p>Upload progress:
<label class="progress-percentage">0%</label>
</p>
</div>
<% end %>
HTML
Copy
Instead of using the selector "#upload_form label.progress-percentage" directly inside the handleProgressEvent(e) function, we define a new function called createProgressHandler($form) which accepts the form jQuery object and returns a handler function.
// assets/js/upload.js
function createProgressHandler($form) {
let $label = $form.find("label.progress-percentage");
return function handleProgressEvent(progressEvent) {
let progress = progressEvent.loaded / progressEvent.total,
percentage = progress * 100,
percentageStr = `${percentage.toFixed(2)}%`;
$label.text(percentageStr);
}
}
JavaScript
Copy
In this way the handler function has access to $label and it’s able to update its text. The handler calculates the progress and updates the label text with the percentageStr string.
To make it work we need to also change a line in the startUpload function.
// assets/js/upload.js
function startUpload(formData, $form) {
jQuery.ajax({
...
xhr: function () {
...
xhr.upload.addEventListener(
'progress',
createProgressHandler($form),
false
);
}
...
JavaScript
Copy
Instead of passing the handler function directly to addEventListener, we call createProgressHandler($form) which returns the handler function that will be called for each progress event.
Let’s try again to upload a file.
Upload Progress
🎉👩💻👨💻🎉
Great, it works! It’s not aesthetically pleasant, but at least it shows dynamically the upload’s progress.
You’ve maybe noticed that once reached 100%, the success callback is called printing the server response to the JavaScript console. This response is the upload list page HTML (GET /uploads). In our case, since we’ve receive the response via jQuery success callback, the browser isn’t redirected to/uploads and we just see a page with the progress stuck at 100%.
In general, it would be better to have an API that sends us a JSON response with the details of the created file – we could then show this data to confirm that the upload succeeded. For simplicity, in the case of success, we just ignore the data and redirect the browser to the uploads page.
// assets/js/upload.js
function startUpload(formData, $form) {
...
jQuery.ajax({
...
success: function (data) {
window.location = "/uploads"
},
...
})
}
JavaScript
Copy
Redirect after completion
HTML5 progress bar
It’s now time to try to nicely show the progress with a progress bar. We can start using the progress HTML5 tag, without having to import any library.
<%= form_for @conn, Routes.upload_path(@conn, :create),
[multipart: true, id: "upload_form"], fn f-> %>
<%= file_input f, :upload, class: "form-control" %>
<%= submit "Upload", class: "btn btn-primary" %>
<div class="upload-progress">
<progress max="100" value="0"></progress>
<label class="progress-percentage"></label>
</div>
<% end %>
HTML
Copy
By default, the result is a thin blue bar.
HTM5 progress bar
I’m neither a front-end developer nor a CSS expert, but we can get a nicer progress bar just playing around with the bar’s CSS. We create a new /assets/css/upload.css file adding the CSS below
/* assets/css/upload.css */
progress {
position:relative;
width: 100%;
height: 25px;
appearance: none;
-webkit-appearance: none;
}
progress::-webkit-progress-bar {
background-color: #eee;
border-radius: 8px;
}
progress::-webkit-progress-value {
background-color: #276bd1;
border-radius: 8px;
}
CSS
Copy
We are able to customize size and colors using the progress element itself and some CSS progress bar pseudo-elements (this code works on Chrome and Safari, to support Firefox and other browsers we’d need to add some extra CSS).
This progress bar doesn’t have an attribute to easily show a label at the center. But we can move our current label label.progress-percentage at the center of the bar.
/* assets/css/upload.css */
label.progress-percentage {
position: absolute;
top: 0;
text-align: center;
width: 100%;
color: white;
font-weight: bold;
text-shadow: 1px 1px 1px #444;
}
CSS
Copy
Similarly to what we did for upload.js, we import it on assets/css/app.css
/* assets/css/app.css */
@import "./phoenix.css";
@import "./upload.css";
CSS
Copy
Adding the CSS progress { display: none; } hides the bar by default. We want to show the bar only when the upload starts. When the startUpload(...) JavaScript function is called, we show the progress bar.
// assets/js/upload.js
function startUpload(formData, $form) {
let $progress = $form.find("progress");
$progress.show()
...
}
JavaScript
Copy
We now need to amend the createProgressHandler and handleProgressEvent functions so they can update both the progress bar and the label.
// assets/js/upload.js
function createProgressHandler($form) {
let $progress = $form.find("progress"),
$label = $form.find("label.progress-percentage");
return function handleProgressEvent(progressEvent) {
let progress = progressEvent.loaded / progressEvent.total,
percentage = progress * 100,
percentageStr = `${percentage.toFixed(2)}%`; //xx.xx%
$label.text(percentageStr)
//PROGRESS BAR
$progress
.attr("max", progressEvent.total)
.attr("value", progressEvent.loaded);
}
}
JavaScript
Copy
As you can see, to update the progress bar we just need to set the max and value attributes, which are respectively total size of the file and current uploaded bytes.
Time to see it in action!
Fully working progress bar
Move the upload form
At the moment the upload form is in its own page /uploads/new. To have everything in the same place, we can move the form into the upload list page.
Instead of copying & pasting the code inside the upload list page, we remove the action :new from the routes and rename the template file new.html.eex to upload_form.html.eex.
We can now render the form into templates/upload/index.html.eex, using the PoeticWeb.UploadView.render function and passing the connection
<%= PoeticWeb.UploadView.render("upload_form.html", conn: @conn) %>
<table class="table">
<thead>
<th>Thumbnail</th>
<th>ID</th>
<th>Filename</th>
<th>Size</th>
<th>Type</th>
<th>Time</th>
</thead>
<tbody>
...
HTML
Copy
Upload form rendered in the upload list page
File input and Upload button
An extra small change: it would be nice to have just one Upload button to choose the file and, once the file is selected, to automatically start the upload.
We start by changing the upload_form.html.eex file, wrapping the file_input and a button inside a div with class upload-btn-wrapper. We also remove the submit button.
<%= form_for ... %>
<div class="upload-btn-wrapper">
<button class="btn btn-primary">Upload a file</button>
<%= file_input f, :upload, class: "form-control" %>
</div>
...
<% end %>
HTML
Copy
We add to upload.css some CSS specific to the wrapper, overlaying the file input with the button.
.upload-btn-wrapper {
position: relative;
overflow: hidden;
display: inline-block;
}
.upload-btn-wrapper input[type=file] {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
opacity: 0;
}
CSS
Copy
And we add in upload.js the JavaScript code to automatically start the upload once the file is selected.
// assets/js/upload.js
jQuery(document).ready(function ($) {
let $form = $("#upload_form"),
$fileInput = $form.find("input[type='file']");
$form.submit(function (event) { ... }
$fileInput.on("change", function (e) {
$form.trigger("submit");
});
});
JavaScript
Copy
Upload form with progress bar
Wrap Up
If you want to try the code of this part, you find it on the GitHub repo poeticoding/phoenix_uploads_articles:part-3_progress-bar.
In this article I preferred to use only jQuery, so we could interact ourself with the upload JavaScript events and understand the dynamics. But when building an application we can’t reinvent the wheel every time. There are a lot of great JavaScript libraries out there that can make our life easier.
If you want to bring the progress bar a step further, give a try to progressbar. j s which is a JavaScript library that displays beautiful progress bars with different shapes, colors and animations. To use it, you just need to include it in the dependencies in package.json and import it in upload.js, like we did with jQuery.
jQuery File Upload is a pretty famous JavaScript library (it has more than 30,000 stars on GitHub) which handles the upload and progress bar. It’s really well documented and it’s still maintained.
A library I mentioned at the beginning is DropzoneJS. I haven’t played a lot with it yet, but it seems a great full-optional library. It creates a drop-zone box in the page where we can drag & drop our files – DropzoneJS will take care of the rest sending the file to the server and showing a nice UI with progress bar and thumbnails. With this library we can easily set the maximum file size, choose the supported file types, generate thumbnails on the client side and many other things.
Share this:
Disqus Recommendations
We were unable to load Disqus Recommendations. If you are a moderator please see our troubleshooting guide.
❮
- 7 years ago
- 2 comments
We setup the AWS account, configure ExAws, put, list, get and delete objects. …
- 7 years ago
- 1 comment
After a quick intro to containers and images, we see how easy it is to run …
- 7 years ago
- 10 comments
We see how to fully implement concurrent HTTP calls, using just spawn, …
- 6 years ago
- 2 comments
Let's see how to use, in Phoenix LiveView, the phx-click binding along with …
- 7 years ago
- 8 comments
One of the beautiful things of Elixir is pattern matching. We'll see pattern …
- 7 years ago
- 5 comments
Focus on LiveView's primitives: the bricks we need to know to building …
- 2 years ago
- 1 comment
As someone who loves experimenting with new technologies, I recently …
- 7 years ago
- 3 comments
DigitalOcean Spaces is a cloud storage alternative to AWS S3. Since Spaces is …
❯
tempest.services.disqus.com
tempest.services.disqus.com is blocked
This page has been blocked by an extension
- Try disabling your extensions.
ERR_BLOCKED_BY_CLIENT
Reload
This page has been blocked by an extension
Disqus Comments
We were unable to load Disqus. If you are a moderator please see our troubleshooting guide.
G
Start the discussion…
Comment
Log in with
or sign up with Disqus or pick a name
Disqus is a discussion network
- Don't be a jerk or do anything illegal. Everything is easier that way.
Read full terms and conditions
This comment platform is hosted by Disqus, Inc. I authorize Disqus and its affiliates to:
- Use, sell, and share my information to enable me to use its comment services and for marketing purposes, including cross-context behavioral advertising, as described in our Terms of Service and Privacy Policy, including supplementing that information with other data about me, such as my browsing and location data.
- Contact me or enable others to contact me by email with offers for goods or services
- Process any sensitive personal information that I submit in a comment. See our Privacy Policy for more information
Acknowledge I am 18 or older
Discussion Favorited!
Favoriting means this is a discussion worth sharing. It gets shared to your followers' Disqus feeds, and gives the creator kudos!
Tweet this discussion
- Share this discussion on Facebook
- Share this discussion via email
- Copy link to discussion
Be the first to comment.
live.rezync.com
live.rezync.com is blocked
This page has been blocked by an extension
- Try disabling your extensions.
ERR_BLOCKED_BY_CLIENT
Reload
This page has been blocked by an extension
pippio.com
pippio.com is blocked
This page has been blocked by an extension
- Try disabling your extensions.
ERR_BLOCKED_BY_CLIENT
Reload
This page has been blocked by an extension
tempest.services.disqus.com
tempest.services.disqus.com is blocked
This page has been blocked by an extension
- Try disabling your extensions.
ERR_BLOCKED_BY_CLIENT
Reload
This page has been blocked by an extension
Crypto Dashboard LiveView course
Phoenix LiveView LiveComponents
Nov 23, 20219 min read
Crypto Dashboard LiveView course
A nice Dashboard UI
Nov 23, 20215 min read
Crypto Dashboard LiveView course
Bindings, Click and Form events, Debounce, Live Flash messages
Jun 30, 20217 min read
Twitter Widget Iframe