Add a Progress Bar in the Phoenix File Upload app

Alvise Susmel About

May 29, 2019

X Follow Button

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 formData to the startUpload function we are going to implement in a second
  • we also pass the $form jQuery object to startUpload – it will be useful later
  • at the end, we stop the form submission event with event.preventDefault(), so we can submit it ourself in the startUpload function.

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

Favoriting means this is a discussion worth sharing. It gets shared to your followers' Disqus feeds, and gives the creator kudos!

Find More Discussions

Share

  • Tweet this discussion

    • Share this discussion on Facebook
    • Share this discussion via email
    • Copy link to discussion
  • Best

Be the first to comment.

Load more comments

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

Alvise Susmel

Nov 23, 20219 min read

Crypto Dashboard LiveView course

A nice Dashboard UI

Alvise Susmel

Nov 23, 20215 min read

Crypto Dashboard LiveView course

Bindings, Click and Form events, Debounce, Live Flash messages

Alvise Susmel

Jun 30, 20217 min read

Twitter Widget Iframe