Archive

Tag Archives: ajax

I wanted to build a page with nice in-place add/delete of some items, and I wanted something nicer-looking than the usual Javascript alert popup for confirmations. Here’s what I came up with; this is in a Rails-powered app using (of course) JQuery. I’ve used Impromptu in the past as a powerful JQuery-based popup library, and I used it again in this case. I won’t go through the setup steps for it, since the site provides a good walk-through.

I started with a basic table showing the items; for the sake of this example, let’s say these are books in my library, displayed in a table. I want to remove books and add books via AJAX, and update the table dynamically.

<table id="books" width="90%" class="booklist">
  <tr>
    <th>Book #</th>
    <th>Title</th>
    <th>Remove</th>
  </tr>
  <% @library.books.each do |book| -%>
    <tr id="<%= book.number %>" class="<%= cycle("even", "odd") -%>">
      <td><%= book.number %></td>
      <td><%= book.title %></td>
      <td align="center">
        <a href="#" onClick="removeBook('<%= book.number %>');">
        <%= image_tag('delete.png', :width => '20') %>
        </a>
      </td>
    </tr>
  <% end -%>
</table>

<form id="add_form" action="#">
  <input type="text" id="newbook" size="20" />
  <%= image_tag 'button-add.png', :id => 'add_button', :width => '20', :style => 'vertical-align:middle' %>
</form>

Pretty straightforward: After the header row, we loop through the library, and for each book we output a row. Note that we use the book’s number (pretend with me that it’s something like the ISBN that has no spaces or weird characters) as the id of each row — this number is also passed in to the removeBook() function when someone clicks the delete button, which allows us to dynamically remove the table row.

After the table, we have a little form for adding a new book — this assumes that we already have a database of books, and we’re just going to look up the new book by number, and add it to our library. All we have here is a button image, to which we will attach a JQuery click action. It would be nice to use the JQuery click action on the remove button as well, but it complicates things when it comes to determining which row was clicked on (yes, we could put the number in as an id on the image and look it up, which I’ll probably look at doing later).

So, when someone clicks on the remove button, what happens? The removeBook() function is called:

  function removeBook(number) {
    $.prompt('Are you sure you want to remove book #' + number +
        ' from the library?<input type="hidden" name="num" id="num" value="' + number + '"/>',{
      callback: removeCallback,
      buttons: { Yes: 'Remove', No: 'Cancel' }
    });
  }

  function removeCallback(button, msg, formvals) {
      if (button == 'Remove') {
        // Call server to remove the book
        $.ajax({
            type: "PUT",
            url: "/libraries/<%= @library.id %>.js",
            data: ({remove : 1, book_num : formvals.num}),
            success: function(msg){
                // Remove the row from the table
                $('#' + formvals.num).remove();
            },
            error: function(response, textStatus, errorThrown){
                var msg = (response.status == 403) ? response.responseText : "unknown error.";
                $.prompt('Error removing book from library: ' + msg);
            }
        });
      }
  }

The removeBook() function uses the Impromptu library to display a message via $.prompt(), specifying the removeCallback() callback function and two buttons. If the user clicks the “No” button, the callback won’t do anything. If they click the “Yes” button, then the callback will make a JQuery AJAX call to the server.

The Impromptu callbacks receive three parameters: the button clicked, the message if any, and the values of any form fields, if any. We were tricky and placed a hidden form field into the popup in the removeBook() function, called “num”. Note that Impromptu uses the NAME of the field, not the ID, for looking up the values; in this case both are “num” to make life easy. We then check the button value to see if it’s “Remove”, and we get the book’s number from the form using formvals.num.

Being somewhat RESTful, we do a PUT to the libraries controller with the ID of the library we’re changing. Note that we specify “.js” for the call, so that our controller will respond appropriately, since this is calling the update() action. As the AJAX call’s data, we pass in “remove” as a flag, and “book_num” to tell it which book to remove. If everything goes well, the success function is called. All it does is to remove the row from the table, using the fact (as noted earlier) that the row id is the book number.

The relevant lines from the controller’s update() method:

  # Have we been called to remove a book?
  if (params[:remove])
    success = @library.remove_book(params[:book_num])
  elsif (params[:add])
    # We've been called to add a book
    success = @library.add_book(params[:book_num])
    if (success)
      # Now let's look up the book so we can get the title for display
      book = Book.find(params[:book_num])
      if (book)
        result = { "number", book.number, "title", book.title}
      else
        # It's a book we don't have info about, but that's okay
        result = { "number", params[:book_num], "title", "Unknown"}
      end
    end
  end

  . . .  # Do other work as needed

respond_to do |format|
  if (success)
    flash[:notice] = 'Library was successfully updated.'
    format.html { redirect_to(@library) }
    format.xml  { head : ok }
    format.js   { render :text => "OK" }
    format.json { render :json => result }
  else
    format.html { render :action => "edit" }
    format.xml  { render : xml => @library.errors, :status => :unprocessable_entity }
    format.js   { render :text => "Failed to save update", :status => 403 }
    format.json { render :json => { "msg", "Failed to save update" }, :status => 403 }
  end
end

(Note that in lines 25 and 30 I had to add a space after the colons, because WordPress is stupid and for some reason thinks that I want damn smilies inside a sourcecode block. Uh, okay, sure.)

This is treating a book removal as a specialized variation of an update; an argument could certainly be made that instead it should be a new action. My jury’s still out, but there are things I like about doing it this way, including keeping the routes simple and encapsulating all library update-related activities in one place. Regardless, if the removal goes well, we return an “OK” — if not, we return a failure message, via the format.js within the respond block. Notice that in the Javascript above, the error-handler will display response.responseText if the response code is 403, and here we set the status to 403 if an application error occurred during the update. In that case the text we return will be displayed.

While you’re looking at this, check out the JSON responses, because we’ll be using those for adding a new book to the library, below.

That’s it. When a user clicks on the Remove button, it will invoke the removeBook() function, which will display a nice-looking confirmation popover using Impromptu. If the user confirms, then the callback function will make the AJAX call. The controller will remove the book from the library, and return a text “OK” if all goes well. The success function will then remove the associated row from the table, and we’re done!

On to adding a new book, which is trickier, though it uses much the same ideas of course. A user enters a book number into the form and clicks the button. What makes something happen then? The JQuery click function we’ve associated with it. Here’s the Javascript:

  $('#add_button').click(function() {
      var add_number = $('#newbook').val();
      if ((!add_number) || (add_number == '')) {
          return false;
      }
      // Call server to add the book to the library
      $.ajax({
          type: "PUT",
          url: "/libraries/<%= @library.id %>.json",
          data: ({add : 1, book_num : add_number}),
          success: function(data){
              // Add a new row to the table
              addTableRow('#books', data['number'], data['title']);
              $('#newbook').val('');   // Clear the input field
          },
          error: function(response, textStatus, errorThrown){
              var msg = (response.status == 403) ? response.responseText : "unknown error.";
              $.prompt('Error adding book to library: ' + msg);
          }
      });
  });

  function addTableRow(book_table, book_number, book_title){
      var row_class = $('tr:last', book_table).attr("class");
      var new_class = (row_class == 'odd' ? 'even' : 'odd');
      var tds = '<tr id="' + book_number + '" class="' + new_class + '">';
      tds += '<td>' + book_number + '</td>';
      tds += '<td>' + book_title + '</td>';
      tds += '<td align="center">' +
        '<a href="#" onClick="removeBook(\'' + book_number +
        '\');"><img src="/images/delete.png" width="20"></a></td></tr>';
      if($('tbody', book_table).length > 0){
          $('tbody', book_table).append(tds);
      }else {
          $(book_table).append(tds);
      }
  }

Whew, that’s a fair amount of Javascript; but it’s pretty straightforward, nonetheless. The first block is of course the click function that we attach to the button. When a user enters a number into the text field and clicks the button, this function will be called, and the first thing it does is to grab the value from the text field. And excuse me while I take a brief moment to rant in a minor way about the fact that the function is called val() instead of value(). Really, are two more letters too much to ask in exchange for better clarity? Every single time I have to write something like this I start with value() and waste a few minutes reminding myself that it’s shortened for no apparent reason. Okay, rant over, sorry.

We do a quick check to make sure that there’s actually something in the text field and return if not. Otherwise, we make our AJAX call, to the same URL as the remove function but with parameters “add” and “book_num”. If you go back now and look at the controller code again, you’ll see where it checks for params[:add] and, if it’s there, it adds the book to the library. After that it does a quick query to grab the book, so that it can return the title. It actually makes a Hash called “result” with the number and the title, which is then used in the render :json => result line. Within render, it will actually JSONize the Hash and send it back to the caller.

Just to prevent any confusion, I’ll take a second here to note that in most cases you probably won’t need to do the Book.find call that’s shown here, because you’ll likely be using ActiveRecord and you might be able to look in the library instance or use some other workaround to have the book available. In my case (remembering that this is a ‘cleansed’ version of my real work, which isn’t about books and libraries at all), I’m using Redis as the data store so adding a book to a library is actually a matter of adding its key to a set, so I don’t have the actual object until I do the find — which is super-fast anyway.

Okay, so if all goes well, the success function is called, which needs to add the new row to the table (you can see that it also clears the value of the text field so it’s empty again). To add the row, it calls the addTableRow() function, passing in the table, book number, and title. To give credit where it’s due, this function is a stripped-down version of the one shown in this blog post, which was quite helpful. This version does some things specifically for this purpose, and isn’t as generic as the original.

The first two lines determine what the class should be for the row, because the table alternates ‘even’ and ‘odd’ in order to have a new striped appearance. Here we grab the class of the last row, and then set our new_class to be whatever the current last row isn’t. That is, if the last row is ‘even’ then our new one will be ‘odd’ and vice-versa. Then we build the HTML for our new row, carefully setting the id of the row to the book number, inserting the number and title, and then adding on the somewhat ugly final table element to have the Remove button with its proper onClick. The last few lines determine whether the table has a tbody or not, and appends our new row accordingly to the body or the table itself, so this works if we start with an empty library.

And that is that — when a user enters a number and clicks the add button, JQuery has attached the click event to it, so our “add” function is called, which gets the value, does the AJAX call which does the server-side adding and returns the book number and title as JSON. Our function takes that and gets a row added to the bottom of the table accordingly.

There’s only one minor improvement which I will make to this shortly, though it’s a minor use case: when a user deletes a book, the corresponding table row is removed but the classes of the remaining rows aren’t being updated. That means we lose our nice even/odd striping. As an exercise for the reader, I’ll let you add the code to the success function inside removeCallback() to iterate the table rows and set the class attributes after removing the row.

I hope this proves useful to some folks out there. The combination of Impromptu for pretty confirmation popovers (and error messaging), with in-place add/remove, provides a really nice user experience. And the patterns here end up being useful all over the place, so you’ll likely want to do the next step, which is to make this code more generic and put it into partials that you can re-use in multiple pages. Enjoy.

Follow

Get every new post delivered to your Inbox.