fallenrogue.com

Zebra Stripes: A look at acts_as_list, ajax sorting and the cycle method in Rails

Between the 3rd slice of Cherry Pie (thanks Mom-in-Law!) and all the left-over turkey a few debates arose over the extended weekend. Some of my asp.net friends were going over some of the things that they love about .net and needless to say, being the Rails fanatic that I am… well, I couldn’t help myself from offering examples of how to do similar things in Rails. Of course, my Rails ways used much less code and were much more flexible. As we were going through some of the examples, I realized that this might make a pretty decent article. The battleground was over zebra stripes on HTML tables. But I couldn’t just cover that. That’s one method!

So, instead we’re going to toss in some other goodies! We’re going to cover “Acts_as_List”, an Ajax sort method for that list complete with Drag-and-Drop, the cycle method and some dynamic features to help with the testing. You can get all of the materials in the sample app here. The name of this app is Zebra. The technique of alternating colors on a table is adding “zebra stripes.” Since that was the inspiration for the original topic I’ve decided to take it too far by making zebra models with lists of stripes!

I’d like to take a minute to say that I’ve just scratched the surface of what’s possible with testing in Rails. This is on purpose. I plan on doing an entire series about Integration testing and I promise you, once you see it in action… you’ll wonder how you ever lived without it. But I don’t want to scare you guys off with testing talk just yet! We’re here to learn some quick, applicable techniques for producing some kick ass stuff with Rails, right? Let’s get to it.

So, you’ve downloaded the project right? No? Fine, you want to type. I respect that. Let’s create the project and models.

rails zebra -d sqlite3

script/generate model zebra
script/generate model stripe

script/generate controller stripes index view sort

Now let’s do some coding. First the migrations. (you could start elsewhere but I’m starting here.)
class CreateZebras < ActiveRecord::Migration
  def self.up
    create_table :zebras do |t|
      t.column :name, :string
    end
  end

  def self.down
    drop_table :zebras
  end
end

class CreateStripes < ActiveRecord::Migration
  def self.up
    create_table :stripes do |t|
      t.column :color, :string
      t.column :zebra_id, :integer
      t.column :position, :integer
    end
  end

  def self.down
    drop_table :stripes
  end
end

please note the position column in the stripes table. That’s what is going to tell us what order our list is in. More on that later, let’s do the models
class Zebra < ActiveRecord::Base
  has_many :stripes, :order => :position
  validates_presence_of :name
end

class Stripe < ActiveRecord::Base
  belongs_to :zebra
  acts_as_list :scope => :zebra_id
  validates_presence_of :zebra_id, :color
  validates_format_of :color, :with=> /(black|white)/, :message=>"stripes are either black or white" 
end

Ok, so our first line in the Zebra model is harmless enough, it’s a has_many relationship that orders based on the position property of stripes. This is important if you’re creating a list out of child elements. If you don’t order them by the position then… you may not know they’ve been re-ordered! Next, you’ll notice in our Stripe model the line acts_as_list :scope=>:zebra_id well, it doesn’t take a rocket scientist to tell you what this sucker is for, right? This single line gives us access to a series of new methods for our stripes that are too numerous to count here! Don’t worry they can all be found here. Pretty cool stuff. But read that later, we’ve got an app to write!

The rest is just some fun validation code. I don’t think I need to explain it so if you have any questions on the validation just leave one in the comments and we’ll discuss. Let’s get the migration going!
rake db:migrate

OK, now let’s get to some fun stuff. A little scriptaculous here, a pinch of cycle there and sprinkled with a touch of RJS. First, rename the generated view for sort.rhtml to sort.rjs. Good. Now, let’s create our controller. I named mine stripes. In retrospect, zebras would have been a better name. But, alas, I’m too tired to refactor (too much turkey!) so this demo and sample will be stripes for the controller.
class StripesController < ApplicationController
  include ActionView::Helpers::TextHelper

  def index
    @zebras = Zebra.find(:all)
  end

  def view
    @zebra = Zebra.find(params[:id])
  end

  def create_new
    # call this with a url like: 
    # "http://localhost:3000/stripes/create_new/?name=Bill" 
    # you can use this to quickly create a model to play with!
    @zebra = Zebra.create(:name=>params[:name])
    1.upto(4) do |number|
      @zebra.stripes.create(:color=>cycle("black","white"))
    end
    redirect_to :action=>"view", :id=>@zebra.id
  end

  def sort 
    @zebra = Zebra.find(params[:id]) 
    @zebra.stripes.each do |stripe| 
      # we must add one to compensate for the zero based index.
      stripe.position = params['zebra_stripes'].index(stripe.id.to_s) + 1 
      stripe.save 
    end 
  end 

end

index and view are pretty vanilla. Look for the zebra or zebras and send them out for processing. The create_new method is just something I tossed in for you all. You probably wouldn’t need that method at all, but instead of manually creating your instances in the console or migrations or where ever you tend to do that (scaffold :stripe perhaps?) I’ve provided a method so that we could all be on the same page.

Finally, the sort method. This method is the Ajax method that will be called by our Ajax sortable list. The RJS file that you created will be used to let the user know that the change has been made. Now, on a personal level, I must say that this is not the most efficient way to process the list. This is just to illustrate the steps that are involved. I encourage you to find faster ways, especially when Ajax is involved! This way you can see exactly how the list is serialized by scriptaculous and sent back to Rails for processing. More on that after we see the views. So, let’s do those…

Oh before I forget, you’ll want to create a new view in the layouts directory called application.rhtml to hold all the defaults for your views. I still don’t know why this isn’t provided by default.
-----------application.rhtml-----------
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

    <title>Zebra Stripes demo by fallenrogue.com!</title>
    <%= javascript_include_tag :defaults %> 
    <style type="text/css">
        .zebra_stripe{
            border:1px solid #ccc;
            margin:2px;
            padding:2px;
        }
        .a{
            background:#f3f3f3;
        }
        .b{
            background:#FFF;
        }
    </style>
</head>

<body>
    <%=yield%>

</body>
</html>

-----------index.rhtml-----------
<h1>My Zebras!</h1>
<% @zebras.each do |zebra| %>
    <%= link_to zebra.name, :action=>"view",:id=>zebra.id %><br>
<% end %>

-----------view.rhtml-----------
<h1>Zebra: <%=@zebra.name%> </h1>
<ul id="zebra_stripes">
    <% @zebra.stripes.each do |stripe| %>
    <li id="stripe_<%=stripe.id%>" class="zebra_stripe <%= cycle 'a','b' %>">
        <%= stripe.color %> stripe with id:<%=stripe.id.to_s%>
    </li>
    <% end %>
</ul>
<%= sortable_element 'zebra_stripes', :url => { :action => "sort", :id => @zebra } %>

-----------sort.rjs--------------
page.visual_effect "highlight", "zebra_stripes" 

Now this is the fun spot. This is where I won my argument (in my mind, at least). The application, index and sort views are pretty simple and if you have questions just let me know. For now I want to look at view. view.rthml is where you might see a few new things. first is cycle ‘a’, ‘b’

The cycle method is a text helper that will “cycle” through a list of options and spit out the next value in your sequence. This is why we called this project zebra. Most people call this technique adding “zebra stripes” and it usually involves some crazy spaghetti implementation (it’s not always, people so I don’t want to start the flame war, ok!) but in Rails in the above View example, a second class will be applied of either “a” or “b”. Yes, yes, I know it should be a better name. I’ll leave that to you! I’m just trying to clearly illustrate its use.

Finally, let’s look at sortable_element. What do we have here? Well, friends, we have a single line of code that is going to provide an Ajax, drag and drop, sortable list. Yup. That’s it. The first argument is to tell scriptaculous which element is going to get serialized for transport. the second is the url to the controller method. It’s simple syntax and if you run what you’ve got right now, you’ll see that you’ve done it. You’ve created a complex drag and drop list with zebra stripes with very little code…but without losing very much control.

This article is getting pretty long, so I’m going to wrap it up and leave some room open for comments and emails. but I’m going to leave you with this. A dynamic fixture for Unit Testing. You’ve all seen the YAML fixtures before, I know that because we’ve done a few on this site. But, what if you’re testing a series of items like the stripes. You don’t want to manually create 50 of them, right? No way. That’s not DRY at all. try this on for size:
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
#Polly Stripes
<% include ActionView::Helpers::TextHelper %>
<% 1.upto(50) do |x| %>
Polly_stripe_<%= x %>:
   id: <%= x %> 
   zebra_id: 1
   color: "<%= cycle "black", "white" %>" 
   position: <%= x %>
<% end %>
<% 1.upto(50) do |y| %>
Frank_stripe_<%= y %>:
   id: <%= y + 50%> 
   zebra_id: 2
   color: "<%= cycle "black", "white" %>" 
   position: <%= y %>
<% end %>

That’s right! We’re bringing our friend the cycle back here! YAML is just like the views in that you can place code blocks right inside them and they will be executed at run time! So, instead of just 2 fixtures that I’ve listed I end up with 100!!! That should make for some fantastic Unit tests! I’ve included a few so that you can run them too.
rake db:test:prepare
rake test

Ok, we have painted zebra stripes. We make the stripes into a list. We sorted that list with some fancy Ajax and tested our models with dynamic fixtures. We did it in no time flat. As always you can download the app here. If you have any questions or comments or just want to say hi either drop a comment below or use the contact form to send me an email. I hope you’ve enjoyed this one, stay tuned cause we’ve got lots more ground to cover… especially with 1.2 on the way!


articleStats

Here are some silly little facts about this Zebra Stripes: A look at acts_as_list, ajax sorting and the cycle method in Rails...

It was written by about 1 year ago.
It has 12400 letters in it.
It has 1738 words in it.
It has a total of 6 comments in all.
So far fallenrogue has the last word!

article Links

These are the links that appear in this article. They probably don't make sense out of context... but just in case. :)

sample app here.
they can all be found here.
here.
 

The other stuff...

What the kids are saying...

about 1 year ago Don said...

OK, baby ruby/rails programmer here: Don't we need to define a zebra_id column in the zebras table? Or does that happen automagically?

about 1 year ago fallenrogue said...

No problem, Don, noobs are totally welcome! By using migrations, Rails automagically provides an id column for the table so you don't have to include it. Then to create the associations defined by the model you use a column in the related table that is "table_name_singular" plus "_id". So, in this example you have a table "zebras" with (an automatically created) column "id". To reference this column we add "zebra_id" to the stripes table. I hope that answers your question and enjoy using those fantastic migrations!

about 1 year ago Jonas said...

Great mini tutorial! It fits perfectly in time for me as I added acts_as_list to one of my models another night, but did not have the time to try it out. I've read many of your mini tutorials and I must say they are very good! Thanks.

about 1 year ago fallenrogue said...

Thanks Jonas! Let me know if you've got any special requests for future tutorials. :)

7 months ago Josh said...

Is it possible to use cycle in the controller?

7 months ago fallenrogue said...

Sure! You just need a reference to the class that includes cycle which is ActionView::Helpers::TextHelper. So, right after your class declaration for the Controller add this...

class YourController < ApplicationController
include ActionView::Helpers::TextHelper
end

And you'll have access to it! I hope that helps, enjoy!

Leave a comment
*name:
*email: (never sold or published.)
url :

©2000-2008 fallenrogue.com | Some Rights reserved.