Adding AJAX Bookmarks to Your Rails Application (Part 1 of 2)

It you want to add the ability to bookmark pages in your Rails application, its actually a fairly straightforward thing to do. You can even do them in AJAX. There may be better ways to do this, but this way is somewhat abstract and it works for me, so hopefully it can work for you too. It is abstract in the sense that it will work for models with different URL styles and different column names.

The way this works is that you add a bookmark icon (which is initially disabled) to a show <model_name> page. When the user clicks on the bookmark icon, an AJAX query will be made in the background and update the users bookmark lists. I am approaching this from an abstract methodology. Meaning that I have “forced” these methods to work with models executed in various fashions (as I give examples of below). The AJAX call is going to be simply work as a toggle. It will actually call a toggle method in the bookmarks controller and change the current value and replace the image. The user can then view the pages they have bookmarked in their profile.

I have decided to break this into a multi-part blog entry because it ends up being quite long. Not necessarily in how long it takes, just the amount of space it takes to show all the code. I have done my best to only show relevant code and maintain brevity. Note: I will not cover how to allow for unobtrusive AJAX calls. That is beyond the scope of this set of posts.

I chose the following images to represent this. The empty blue star signifies that a bookmark is disabled. The filled in blue star represents the fact that the page is bookmarked by the current user. You can go to Google images or anywhere else to find enabled/disabled images. I just chose these particular images because they matched my site.

Star Bookmark Enabled

Star Bookmark Enabled

Star Bookmark Disabled

Star Bookmark Disabled

The first thing you need to do is to create the migrations for bookmarks and bookmarktypes. Make sure the bookmarktypes exist for every model that you want to allow to be bookmarked and are the singularized lowercase version of the model name (e.g. Users -> user, Businesses -> business).

class AddBookmarks < ActiveRecord::Migration
  def self.up
    # Note we are setting no primary key here intentionally
    create_table :bookmarks, {:id => false, :force => true} do |t|
      t.belongs_to  :user
      t.integer     :model_type_id
      t.integer     :model_id
      t.timestamps
    end
   
    create_table :bookmarktypes, :force => true do |t|
      t.string    :model
      t.timestamps
    end
    Bookmarktype.create(:model => 'business')
    Bookmarktype.create(:model => 'singularlowercasemodelname')
  end

  def self.down
    drop_table :bookmarktypes
    drop_table :bookmarks
  end
end

Here we are adding the bookmark and bookmarktype model.

class Bookmark < ActiveRecord::Base
  belongs_to  :user
  has_one     :bookmarktype
 
  def self.find_bookmark(user_id,model_type_id,model_id)
    find(
      :first,
      :conditions => ["user_id = ? AND model_type_id = ? AND model_id = ?",
               user_id, model_type_id, model_id],
      :limit => 1
    )
  end

  def self.is_bookmarked?(user_id,model_type_id,model_id)
    find_bookmark(user_id, model_type_id, model_id)
  end
end

class Bookmarktype < ActiveRecord::Base
  validates_presence_of :model, :on => :create, :message => "can't be blank"  
end

Once the models and migrations are complete, then we can go ahead and add the controller code. Create a bookmarks controller. This will contain the toggle method that will be doing the majority of the legwork. You’ll notice that the toggle method below uses a url param. This works in the following way:

  1. When the AJAX link is clicked on the model show view page, it will send an AJAX request to bookmarks controller that looks like this:
    • http://website/bookmarks/toggle?url=/businesses/mybusiness&_=1258315454002
    • http://localhost/bookmarks/toggle?url=/user/1234&_=1258315600823

    The _ param is simply the authenticity token put into a GET string.

  2. The URL parameter will be broken down into an array:
    • [0]: empty   OR   empty
    • [1]: businesses   OR   user
    • [2]: mybusiness   OR   1234
  3. We get the modeltype (which will soon act just like a class like you would call Business.find or User.find). We do this by capitalizing it and constantizing it. We capitalize it so it looks like the model class name otherwise we generate a nomethod error. Constantizing the model name turns it into something that is usable as a class: Bookmarktype.find_by_model(uri_array[1].singularize).model.capitalize.constantize
  4. This is where it gets interesting and you get to deal with multiple types of URLs in a model: modeltype.find_id_by_site_url(request_uri). The find_id_by_site_url method is a method that requires definition in each model because it can be different in each model. We will cover this later.
  5. Then we check to see if the bookmark exists by calling the find method we wrote in our Bookmark model. We use the result to determine whether the bookmark should be added or removed in the actual toggle process.
  6. The bookmark status is toggled in the database, either added or deleted. The delete_all method is used because there is no way in Rails (that I could find) to delete a row with no primary key.
  7. Finally, the Javascript is rendered so nothing happens to the original calling page.

And finally, here is the actual bookmarks controller. We will go through the usage of the show method later.

class BookmarksController < ApplicationController
  layout 'home'
 
  def show
    @user = User.find(params[:user_id])
    @bookmarks = Bookmark.find_all_by_user_id(params[:user_id]).paginate(:page => params[:page], :per_page => 10)
  end

  def toggle
    uri_array = CGI::unescape(params[:url]).split(%r{/})
    bookmark_type_id = Bookmarktype.find_by_model(uri_array[1].singularize).id
    modeltype = Bookmarktype.find_by_model(uri_array[1].singularize).model.capitalize.constantize
    model_id = modeltype.find_id_by_site_url(request_uri)
   
    @bookmark = Bookmark.find_bookmark(current_skydiver.id,bookmark_type_id,model_id)
   
    # We use a delete_all here because there is no other way in Rails to delete a row from a table without a PRIMARY KEY id
    if ( @bookmark )
      Bookmark.delete_all(["user_id = ? AND model_type_id = ? AND model_id = ?", current_user.id, bookmark_type_id, model_id])
    else
      Bookmark.create( :user_id => current_user.id, :model_type_id => bookmark_type_id, :model_id => model_id)
    end
   
    respond_to do |format|
      format.html
      format.js { render :layout => false }
    end
   
  end

end

Lastly on the AJAX side of this, we need to put response view together. This is a very simple line of code in the toggle.js.erb:

swapBookmarkImage();

And for the unobtrusive aspect of Javascript and in the interest of keeping Javascript all in the same place, add that function to your application.js:

function swapBookmarkImage()
{
    var origSrc = jQuery("img#bookmark_star").attr('src');

    // Change the image src toggle based on the current image
    if ( origSrc == '/images/star_bookmark_disabled.png' ) {
        var newSrc = '/images/star_bookmark_enabled.png';
    }
    else {
        var newSrc = '/images/star_bookmark_disabled.png';
    };
   
    // Swap out the image
    jQuery("img#bookmark_star").attr('src', newSrc);
}

Click here for part 2 of this series.

Posted in Rails. Tags: , . 2 Comments »