RoR SubList Plugin
Posted by Ian J Cottee Fri, 30 Jun 2006 18:32:00 GMT
Rails should make things easier but sometimes it doesn’t seem that way. As an example consider a parent/child relationship – a sales order with sales order lines, a purchase order with purchase order lines, a dispatch record with dispatch lines etc etc. You need a design to handle this type of thing easily.
I came across the Ruby on Rails SubList Plugin which does a good job of removing the pain. Big thanks must go to the author Luke Galea for a very useful plugin. The docs are fine but it’s always good to have a ‘real’ example of how it works. I was working on a credit note screen - that has a credit note and credit note lines so I’ll describe how I fitted it together.Before you read this however you should read the notes that come with the plugin.
If you look at the details I’ve posted it might seem rather involved but I’ve tried to be reasonably thorough to answer any questions that pop up as you use the plugin.Just to reassure you that it’s actually simple here’s a quick run down of what you need to do (assuming you have the plugin already).
- Amend your controller – adding a few lines of magic code in four places.
- Create a partial for your line view
- Amend the parent form to use the partial
That is it! Now let’s go into the details.
First of all you’ll need the plug in. Download the tarball from the link above and move the extracted folder ‘sub_list’ into the vendor/plugins directory of your project and restart your application.
I’m assuming you’ve generated the basics for your parent model already (using normal scaffolding for example). So you should have the basic views and controllers created. Now you need to do a little bit of work for your specific project. Basically you are going to change the controller for your parent model and add/amend two views – the parent view (usually the partial _form) and a view for the lines. That’s basically about it.
The changes in the controller start with you adding these two lines within the controller class shown here within my credit note controller.
class CreditNoteController < ApplicationController
include UIEnhancements::SubList
helper :SubList
... class continues ...
that does the magic bit and you don’t have to think about it. Next
you’ll need to add the following code into your controller class.
sub_list 'CreditNoteLine', 'credit_note' do |new_credit_note|
end
CreditNoteLine is the class name of your child model. credit_note is the
name of the instance variable of your parent class. We’ll see later
where that is used. new_credit_note is a block parameter (you can call
it what you like) and allows you to do some initialisation via a
callback when the user adds a new line. In my case I wanted to set a
price and quantity field to be 0 initially. I accomplished this via
sub_list 'CreditNoteLine', 'credit_note' do |new_credit_note|
new_credit_note.qty = 0
new_credit_note.price_in_cents = 0
end
Let’s do some work on the views now, although we’ll need to return to
the controllers a little later. As I mentioned there are two views to be
amended. Your main form and the partial that will be used to render the
credit note lines. Let’s take the main form first. You basically want to
add a link to create a new line and a link that displays the list of
lines you are editing. Both of these are simple. The following will give
you a link to add a new credit note line.
<%= sub_list_add_link 'CreditNoteLine', 'Add Line' %>
The sub_list_add_link takes a model name (The name of the model you are
adding) and a label for the link. In this case we’ve just used some text
but you could pass an image in there as well using image_tag. e.g.
<%= sub_list_add_link 'CreditNoteLine', image_tag('my_add_image.png') %>
In that case you’ll need to make sure that the image is in your
public/images directory. Now let’s create our partial which will display
the lines themselves. Make this partial the table name of your model. So
the model is called CreditNoteLine, the table name is
credit_note_line, Our partial is called _credit_note_line.rhtml (don’t forget
the usual leading underscore for partial names).
Within the partial things are pretty straight forward. credit_note_line
holds the instance of our particular line and we need to assign that to
an instance variable so that we can use it within our form
helpers. Therefore our first line does that assignment:
<% @credit_note_line = credit_note_line %>
I’ll post the entire partial at the end of this but let’s just take a
look at a small subset
<div id="<%= "credit_note_line_#{credit_note_line.id}" %>">
<%= text_field 'credit_note_line[]', 'qty', :size => 10 %>
That div should enclose everything within the partial as otherwise the
ajax routines won’t be able to identify what div’s to remove should
a line be deleted. So for a credit_note_line with an id of 887 that dive
would look like:
<div id="credit_note_line_887">
Looking at that you might be wondering what that div
id would be if it’s a new line (and the line record doesn’t have an id
assigned). The answer is that newly added lines are assigned a temporary
id (it’s actually the integer representation of the current
time). Currently Time.now.to_i gives me 1151686675 so the id’s are
unlikely to be overlapping with your real ids. This temporary id is
exchanged for a real id if the data is written to the database.
With our partial wrapped around with that div we can now work on putting
our fields on the partial. Here’s an example of one of the fields on my
form.
<label for="credit_note_line[]_qty">
Qty
<%= text_field 'credit_note_line[]', 'qty', :size => 10 %>
There isn’t that much to say about this. the ‘[]’ at the end of instance
variable name means we’ll get these lines back as a collection within
our params. You can use any of the normal form fields of course. Here’s
a dropdown that I am using so that the user can select an item
from their parts database.
<%= select 'credit_note_line[]','item_id',
Item.find(:all,
:order=>'description').collect { | i | [i.code, i.id] },
{:include_blank => true } %>
Same meat, different gravy. To finish the partial off you’ll need to
include a link for each line to allow that line to be deleted. That’s
accomplished using the sub_list_remove_link tag. It looks like this:
<%= sub_list_remove_link credit_note_line, 'CreditNoteLine', 'Del' %>
Same parameters as the sub_list_add_link tag- a model name and a label for
the link. We should be ready now to display our lines within our credit
note form. That’s done with the sub_list_content tag – it’s as simple as this:
<%= sub_list_content 'CreditNoteLine', 'credit_note' %>
The first parameter is the name of the model for the child records we
are displaying, the second parameter is the name of the instance
variable which holds the parent record.
You should now be able to create or display a record (assuming you have the controller work done for them) and your child lines will be displayed automatically. Not only that you can add and delete those lines as well. What you can’t do is save your record and associated lines yet. That’s the last bit of work we need to accomplish.
I am going to assume here that you created your controllers via the Rails 1.1. scaffolding so you should have two methods in there called ‘update’ and ‘create’. I’ve used the examples that Luke gives in his docs as they work well. Here’s the create method
def create
@credit_note = CreditNote.new(@params[:credit_note])
success = true
success &&= initialize_credit_note_lines
success &&= @credit_note.save
if success
flash[:notice] = 'Credit note successfully created.'
redirect_to :action => 'list'
else
prepare_credit_note_lines
render_action 'new'
end
end
That code should be pretty self explanatory. However it does have two
‘magic’ method calls.
- initialize_credit_note_lines
- prepare_credit_note_lines
You don’t need to know what those two calls do. You just need to know that you must call the initialize_ routine (with the name of your collection of records from the parent record, in our case credit_note_lines) just before you save your parent record. And if you try and save your record and it fails you’ll need to call the prepare_ routine before redisplaying your form to correct whatever problems are thrown up.
As you may have guessed, the initialize_ method tries to save your child records. I think the prepare_ method is used to put the temporary ids back into the unsaved child records before redisplaying but as I mentioned before you don’t need to know this.
The same holds true for the other controller method you need to amend. Here we go with the update method:
def update
@credit_note = CreditNote.find(params[:id])
@credit_note.update_attributes(@params[:credit_note])
success = true
success &&= initialize_credit_note_lines
success &&= @credit_note.save
if success
flash[:notice] = 'Credit note successfully updated.'
redirect_to :action => 'list'
else
prepare_credit_note_lines
render_action 'edit'
end
end
That’s it. Now go and buy that Luke a beer!
p.s. Here’s the complete partial used for rendering a line
<% @credit_note_line = credit_note_line %>
<div id="<%= "credit_note_line_#{credit_note_line.id}" %>">
<fieldset>
<label class="first" for="credit_note_line[]_item_id">
Item
<%= select 'credit_note_line[]','item_id',
Item.find(:all, :order=>'description').collect { | i | [i.code, i.id] },
{:include_blank => true } %>
</label>
<label for="credit_note_line[]_qty">
Qty
<%= text_field 'credit_note_line[]', 'qty', :size => 10 %>
</label>
<label for="credit_note_line[]_price_in_cents">
Price
<%= text_field 'credit_note_line[]', 'price_in_cents', :size => 10 %>
</label>
<%= sub_list_remove_link credit_note_line, 'CreditNoteLine', 'Del' %>
</fieldset>
</div>

Hello Ian,
It was great getting to meet you at railsconf. Even if I did give you Macbook envy :-)
Send me an email at: jpinnix at pixelgrazer dot com
Thanks for this very helpful walk-through. I had run across the plugin but was having issues implementing it (very dumb mistakes on my part) in my first Rails project.