AJAX stands for Asynchronous Javascript And XML. In today’s lab, we are going to modify our Twitter application from last week to use AJAX to dynamically update webpages.

Start with Twitter from Last Week ago

We will start by copying our Twitter application from last week:

$ cd twitter
$ git pull
$ git tag -a lab9

Start up the server and make sure everything works. In particular, you should verify that a person can log in and submit new tweets.

Before we can start using AJAX technologies, we need to prepare our application. Specifically, we need to set up the application to load the javascript libraries we will use. We also will need to set up our application to use partial templates (Lab 7: Views and Uploads).

Clean up Twitter app

First, we need to create a layout for our twitter application. Rails figures out which layout to use in three ways. First, if the controller has the line layout 'name', then it will use the layout app/views/layouts/name.html.erb. Second, if there is no explicitly specified layout, then it will look for the file app/views/layouts/controller_name.html.erb. Finally, if that file doesn’t exist, it will use app/views/layouts/application.html.erb. Therefore, this last one (application.html.erb) serves as the layout for the entire application. Let’s edit that file to create our “twitter” layout:

<!DOCTYPE html>
<html>
<head>
  <title>Rick's Twitter</title>
  <%= stylesheet_link_tag    "application", media: "all", "data-turbolinks-track" => true %>
  <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
  <%= csrf_meta_tags %>
</head>
<body>

<h1> Rick's Twitter </h1>

<div id="container">
<%= yield %>
</div>

<hr />
<%= link_to "Home", tweets_url %>

</body>
</html>

As you edit this, you’ll notice a couple of things. First, it automatically loads all CSS stylesheets that it can by loading application.css. If you create a .css file in the app/assets/stylesheets directory, this layout will automatically load it. Second, it loads a bunch of javascript libraries. Also, I customized this layout a bit by adding three things:

Now open up your application, and test to see if the layout is working. It should have the new title, and a link to the home page at the bottom of every page.

Next, we can start to edit the CSS style file in app/assets/stylesheets/twitter.css:

h1 {
  color: white;
  background-color: #153;
  text-align: center;
}

#container {
  margin: 0 auto;
  width: 600px;
  background-color: #7f9;
  padding: 10px;
}

(Note, you can make your CSS look however you want.)

Default Routes

Next, we can set up a “default” route. This is where the application should go if you don’t specify a path. To do this, we need to edit the config/routes.rb file to tell it where to go:

  root to: 'tweets#index'

Now test this. Go to http://webdev.cas.msu.edu:xxxx and see if it works.

Use partials to display tweets

Next, we need to convert the main index.htm.erb (in the tweets controller) page to partials. First, copy the inside of the main loop and paste it into a new partial file app/views/tweets/_tweet.html.erb: (don’t forget the underscore in the filename)

  <div class="tweet" id="tweet_<%= tweet.id %>">
    <div class="content">
     <%= tweet.content %>
    </div>
    <span style="font-size:small;">
     Posted at <%= tweet.posted %>
     <% unless tweet.user.nil? %>
       by <%= link_to tweet.user.username, user_url(tweet.user) %>
     <% end %>
    </span>
    <hr />
  </div>

Notice that the main variable is called tweet, which is also the same name as the partial. Also notice that I wrapped the whole tweet in a div with the ID of the tweet. This way we can refer specifically to this block of HTML and do things like highlight it or remove it, based on the ID number of the tweet.

Next, we should convert out index.html.erb to use a partial rather than rendering the whole list. To do this, delete all of the code that creates the list – the entire .each loop – and replace it with a single line that renders a partial on a collection:

<div id="tweet_list">
<%= render @tweet_list %>
</div>

Also, we wrapped this list in a div so we can identify the list later.

Test your application to make sure it still renders the list of tweets properly.

The other thing we can easily do is change the app/views/users/show.html.erb code to also use this same partial. It should look pretty much the same. Don’t forget to also wrap that code in a div block:

<div id="tweet_list">
  <%= render @user.tweets %>
</div>

Notice that we can use the same partial to display tweets, and just pass in a different list of tweets to display.

Use partials to display the tweet submission form

OK, now our application has the right structure so we can start making it use AJAX. The first step is to put our new tweet submission form at the top of the main page, much like Twitter and Facebook do. To do this, we will use another partial to display the form. We start by simply copying the form out of new.html.erb and paste it into a new file we will call _newtweet.html.erb. (Don’t forget the underscore.) We are going to make one change, though. Instead of using the @tweet variable to seed the form, we will use a newtweet variable:

<div id="newtweet">
<%= form_for newtweet do |f| %>
  <% if newtweet.errors.any? %>
  <div id="errorExplanation">
    <h2><%= pluralize(newtweet.errors.count, "error") %> prohibited this user from being saved:</h2>
    <ul>
    <% newtweet.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
  <% end %>

  <%= f.label :content, "What's happening?" %>
  <%= f.text_area :content, :rows => 3, :cols => 70 %>
  <%= f.submit "Tweet" %>
<% end %>
</div>

Next, we will render that partial on the top of the main index.html.erb page:

  <%= render :partial => "newtweet", :object => Tweet.new %>

Notice that we are passing a new, blank, Tweet object into the partial.

Now, test and see if our application looks right. It should have a text box at the top of the page where users can enter a tweet, and a list of all the current tweets. And it should all work correctly.

Submit Tweets via Ajax

Next, we can change our newtweet form to submit the results via AJAX rather than through a traditional POST request. We do this by changing one line. The first form_for line should be changed to specify :remote => true:

<%= form_for newtweet, remote: true do |f| %>

The only change here is that we are adding remote: true. This tells rails to submit the tweet in the background via AJAX rather than making it a normal request. If you try to make a request right now, nothing on the page will change. So we need to do two more things. First, we need to modify the create action. The actual process of posting a new tweet is still the same, and we don’t want to change it. But we don’t need to redirect the user any more. So we can simply modify the create action to this:

  def create
    tweet_params = params.require(:tweet).permit(:content)
    @tweet = Tweet.new tweet_params
    @tweet.posted = Time.now
    @tweet.user = @user
    @tweet.save
  end

(We can add the error checking back in later…)

Next, we need to tell the web browser what to do after the post action is finished running. To do this, we create a .js.erb file – a ruby javascript file. This file basically contains ruby commands that generate javascript code for you. Our initial file – app/views/tweets/create.js.erb – will contain just one line:

$('#tweet_list').prepend("<%=j render @tweet %>");

This code tells the web browser to render the tweet partial, passing our new @tweet variable to the partial for rendering. THEN, it tells the web browser to insert this HTML code at the top of the tweet_list div. Remember that we put a div block around our tweets and called it id="tweet_list". We are referring to this ID in the HTML when we say insert our new code at the top of this.

In addition to .prepend(), we can also .append() to put it at the end of a block, or .insertBefore() to put it before the named element, or .insertAfter() to put it after the named element.

OK, your application should work now. Try adding a new tweet. The tweet should appear at the top of our list, and it should should appear quickly; it doesn’t need to reload the whole webpage just to display the new tweet.

Notice: when you submit the form, the text of the tweet still remains in the textbox. The new tweet appears, but they don’t have the option to type in a new tweet because the old one is in the way. So let’s replace the whole form with a new form:

$('#newtweet').replaceWith("<%=j render :partial => "newtweet", :object => Tweet.new %>");

The other thing to notice, now: it is hard to tell that a new tweet has been added. So let’s add a temporary highlighting by adding another line to our create.js.erb file:

$('#tweet_<%= @tweet.id %>').effect("highlight");

However, to get this effect to work, you need the jquery-ui javascript library. This is packaged in the jquery-ui-rails gem. To install this, add this line to your Gemfile:

gem 'jquery-ui-rails'

We also need to include the javascript in our application. Open up the file app/assets/javascripts/application.js and add the following line:

//= require jquery.ui.all

Not your highlighting should work. If you want to use a different effect, you can see all of the jquery-ui effects on their webpage.

AJAX Deleting Tweets

Next, lets add the ability for any user to delete his or her own tweets. The first thing we are going to do is revisit our code that checks for a logged in user in the tweets controller. Specifically, we are going to split it into two pieces: one that gets the currently logged in user, and the other that enforces the “must be logged in” requirement:

private
  def get_logged_in_user
    id = session[:user_id]
    unless id.nil?
      @current_user = User.find id
    end
  end

  def require_login
    get_logged_in_user
    if @current_user.nil?
      flash[:notice] = "You must log in first"
      redirect_to login_url
    end
  end

This split is nice because we can use these separately as before filters:

  before_filter :require_login, :only => [:new, :create]
  before_filter :get_logged_in_user, :only => [:index]

We can also add those same two functions to the bottom of the users_controller.rb file, and add a before filter for the user/show action:

  before_filter :get_logged_in_user, :only => [:show]

Since we are using the @current_user variable to hold the currently logged in user, we need to modify the line in tweets/create that assigns the current user to a tweet:

    @tweet.user = @current_user

OK, now we’ve got the controller checking for a logged in user, and storing their information in the @user variable. We can then use this variable when displaying our tweets to figure out if a tweet was created by the currently logged in user. Add the following code inside the _tweet.html.erb partial:

  <% if (not @current_user.nil?) and (not tweet.user.nil?) and  (tweet.user.id == @current_user.id) %>
    You!
  <% end %>

This code goes inside the unless tweet.user.nil? block. What this does is two things. First it checks to see if there is a user logged in – not @current_user.nil? – and then it checks to see if the user who created this tweet (tweet.user) is the same as the currently logged in user (@current_user). To compare the two objects, it compares the ids of the objects. It says if the currently logged in user created this tweet, then display the word “You!” on the output page. Test this and see if this works. All the tweets of the currently logged in user should be labeled as “You!”. If you log out, no tweets should be labeled this way. And, if you log in as a different user, then that user’s tweets should be labeled “You!”.

The next thing we will do is replace this “You!” text with a link to delete this tweet:

<%= link_to("X", tweet_url(tweet.id), method: :delete, remote: true) %>

This creates a “X” that is a link to delete the tweet via AJAX. To actually delete the tweet, we need to write the code for the controller action and the .js.erb response.

  # Expects :id => the ID of the tweet to delete
  def destroy
    @tweet = Tweet.find params[:id]
    @tweet.destroy
  end

And the destroy.js.erb code:

$('#tweet_<%= @tweet.id %>').fadeOut(500);

Now try it. Go and try to delete something. See if it dynamically removes it from the webpage by causing it to fade away. Also, notice that if you log out, or log in as a different user, you don’t have the ability to delete tweets anymore.

You can experiment using addition jQuery-UI effects here: http://jqueryui.com/docs/effect/