POSTS

[Rails] Cleaning up HABTM views (2/2)

Blog

The reality is that most people use Rails for web frameworks … the console manipulation stuff we explored in the previous post doesn’t really help us much. We need a web interface. While using the script/generate gave us some scaffolds that we can play with, it’s not really “integrated” and it doesn’t quite demonstrate the “has many”-ness.

First we’ll fire up the web server (script/server) and see what the ’new’ view looks like:

Hm, that’s certainly not giving us any place to enter some flavorful information. Let’s fix that in the view.

	<p>
		<b>Flavors ( comma-delimited )</b><br/>
 	  <%= f.text_field :flavorstring %>
	</p>

Try reloading the view…

And we get…an error:

	Undefined method `flavorstring' for #<Candy id: nil, name: nil, created_at: nil, updated_at: nil>

What I’m effectively doing here is making us need to call a “virtual attribute”. You see, what goes in here is the CGI code to generate a text field such that we can put in a comma-delimited list of flavors. On the flip side, in other views, we’ll want to have ‘flavorstring’ give us a comma-delimited listing of the associated flavors.

So, let’s add the virtual attribute into the app/models/candy.rb file:

  def flavorstring
     if (self.flavors.length > 0)
       x=String.new()
       self.flavors.uniq.each do |f|
         x += f.name
         x += ", "
       end
       return x.sub(/,\s+$/,'')
     else
       return ""
     end

   end

   def flavorstring=(input)
       if input.empty?
         self.flavors.destroy_all
       end

       self.flavors.destroy_all
       input.gsub(/\s+/, '').split(',').uniq.each do |flav|
         if Flavor.find_by_name(flav).nil?
           self.flavors << Flavor.create(:name=> flav)
         else
           self.flavors << Flavor.find_by_name(flav)
         end
       end
   end

It’s also handy to define the to_s method to return the “name” attribute of this model ( this should also be added to the Flavor model). It means that if anyone executes something like puts aCandy or puts aFlavor they get the name of that item, versus something like Flavor:124124.

Reload the view and create some candies with your own tags…

If you want to edit the flavors, you can do so by taking advantage of the scaffolds we created earlier:

/flavors

Because I’m sorta anal, i’m going to alphabetically sort my scaffolds. I go to the ‘candies_controller’ or ‘flavors_controller’ and edit the ‘index’ method to make my find command look like:

@candies = Candy.find(:all, :order=>'name asc')

Hey, looking at that index, it’s notably not showing our flavor data. Let’s fix that.

Now, I know I can get a given Candy’s flavors by issuing aCandy.flavors which will come back as an array. So what I would like to do is find a way to pass an array to a loop, have it assemble some sensible HTML, and then return it to me. This will keep my view neat.

But I also know that, especially in a HABTM relationship, if I want something to work for one side, I’ll probably want it to work for the other side. That is, if I want to get a comma-delimited list of flavors associated with a candy which are links to that flavor ( which should also show the candies associated with it) – I’ll also probably want the reverse, from the perspective of the flavors.

{Yeah, you may want to re-read that a few times }

So I’m going to write a “helper”. I’m not going to put this in the candies_helper or in the flavors_helper.rb. Because of the reflexivity ( Flavor <-> Candy ), I want to put this method into the application_helper.rb and make it sufficiently abstract so that it could be called either by a view associated with a Candy or with a Flavor.

  def link_to_comma_sep_elements(hash)
    html = ''
    hash[:ray].each do |x|
      puts x.class
      html << link_to(x,:controller=>hash[:controller],:action=>hash[:action], :id=>x.class.find(x.id))
      html << ", "
    end
    return html.sub(/,\s+$/, '')
  end

This means it can be called like so:

	<%= link_to_comma_sep_elements(:ray=>candy.flavors, :controller=>"flavors", :action=>"show") %>

I pass an array to the function, a controller(“flavors”) that the comma-ized elements will want to point to and the action they should point to (“show”).

But if i click on one of those flavors, I’m taken to a view of the flavor but without associated candy information. Simple. Add to app/view/flavors/show.html.erb:

	<p>
		<b>Associated Candies: </b>
	  <%= link_to_comma_sep_elements(:ray=>@flavor.candies, :controller=>"candies", :action=>"show") %>
	</p>

See! Our helper is already saving is work! Cool eh?

Now say we approach from the /flavors scaffold, what if we want to have a comma-delimited list of associated candies? You guessed it, our helper to the rescue ( again! )

    	<table>
    	  <tr>
    	    <th>Name</th>
    			<th>Candies</th>
    	  </tr>

    	<% for flavor in @flavors %>
    	  <tr>
    	    <td><%=h flavor.name %></td>
    			<td>
    			  <%= link_to_comma_sep_elements(:ray=>flavor.candies, :controller=>"candies", :action=>"show") %>
    			</td>	
    	    <td><%= link_to 'Show', flavor %></td>
    	    <td><%= link_to 'Edit', edit_flavor_path(flavor) %></td>
    	    <td><%= link_to 'Destroy', flavor, :confirm => 'Are you sure?', :method => :delete %></td>
    	  </tr>
    	<% end %>
    	</table>	

Now at this point you have a pretty functional setup. To make playing with and exploring your HABTM relationship a bit easier, I added links at the bottom of each model’s index view so that you could flip between the two.

Flavor’s index: <%= link_to 'See Candies', "candies", :action=>'index' %>

Candy’s index: <%= link_to 'See Flavors', "flavors", :action=>'index' %>