AJAX in practice

  1. Adding comments to coffee houses
  2. DB

    1. script/generate migration create_comments
    2. 
      class CreateComments < ActiveRecord::Migration
        def self.up
          create_table "comments", :force => true do |t|
            t.column "body", :text
            t.column "coffee_house_id", :integer
            t.column "author", :string
            t.column "created_at", :datetime
          end
        end
      
        def self.down
          drop_table "comments"
        end
      end
      
    3. rake db:migrate

    Model

    1. script/generate model comment
    2. app/models/comment.rb
      
      class Comment < ActiveRecord::Base
        belongs_to :coffee_house
        validates_presence_of :author, :body, :coffee_house_id
      end
      
      
    3. app/models/coffee_house.rb
      
      class CoffeeHouse < ActiveRecord::Base
        #...
        has_many :comments, :order => 'created_at DESC'
        #...
      end
      

    Controller

    1. app/controllers/coffee_house_controller.rb
    2. 
      public
        def show_comments
          coffee_house = CoffeeHouse.find_by_id(params[:id])
          if coffee_house
            @comments = coffee_house.comments #fetch list of all comments for this coffee house
          else
            render :text => 'Coffee house not found!' and return
          end
          render :partial => 'comments' #this partial shows a list of all comments
        end
      
        def new_comment
          @coffee_house = CoffeeHouse.find_by_id(params[:id])
          if @coffee_house
            @comment = Comment.new(:coffee_house_id => @coffee_house.id)
            render :partial => 'new_comment' #this partial shows the add comment form
          else
            render :text => 'Coffee house not found!'
          end
        end
      
         def create_comment
          @comment = Comment.new(params[:comment])
          if(@comment.save)
            @comments = @comment.coffee_house.comments
            render :partial => 'comments'
          else
            render :text => 'Could not save comment'
          end
        end
      
      

    View

    1. app/views/layouts/coffeehouse.rhtml
      
      <%= javascript_include_tag :defaults %>
      
    2. app/views/coffee_house/_comments.rhtml
      
      <% if @comments.size > 0 %>
        
        <% for comment in @comments %> <%= comment.author %> said <%= time_ago_in_words(comment.created_at) %> ago: <%= simple_format(comment.body) %> <% end %>
      <% else %> no comments, add one! <% end %>
    3. app/views/coffee_house/_new_comment.rhtml
      
      <% form_remote_tag :url => {:action => 'create_comment'},
        :update => 'comments',
        :complete => "Effect.SlideDown('comments')" do %>
        <%= hidden_field 'comment', 'coffee_house_id' %>
        <div><label for="comment_author">Your name: </label>
        <%= text_field 'comment', 'author' %></div>
        <div><label for="comment_body">Your comment:</label>
        <%= text_area 'comment', 'body' %></div>
        <%= submit_tag 'Add comment' %>
      <% end %>
      
      
    4. app/views/coffee_house/show.rhtml
      
      <%= link_to_remote 'Add comment', :url=>{:controller => 'coffee_house', :action => 'new_comment', :id => @coffee_house}, :update => 'comments' %>
      <%= link_to_remote 'Show comments', :url=>{:controller => 'coffee_house', :action => 'show_comments', :id => @coffee_house}, :update => 'comments' %>
      
      <div id="comments"></div>
      
      

  3. In place editing
  4. Provide AJAX in place editing for coffee house details.

    Model

    app/models/coffee_house.rb -- add new virtual attribute which will be helpful for in place city editing
    
    def city_name
      self.city ? self.city.name : ''
    end
    

    Controller

    app/controllers/coffee_house_controller.rb -- at the top
    
      in_place_edit_for :coffee_house, :name
      in_place_edit_for :coffee_house, :street
      in_place_edit_for :coffee_house, :building_number
      in_place_edit_for :coffee_house, :apartment_number
      in_place_edit_for :coffee_house, :zipcode
      in_place_edit_for :coffee_house, :city_name
    
      def set_coffee_house_city_name
        @coffee_house = CoffeeHouse.find_by_id(params[:id])
        if(@coffee_house)
          @coffee_house.update_attribute('city_id',params['value'])
          render :text => @coffee_house.city_name
        else
          render :text => 'Error updating city!'
        end
      end
    

    View

    1. app/helpers/application_helper.rb
      def in_place_collection_editor_field(object,method,container, tag_options={})
          tag = ::ActionView::Helpers::InstanceTag.new(object, method, self)
          tag_options = { :tag => "span",
            :id => "#{object}_#{method}_#{tag.object.id}_in_place_editor",
            :class => "in_place_editor_field" }.merge!(tag_options)
          url = url_for( :action => "set_#{object}_#{method}", :id => tag.object.id )
          collection = container.inject([]) do |options, element|
            options << "[ '#{escape_javascript(element.last.to_s)}', '#{escape_javascript(element.first.to_s)}']" 
          end
          function =  "new Ajax.InPlaceCollectionEditor("
          function << "'#{object}_#{method}_#{tag.object.id}_in_place_editor',"
          function << "'#{url}',"
          function << "{collection: [#{collection.join(',')}], id: '#{object}_#{method}'});"
          tag.to_content_tag(tag_options.delete(:tag), tag_options) + javascript_tag(function)
      end
      
      solution taken from in_place_collection_editor_field
    2. app/views/coffee_house/show.rhtml -- at the top
      
      <p><label>Name:</label>
      <%= in_place_editor_field 'coffee_house','name' %></p>
      <p><label>Address:</label> </p>
      <p><%= in_place_editor_field 'coffee_house', 'street' %>
      <%= in_place_editor_field 'coffee_house', 'building_number' %>
      / <%= in_place_editor_field 'coffee_house', 'apartment_number' %></p>
      <p><%= in_place_editor_field 'coffee_house', 'zipcode' %>
      <%= in_place_collection_editor_field 'coffee_house', 'city_name', City.find(:all).collect{|c| [c.name,c.id]} %></p>
      

  5. Auto complete simple search
  6. Controller

    app/controllers/coffee_house_controller.rb
    1. add auto complete for coffee house name:
      auto_complete_for :coffee_house, :name
    2. add search function:
      
        def search
          @query = params[:coffee_house][:name] if params[:coffee_house]
          @query ||= params[:query]
          @query ||= ''
          @coffee_house_pages, @coffee_houses = paginate :coffee_houses,
            :conditions => ['name LIKE ?','%'+@query+'%'],
            :per_page => 5
          render :partial => 'list'
        end
      

    View

    1. public/stylesheets/coffee_houses.css
      
      .coffee_house{
        width: 400px;
        border: 1px solid #000000;
        margin: 10px;
      }
      
      .thumb{
        float:left;
      }
      
      .info{
        width: 300px;
        float:right;
      }
      
      .search-box{
        width: 300px;
        padding: 10px;
      }
      
      
    2. app/views/coffee_house/_list.rhtml -- new list partial
      
      <% for coffee_house in @coffee_houses %>
        <%= render :partial => 'list_element', :locals => {'coffee_house' => coffee_house} %>
        <div>
          <%= link_to 'Show', :action => 'show', :id=>coffee_house %>
          <%= link_to 'Edit', :action => 'edit', :id => coffee_house %>
          <%= link_to 'Destroy', {:action => 'destroy', :id=>coffee_house}, :method => :post %>
        </div>
      <% end %>
      
      <% if @query %>
        <%= link_to_remote "Previous", :url => { :query => @query, :page => @coffee_house_pages.current.previous }, :update => 'coffee_houses' if @coffee_house_pages.current.previous %>
        <%= link_to_remote "Next", :url => { :query => @query, :page => @coffee_house_pages.current.next }, :update => 'coffee_houses' if @coffee_house_pages.current.next %>
      <% end %>
      
    3. app/views/coffee_house/_list_element.rhtml -- new list element partial
      
      <div class="coffee_house">
        <% photo = coffee_house.photos.first %>
        <%= image_tag(photo.public_filename('thumb'),:class=>'thumb') if photo %>
        <div class="info">
        <h3><%= link_to coffee_house.name, :controller => 'coffee_house', :action => 'show', :id => coffee_house %></h3>
          <p><%= link_to coffee_house.company.name, :controller => 'company', :action => 'show', :id=>coffee_house.company if coffee_house.company %></p>
          <p><%= "#{coffee_house.street} #{coffee_house.building_number}" %></p>
          <p><%= link_to coffee_house.city.name, :controller => 'city', :action => 'show', :id => coffee_house.city if coffee_house.city %></p>
        </div>
        <div style="clear:both"></div>
      </div>
      
    4. app/views/coffee_house/list.rhtml -- add the search box and replace old listing
      
      <h1>Listing Coffee Houses</h1>
      
      <div class="search-box">
        <% form_remote_tag :url => {:action => 'search'}, :update => 'coffee_houses',
        :loading => "Element.show('search-loader')", :complete => "Element.hide('search-loader')" do %>
          <%= text_field_with_auto_complete 'coffee_house','name' %>
          <%= submit_tag 'Search' %>
        <% end %>
        <%= image_tag 'ajax-loader.gif', :id => 'search-loader', :style => 'display:none' %>
      </div>
      
      <div id="coffee_houses">
        <%= render :partial => 'list' %>
      
        <%= link_to 'Previous page', { :page => @coffee_house_pages.current.previous } if @coffee_house_pages.current.previous %>
        <%= link_to 'Next page', { :page => @coffee_house_pages.current.next } if @coffee_house_pages.current.next %>
      </div>
      
      <%= link_to 'New coffee_house', :action => 'new' %>
      <hr>
      <%= link_to 'New company', :controller => 'company', :action => 'new' %>
      <%= link_to 'New city', :controller => 'city', :action => 'new' %>
      
      
      ajax-loader.gif