Tag Type-Ahead with Multiple Items
If you've ever implemented type-ahead functionality and wondered how to get it to work when users are entering multiple items, like a list of comma delimited tags, here it is.
You can, of course, roll your own Javascript and Ajax calls but why bother when Rails gives us a nice Javascript helper to handle this. The helper is text_field_with_auto_complete but this defaults to the current controller. For my purposes tags can be entered on different areas of the site for different models so I used text_field_with_auto_complete with one modification:
def text_field_with_auto_complete_with_custom_url(object, method, url_options = {}, tag_options = {}, completion_options = {}) (completion_options[:skip_style] ? "" : auto_complete_stylesheet) + text_field(object, method, tag_options) + content_tag("div", "", :id => "#{object}_#{method}_auto_complete", :class => "auto_complete") + auto_complete_field("#{object}_#{method}", { :url => { :action => "auto_complete_for_#{object}_#{method}" }.update(url_options) }.update(completion_options))end
Note: text_field_with_auto_complete is deprecated and will be moved to a plugin in Rails 2.0.
The third parameter is now a hash of optional url arguments (action, controller, etc). You can also monkey patch the original method or better use some alias/alias_method_chain trickery in your own app. In your view call the newly created helper with your arguments.
<label for="tag_name">Tags:</label><%= text_field_with_auto_complete_with_custom_url :tag, :names, {:controller => "tagging_demo"}, {}, { :tokens => ','} %>
Include the necessary javascript libraries (prototype and scriptaculous):
<%= javascript_include_tag 'prototype' %><%= javascript_include_tag 'scriptaculous' %>
You can set the controller and action to whatever you like but it defaults to a method in the form of auto_complete_for_object_method. Going with that your action could look something like this:
def auto_complete_for_tag_names @tags = Tag.find(:all, :conditions => ["name like ?", "%#{params[:tag][:names]}%"], :order => 'name DESC', :limit => 20) render :layout => falseend
And your view (auto_complete_for_tag_names.html.erb):
<ul class="tags"><% for tag in @tags do -%> <li class="tag"><%= tag.name -%></li><% end -%></ul>
That's all there is to it. The magic comes from providing a token to Ajax.Autocompleter which we provided in the argument hash to text_field_with_auto_complete_with_custom_url ( { :tokens => ','} ). You can use any delimiter you like. You can also customize the UI in your view and CSS.
Here's a demo http://www.naffis.com/demos/tagging_demo
Ajax uploads? Image manipulation & drag-and-drop sorting.
Wouldn’t it be nice to allow uploads in a cool Ajaxy way? Well, because of security restrictions it’s just not possible. There are however ways to create the same effect.
Here’s a quick demo of an ajax-ish image upload as well as some image manipulation functionality, and drag and drop sorting. I’m not sure this will work on all browsers but it’s been tested successfully with most. This was created about 4 months ago and I never had time to polish any of it up so take what you can from it.
http://www.naffis.com/demos/image_demo
First our layout (layouts/image_demo.rhtml):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/strict.dtd"><html> <head> <title>Image Demo</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <%= javascript_include_tag :defaults %> <%= stylesheet_link_tag 'image_demo' %> </head> <body> <div id="titlebar">Image Demo</div> <%= render :partial => "upload_form" %> <div id="centercontent"> <%= yield %> </div> <div id="next"> <%= link_to "Create Animated Gif", :action => "animate" %> </div> <div id="bottom"> © naffis.com 2006 </div> </body></html>
We’re going to extend the form_remote_tag to handle file uploads.
Drop this in your lib directory (lib/remote_uploads.rb):
module ActionView module Helpers module PrototypeHelper alias_method :form_remote_tag_old, :form_remote_tag def form_remote_tag(options = {}) if options[:html] && options[:html][:multipart] uid = "a#{Time.now.to_f.hash}" <<-STR <form method="post" action="#{url_for options[:url].update({:iframe_remote => true})}" enctype="multipart/form-data" target="#{uid}" #{%(onsubmit="#{options[:loading]}") if options[:loading]}> STR else form_remote_tag_old(options) end end end endendAdd the require in your environment.rb:
require 'remote_uploads.rb'
This will create a custom form for file uploads (multipart => true) that submits to a hidden iframe. If it’s not a file upload then it will revert to the standard form_remote_tag of PrototypeHelper.
Some boring half baked styles for our demo:
body { background-color: #FFFFFF; background-image: url(/maps/images/gradient.jpg); background-repeat: no-repeat; color: #666666; font-family: arial, sans; font-size: 100%; line-height: 1.7em; margin: 1em 2em;}#titlebar { font-size: 1.2em; border-bottom: 2px solid #333333; margin-bottom: 1em; padding-bottom: 1em;}h2 { font-size: 1.2em;}ul.navigation { background-color: #333333; padding: 0em 0.5em; list-style-type: none;}ul.navigation li { border-right: 1px solid #666666; display: inline;}.navigation a { color: #FFFFFF; padding: 0.5em;}.description { font-size: 1.2em;}.upload { font-size: 1.2em;}strong { background-color: #FFFF99;}#centercontent { width: 100%; text-align: center; margin-bottom: 1em; padding-bottom: 1em; margin-top: 1em; padding-top: 1em;}#bottom { width: 100%; float: left; text-align: center; border-top: 2px solid #333333; margin-top: 1em; padding-top: 1em;}div.float { width: 120px; padding: 10px; float: left;}div.spacer { clear: both;}div.float img { margin-left: 5px; }div.float p { font-size: 9px; text-align: center; }#image-list ul { list-style: none;}#image-list ul li { list-style: none; display: inline; float: left; width: 120px; height: 120px; padding: 10px; border: 1px solid #000;}We’re using Sean Treadway’s responds_to_parent plugin (http://sean.treadway.info/svn/plugins/responds_to_parent/) to execute our RJS generated javascript in the parent window instead of the iframe which the file upload is submitted to. There are other ways of doing this that use less code but the plugin is simple so why not use it?
Everything from this point on is pretty self explanitory. I can expand on it later but here’s the rest of the code.
Our index:
<div id="image-list"> <ul id="sortable_list"> <% for @asset in @assets %> <%= render :partial => "image_container", :locals => { :asset => @asset } %> <% end %> </ul></div><%= sortable_element('sortable_list', :constraint => false, :url => {:action => :update_positions}) %>Some partials used above:
_image_container.rhtml
<li id="item_<%= @asset.id %>" class="float"> <%= render :partial => "image_thumb", :locals => { :asset => @asset } %></li>_image_thumb.rhtml
<%= image_tag @asset.thumbnail, :border => 2 %> <br> <%= link_to_remote(image_tag("arrow_rotate_anticlockwise.png", :border => 0), :url => {:action => "rotate", :id => @asset.id, :direction => "left"} ) %> <%= link_to_remote(image_tag("cross.png", :border => 0), :url => {:action => "remove", :id => @asset.id} ) %> <%= link_to_remote(image_tag("arrow_rotate_clockwise.png", :border => 0), :url => {:action => "rotate", :id => @asset.id, :direction => "right"} ) %>_upload_form.rhtml
<%= form_remote_tag(:url => { :controller => "image_demo", :action => "create" }, :html => {:multipart => true}) %> <b>Picture:</b> <%= file_field_tag "asset" %> <%= submit_tag "Upload" %> <%= end_form_tag %>Our RJS to handle the create, remove, and rotate.
create.rjs
if @asset.new_record? page.alert "There was a problem uploading your file:\n" + @asset.errors.full_messages.join("\n")else page.insert_html :top, 'sortable_list', :partial => 'image_container', :locals => { :asset => @asset } page.visual_effect :highlight, "item_#{@asset.id}" page.sortable "sortable_list", :constraint => false, :url => { :action => :update_positions }endremove.rjs
page.remove "item_#{@asset_id}"page.sortable "sortable_list", :constraint => false, :url => { :action => :update_positions }rotate.rjs
page.replace_html "item_#{@asset.id}", :partial => 'image_thumb', :locals => { :asset => @asset }page.visual_effect :highlight, "item_#{@asset.id}"page.sortable "sortable_list", :constraint => false, :url => { :action => :update_positions }Our controller:
class ImageDemoController < ApplicationController layout 'image_demo' def index session[:uid] = Time.now.to_i unless session[:uid] @assets = Asset.find(:all, :conditions => ["user_id = ?", session[:uid].to_i], :order => "position") end def create @asset = Asset.new() @asset.uploaded_file = params['asset'] @asset.position = 0 @asset.user_id = session[:uid].to_i @asset.save responds_to_parent do render :action => 'create.rjs' end return end def list @assets = Asset.find(:all, :conditions => ["user_id = ?", session[:uid].to_i], :order => "position") end def update_positions params[:sortable_list].each_with_index do |id, position| Asset.update(id, :position => position) end render :nothing => true end def rotate @asset = Asset.find(params[:id]) degrees = params[:direction] == "left" ? -90 : 90 @asset.rotate(degrees) end def remove @asset_id = params[:id] Asset.delete(@asset_id) endend
Our asset model:
require 'RMagick'class Asset < ActiveRecord::Base def uploaded_file=(incoming_file) content_type = incoming_file.content_type.chomp if content_type.rindex(/image\/[(jpe?g)||(gif)]/) self.name = base_part_of(incoming_file.original_filename) base_dir = "/some/path/you/like" # save original file self.original = "image_demo_assets/o_#{Time.now.utc.to_i}#{rand(1000000)}."+self.name File.open(base_dir+self.original,File::CREAT|File::TRUNC|File::WRONLY,0666){ |f| f.write(incoming_file.read) } self.resized = "image_demo_assets/r_#{Time.now.utc.to_i}#{rand(1000000)}."+self.name resized = Magick::Image.read(base_dir+self.original).first resized.change_geometry!('500x500') { |cols, rows, img| img.resize!(cols, rows) } resized.write(base_dir+self.resized) self.thumbnail = "image_demo_assets/t_#{Time.now.utc.to_i}#{rand(1000000)}."+self.name thumb = Magick::Image.read(base_dir+self.original).first thumb.change_geometry!('100x100') { |cols, rows, img| img.resize!(cols, rows) } thumb.write(base_dir+self.thumbnail) self.save end end def rotate(degrees) base_dir = "/some/path/you/like" #main photo image = Magick::ImageList.new(base_dir+self.original) image = image.rotate(degrees) image.write(base_dir+self.original) # resized resized = Magick::ImageList.new(base_dir+self.resized) resized = resized.rotate(degrees) resized.write(base_dir+self.resized) # thumb thumb = Magick::ImageList.new(base_dir+self.thumbnail) thumb = thumb.rotate(degrees) thumb.write(base_dir+self.thumbnail) end private def base_part_of(file_name) name = File.basename(file_name) name.gsub(/[^W._-]/, '') sanitize_filename(name) end # Fixes a 'feature' of IE where it passes the entire path instead of just the filename def sanitize_filename(value) #get only the filename (not the whole path) just_filename = value.gsub(/^.*(\\|\/)/, '') just_filename.gsub(/[^\w\.\-]/,'_') endendSome suggestions:
- Use form_for and get rid of some ugliness in the controller by using Asset.new(params[:asset]) instead of setting each value individually.
- Use simply_helpful for generiting your DOM id’s.
- Use acts_as_attachment for handing the storing of files.
- Better validations (aaa will handle that too).
- Rewrite the whole thing.
Again, this is a VERY quick-and-dirty demo written in about 20 minutes with so much room for improvement. If I had the time I would, but alas I hope it helps.
Ruby on Rails, Ajax & CSS Star Rating System
I’m sure everyone by now has seen those oh-so-Web 2.0 star rating features on hundreds of websites. Well I needed to implement one for a site I’m working on and I couldn’t find a complete example anywhere (not in RoR). So here it is. A complete Rails based Ajax and CSS star ratings sytem with some RJS thrown in for good measure.
I used Rogie’s very elegant CSS only star rating system found here CSS Star Rating Part Deux. I also used Chris Ingrassia’s acts_as_rateable plugin.
So here we go.
Get the CSS and change the image url’s
First figure out which version of the CSS ratings you like. I used this example.
/* styles for the star rater */ .star-rating{ list-style:none; margin: 0px; padding:0px; width: 150px; height: 30px; position: relative; background: url(/images/star_rating.gif) top left repeat-x; } .star-rating li{ padding:0px; margin:0px; /*\*/ float: left; /* */ } .star-rating li a{ display:block; width:30px; height: 30px; text-decoration: none; text-indent: -9000px; z-index: 20; position: absolute; padding: 0px; } .star-rating li a:hover{ background: url(/images/star_rating.gif) left center; z-index: 2; left: 0px; border:none; } .star-rating a.one-star{ left: 0px; } .star-rating a.one-star:hover{ width:30px; } .star-rating a.two-stars{ left:30px; } .star-rating a.two-stars:hover{ width: 60px; } .star-rating a.three-stars{ left: 60px; } .star-rating a.three-stars:hover{ width: 90px; } .star-rating a.four-stars{ left: 90px; } .star-rating a.four-stars:hover{ width: 120px; } .star-rating a.five-stars{ left: 120px; } .star-rating a.five-stars:hover{ width: 150px; } .star-rating li.current-rating{ background: url(/images/star_rating.gif) left bottom; position: absolute; height: 30px; display: block; text-indent: -9000px; z-index: 1; }Make sure you change your image url’s so that your Rails app can find them.
Get the images for your CSS
Grab the images used in your CSS and put them in your images directory. Here are both
.
Install the acts_as_rateable plugin.
Run the following from the root of your Rails app to install the plugin.
script/plugin install http://juixe.com/svn/acts_as_rateable
Create the tables used by acts_as_rateable
Create a file db/migrate/xxx_create_ratings.rb (xxx is 001 if it’s the first migration file you have).
class CreateRatings< ActiveRecord::Migration def self.up create_table :ratings, :force => true do |t| t.column :rating, :integer, :default => 0 t.column :created_at, :datetime, :null => false t.column :rateable_type, :string, :limit => 15, :default => "", :null => false t.column :rateable_id, :integer, :default => 0, :null => false t.column :user_id, :integer, :default => 0, :null => false end add_index :ratings, ["user_id"], :name => "fk_ratings_user" end def self.down drop_table :ratings endend
Run your migration.
rake migrate
You should now have the appropriate tables.
Make one of your models rateable
I was trying to add a rating system for the model Asset. Yours can obviously be whatever you like but from here on out I’ll be using Asset. So add acts_as_rateable to your model.
class Asset < ActiveRecord::Base acts_as_rateable ...end
Create a controller to handle the rating submissions
Create the file /controllers/rating_controller.rb
class RatingController < ApplicationController def rate @asset = Asset.find(params[:id]) Rating.delete_all(["rateable_type = 'Asset' AND rateable_id = ? AND user_id = ?", @asset.id, current_user.id]) @asset.add_rating Rating.new(:rating => params[:rating], :user_id => current_user.id) endend
Two things to note here. First I’m associating ratings to users. I’ve already implemented a user/permission system for my site using the model User. Use whatever is appropriate for you. You can modify this whole example to work without associating ratings to users, the acts_as_rateable plugin will handle it just fine. However, I’m not going to get into that here.
Since I am associating ratings to users it would be bad to have a user skew the results by storing multiple ratings for a single Asset. Hence the delete. I’m telling it to delete all ratings for the rateable_type ‘Asset’ and the id (rateable_id) of the Asset. The rateable_type of Asset is handled by the plugin and stored in the ratings table.
Create your views
Create the partial /views/rating/_rating.rhtml
<%= number_with_precision(asset.rating, 1) %>/5 Stars<br><ul class='star-rating'> <li class='current-rating' style='width:<%= (asset.rating * 30).to_i -%>px;'> Currently <%= number_with_precision(asset.rating, 1) %>/5 Stars. </li> <li> <%= link_to_remote( "1", {:url => { :controller => "rating_demo", :action => "rate", :id => asset.id, :rating => 1}}, :class => 'one-star', :name => '1 star out of 5') %> </li> <li> <%= link_to_remote( "2", {:url => { :controller => "rating_demo", :action => "rate", :id => asset.id, :rating => 2}}, :class => 'two-stars', :name => '2 stars out of 5') %> </li> <li> <%= link_to_remote( "3", {:url => { :controller => "rating_demo", :action => "rate", :id => asset.id, :rating => 3}}, :class => 'three-stars', :name => '3 stars out of 5') %> </li> <li> <%= link_to_remote( "4", {:url => { :controller => "rating_demo", :action => "rate", :id => asset.id, :rating => 4}}, :class => 'four-stars', :name => '4 stars out of 5') %> </li> <li> <%= link_to_remote( "5", {:url => { :controller => "rating_demo", :action => "rate", :id => asset.id, :rating => 5}}, :class => 'five-stars', :name => '5 stars out of 5') %> </li></ul>Obviously it’s using Ajax with the prototype helper link_to_remote to submit the user’s rating. One thing to note. Where you see width:<= (asset.rating * 30).to_i ->px;’ you’ll have to modify this to correspond with the images you chose to use. The one I’m using has images which are 30px wide. If you chose the smaller star images then you’ll have to modify this calculation to correspond to your image width. By the way, this is the line that handles the display of the current rating.
And now a little RJS
Create the file /views/rating/rate.rjs
page.replace_html "star-ratings-block", :partial => 'rating/rating', :locals => { :asset => @asset }This will replace the star ratings with the partial we created previously in order to reflect any rating changes made by the submission.
And finally put it on your page
Render the partial in one of your views.
<div id="star-ratings-block"> <%= render :partial => "rating/rating", :locals => { :asset => @asset } %></div>This needs @asset (or whatver you’re going to be using) in order to function.
Done
Now wasn’t that easy? Gotta love rails. 10 minutes of coding and you have a complete Ajax and CSS star rating system just like the pros use. Here’s a demo.
I could very well have skipped something so let me know if you have any problems.
