code.life

Rose Weixel’s technical blog

Live Updating DOM Elements With jQuery and Ajax

Based on a talk I gave at the NYCHTML5 Meetup on June 2nd, 2015.

Any web app that involves real-time interactions between users requires some form of live notifications. Implementing this, for a beginner developer such as myself, can be a daunting challenge. This post will walk through how I went about solving this problem when working on Lacquer Love&Lend, a social network for nail polish lovers that allows users to interact via friendships and lacquer loans. As with any social network, I wanted users to see live notifications whenever they received a new friendship or transaction request, or when the state of any of their friendships or transactions changed. The example that follows assumes some basic knowledge of Rails.

Some Basic Ajax

In a basic Ajax request, a user clicks on something, the Ajax request gets sent, and a part of the DOM gets updated without the entire page reloading.

images

The code usually looks something like this:

app/assets/javascripts/something.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1) Wait for the document to be ready.
$(document).ready(function() {
    // 2) Listen for the submission of the form.
    $("form").submit(function(event) {

        // 3) Prevent an entire page load (or reload).
        event.preventDefault();

        // 4) Grab the information from the form needed for the Ajax request.
        var formAction = $(this).attr('action'); // e.g. '/somethings'
        var formMethod = $(this).attr('method'); // e.g. 'post'
        var formData   = $(this).serializeArray(); // grabs the form data and makes your params nicely structured!

        // 5) Make the Ajax request, which will hit the 'create' action in the 'somethings' controller
        $.ajax({
          url:  formAction,
          type: formMethod,
          data: formData
        });
    });
});

For the basic example, I’m omitting the controller and view as the main focus of this post is how I implemented live notifications. A more detailed explanation of basic Ajax follows in my next post - a prequel to this one, if you will :).

With the code above, a single user’s action of submitting the form sets off the whole chain of events. But for live notifications, more than one user is involved and the action that changes one user’s data is hapenning on another user’s client! Making this happen twisted my brain into a pretzel at first, but after several attempts I got the functionality I wanted. A description of these follows below.

Attempt #1: Refresh a Single Div Every 3 Seconds

In order to get a single part of the page to update without the entire page refreshing, I used a setInterval() function to make an Ajax request every 3 seconds. This would make a GET request to a custom route: users/:id/live_notifications that hit an action named live_notifications in the UsersController.

1) Separate the “live notifications” div into a partial:

images

2) Create a route and a controller action:

config/routes.rb
1
get 'users/:id/live_notifications' => 'users#live_notifications'
app/controllers/users_controller.rb
1
2
3
4
5
6
7
def live_notifications
  @user = User.find(params[:id])

  respond_to do |format|
    format.js
  end
end

3) Make the Ajax request to hit users#live_notifications every 3 seconds:

app/assets/javascripts/live_notifications.js
1
2
3
4
5
6
7
8
9
10
11
12
13
$(document).ready(function() {
    var currentUrl = window.location.href;

    // Given that we're at a url like 'users/:id', this saves the unique id of the user whose show page we are currently looking at
    var userID = currentUrl.substr(currentUrl.lastIndexOf('/') + 1);

    setInterval(function() {
        $.ajax({
            type: "GET",
            url: "/users/" + userID + "/live_notifications"
        });
    }, 3000);
});

4) Once this Ajax request hits the controller (which is set up to handle a JavaScript response in the respond_to block), Rails by default will look for app/views/users/live_notifications.js.erb and execute the following to refresh the partial:

app/views/users/live_notifications.js.erb
1
$("#live-notifications").html('<%= j render "live_notifications", user: @user %>');

This is all it took to refresh that single div every 3 seconds. However, it was far from ideal:

  • Lots of refreshing for no reason (like when you’re looking at another user’s page and no notifications are displayed, or when nothing has changed)

  • Things that never would change are part of the div that is being refreshed (like header text, for example)

  • Last but not least, this kind of indescriminate refreshing breaks the functionality of forms…

images

A Quick Fix for Form Problems

Thanks to jQuery pseudo selectors, we can stop the Ajax call from being made if an input field is currently focused:

app/assets/javascripts/live_notifications.js
1
2
3
4
5
6
7
8
9
10
11
12
$(document).ready(function() {
    ...

    setInterval(function() {
        // prevent the Ajax call from being made if the input field in the live notifications div is focused
        if (!$('#transaction_due_date').is(":focus")){
            $.ajax({
                ...
            });
        }
    }, 3000);
});

Getting More Specific

The next logical step up from the “refresh everything all the time” strategy was to refresh the live notifications div only when looking at one’s own show page (in other words, when the user id in the url matches the id of the current user stored in the session).

In order to make the current_user from the Rails backend available to JavaScript, I put the following in my application layout:

app/views/layouts/application.html.erb
1
2
3
4
5
<script type="text/javascript">
  window.currentUser = {
      id : "<%= current_user.id if current_user %>"
  }
</script>

With current_user.id stored in an object attached to the window, making sure the Ajax call only gets made when a user is looking at his/her own profile page is simple:

app/assets/javascripts/live_notifications.js
1
2
3
4
5
6
7
8
9
10
$(document).ready(function() {
    var currentUrl = window.location.href;
    var userID = currentUrl.substr(currentUrl.lastIndexOf('/') + 1);

    if (currentUrl.endsWith('/users/' + window.currentUser.id)) {
        setInterval(function() {
            ...
        }, 3000);
    }
});

The Final Refactor: Only Refresh When things Have Changed

To change only things that have changed, when they have changed, the ability to compare what’s on the back end with what’s on the front end is needed.

For this part, I needed to capture the state of all of a user’s transactions and friendships (the two things for which there may be a notification), and hide this information on the page. I created a method in the User model that returns all of these states in an array, and put this into a hidden element on the page:

images

Since the transactions could also have due dates, I did something similar for those.

The next step was to change the code in app/views/users/live_notifications.js.erb to check the current state of the user’s transactions and friendships and see if the #all-categories-tracker is up to date:

app/views/users/live_notifications.js.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var previousInteractionStates = $('#all-categories-tracker').text();
var currentInteractionStates = "<%= @user.transactions_and_friendships_data_array %>";

var countOrStatusChanged = previousInteractionStates !== currentInteractionStates;

var previousDueDates = $.map($(".due-date"), function(val) {
  return $(val).text()
});
var currentDueDates = "<%= @user.due_date_list %>";

var dueDatesChanged = currentDueDates !== previousDueDates.toString();

if (countOrStatusChanged || dueDatesChanged) {
    // UPDATE THE "MASTER TRACKER"
    $('#all-categories-tracker').html(currentInteractionStates);

    // CHECK EACH INDIVIDUAL CATEGORY AND CHANGE ONLY WHAT’S NEEDED
    <% notification_categories.each do |category| %>
        var currentCategory = "<%= category %>";

        // DEAL WITH ANY CHANGES IN COUNT OR STATE
        var currentCategoryStates  = "<%= @user.states(category) %>";
        var previousCategoryStates = $(".category-tracker#" + currentCategory).html();

        // if there's been a change for this category
        if (currentCategoryStates !== previousCategoryStates) {
            // Update the DOM accordingly
             
        }

        // DEAL WITH CHANGED DUE DATES FOR TRANSACTIONS
        if (currentCategory === 'active_requested_transactions' && dueDatesChanged) {
            // Update the DOM accordingly
              
        }
    <% end %>
}

With this final refactoring, checks are in place that stop the unnecessary refreshing that came with the first version:

images

Alternatives

The method I described above for achieving live notifications is very basic Ajax polling. Every serveral seconds, a request/response cycle fires. This inevitably means lots of database querying, even if the amount of refreshing can be reduced to a minimum. In my search for ways to reduce the burden this puts on the database, here are some other techniques I’ve found that may offer some advantages:

1) Long Polling

With this technique, a request fires and waits for a change before sending a response. Then another request can be fired.

2) Web Sockets

Very different than Ajax polling or long polling, web sockets are used for continuous communication between server and client.

3) Server-Sent Events

Unlike web sockets which allows for continuous back and forth from server to client, this technique establishes a persistent connection that allows the server to send data to the client, but not the other way around.