Adding Dynamic Select Lists to Resource Scaffolding in Rails

I am starting my first non-trivial Rails application (finally!), and I quickly encountered the common situation in which you want to assign hierarchical subject categories to an entity. I also wanted to do this within the context of using RESTful controllers in Rails. Designing RESTful controllers is a relatively new concept to me, and its a relatively new feature in Rails itself. All in all, implementing this was remarkably fast and smooth, true to Rails’ reputation, although there were a few minor things along the way that weren’t immediately obvious to me. So, because this is probably such a common scenario, I decided to recreate the steps I took to implement this for others like me who are very new to Rails.

Here is the data model I have in mind, using my poor man’s crow’s feet notation:

______ | | | ^ [categories] –< [categories_items] >– [items]

Obviously a given category can be assigned to many items, and likewise, I want to allow any given item to be assigned more than one category. Categories are also related to each other in a parent-child hierarchy. So, a category can have multiple sub-categories.

Let’s begin by creating a fresh Rails project from scratch, imaginatively called ‘test’. From the command line:

rails test

All the hard work of implementing hierarchical categories in this application will be done for us by the acts_as_tree plugin. Again, from the command line, change directories to the test application and install the plugin:

script/plugin install acts_as_tree + ./acts_as_tree/README + ./acts_as_tree/Rakefile + ./acts_as_tree/init.rb + ./acts_as_tree/lib/active_record/acts/tree.rb + ./acts_as_tree/test/abstract_unit.rb + ./acts_as_tree/test/acts_as_tree_test.rb + ./acts_as_tree/test/database.yml + ./acts_as_tree/test/fixtures/mixin.rb + ./acts_as_tree/test/fixtures/mixins.yml + ./acts_as_tree/test/schema.rb

You will find the plugin installed under vendor/plugins.

Lets begin our part in earnest by creating two resources, starting with the categories resource itself, by executing the scaffold_resource generator, which takes the name of the resource (typed lower case, spelled single) along with fields and their types:

script/generate scaffold_resource category name:string parent_id:integer exists app/models/ exists app/controllers/ exists app/helpers/ create app/views/categories exists test/functional/ exists test/unit/ create app/views/categories/index.rhtml create app/views/categories/show.rhtml create app/views/categories/new.rhtml create app/views/categories/edit.rhtml create app/views/layouts/categories.rhtml create public/stylesheets/scaffold.css create app/models/category.rb create app/controllers/categories_controller.rb create test/functional/categories_controller_test.rb create app/helpers/categories_helper.rb create test/unit/category_test.rb create test/fixtures/categories.yml create db/migrate create db/migrate/001_create_categories.rb route map.resources :categories

The parent_id field is so named by convention, and will enable the acts_as_tree plugin to magically manage the hierachy of the categories table.

Then, lets similarly create the resource that will have categories assigned to it:

script/generate scaffold_resource item title:string exists app/models/ exists app/controllers/ exists app/helpers/ create app/views/items exists test/functional/ exists test/unit/ create app/views/items/index.rhtml create app/views/items/show.rhtml create app/views/items/new.rhtml create app/views/items/edit.rhtml create app/views/layouts/items.rhtml identical public/stylesheets/scaffold.css create app/models/item.rb create app/controllers/items_controller.rb create test/functional/items_controller_test.rb create app/helpers/items_helper.rb create test/unit/item_test.rb create test/fixtures/items.yml exists db/migrate create db/migrate/002_create_items.rb route map.resources :items

The thing that really blows me away about the scaffold_resource generator is that it not only creates a reasonably complete set of controllers, database migrations and views from which to start customizing the application, but it also creates the appropriate testing scripts, including complete functional tests.

Now, lets create the migration for the table bridging these two resources in the database, remembering that the naming convention for this bridge table involves combining the names of the tables being bridged, in alphabetical order, with an underscore:

script/generate migration create_categories_items exists db/migrate create db/migrate/003_create_categories_items.rb

Crack open this migration file, and modify it like so:

class CreateCategoriesItems < ActiveRecord::Migration def self.up create_table :categories_items, :id => false do |t| t.column :category_id, :integer, :null => false t.column :item_id, :integer, :null => false end add_index :categories_items, [:item_id, :category_id] add_index :categories_items, :category_id end def self.down remove_index :categories_items, [:item_id, :category_id] remove_index :categories_items, :category_id drop_table :categories_items end end

This closely follows the recommendations from Agile Web Development With Rails pages 323-324. Aside from adding the indexes for performance, an important part of this file is the :id => false specification. If you don’t do this, an id column will be created automatically for this table, which will confuse ActiveRecord, and as a result, you’ll see some very interesting bugs when you try to add and edit category records.

At this point, we might as well create the underlying database tables. Properly configure your config/database.yml file with the appropriate test_development database connection information, and on the command line execute this to create the test_development database and install our three database migrations to create the tables:

mysqladmin -u root create test_development rake db:migrate == CreateCategories: migrating ================================================ — create_table(:categories) -> 0.0777s == CreateCategories: migrated (0.0779s) ======================================= == CreateItems: migrating ===================================================== — create_table(:items) -> 0.0550s == CreateItems: migrated (0.0552s) ============================================ == CreateCategoriesItems: migrating =========================================== — create_table(:categories_items, {:id=>false}) -> 0.0034s — add_index(:categories_items, [:item_id, :category_id]) -> 0.0181s — add_index(:categories_items, :category_id) -> 0.0045s == CreateCategoriesItems: migrated (0.0264s) ==================================

Now that the database is in place, we can begin to tell our models about it. Starting with categories, modify app/models/category.rb accordingly:

class Category < ActiveRecord::Base has_and_belongs_to_many :items acts_as_tree def ancestors_name if parent parent.ancestors_name + parent.name + ‘:’ else “” end end def long_name ancestors_name + name end end

This tells rails about the relationship the Category model has with the Item model, and also tells Rails about the self referential relationship that the Category model has with itself, through the acts_as_tree declaration.

The ancestors_name and long_name methods are lifted straight out of Ruby on Rails: Up and Running by Bruce Tate and Curt Hibbs, page 79. These methods enable a quick and dirty way of displaying the name of each category, preceded by each of its ancestors in order, to use in select lists in the interface.

We also need to modify the Item model by modifying app/models/item.rb accordingly:

class Item < ActiveRecord::Base has_and_belongs_to_many :categories end

Again, this tells Rails about the relationship the Item model has with the Category model, this time from the perspective of the Item model.

Moving up the ladder, we need to modify both app/controllers/categories_controller.rb and app/controllers/items_controller.rb controllers to supply the views with the appropriate data to populate select lists. To do this, we query the database for all the categories and store it in an instance variable for both the new and edit methods for both the categories_controller and the items_controller:

@all_categories = Category.find(:all, :order=>”parent_id, name”)

Ordering the results by parent_id, then name should give us an acceptable ordering.

Now for the most challenging part of making this functional: displaying select lists in the new/edit views for categories and items. OK, this may not be “challenging” per se, but as someone new to Rails, I did have to spend a little time orienting myself to the various options for implementing select lists using Rails helpers. I ultimately found the collection_select helper, the API documentation for which is certainly suggestive, but not drop-dead obvious:

collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})

Here is how I currently understand it. The first two parameters, object and method, are strings referring to the name of the current object (resource) you are working on and the name of that object’s attribute that gets assigned the value from the select list (basically, the name of the foreign key column). Note that the object name here is the lower case, singular name of the object–the same name we used when generating its scaffolding. If you are new to Rails as I am, getting these right can be a little confusing at first.

For the next parameter, we supply the collection of model objects that will be used to populate the select list itself, which in this example is the @all_categories instance variable we created in the two controllers. In the next two parameters after this, we tell Rails which attributes of those model objects should be used to populate the option tag value (what ultimately gets assigned to the current object we are creating/editing) and the option tag text that is displayed to the user. Both of these parameters should be specified as symbols (preceded by ‘:’).

The final two parameters are optional and allow us to further customize the look and behavior of the select list. The first options parameter is specifically for options of the collection_select form helper itself, while the html_options allow you to pass in attributes on the HTML select tag, such as assigning a CSS class for example. Both sets of options are specified as hashes, using symbols for the keys.

Lets look at two examples. For the categories new/edit views, we only need a single selection list, because each category has at most one parent. For the items new/edit views, we want a multiple selection list, because multiple categories can be assigned to a given item. We can do both using the collection_select helper defined in a ‘partial’ page template.

Starting with the collections new/edit views, create a new file for the single selection list partial in app/views/categories/_select_categories.rhtml:

<%= collection_select(‘category’, ‘parent_id’, @all_categories, :id, :long_name, { :include_blank => true } ) %>

Remember that the file names of partials start with an underscore. I added the :include_blank option to allow for top level categories without parents. Then in both app/views/categories/new.rhtml and app/views/categories/edit.rhtml, replace the existing parent form field with the following to display the select list:

<%= render(:partial => “select_categories”) %>

Again, note that we are referring to the partial without the underscore.

Similarly, create a new file for the multiple selection list partial in app/views/items/_select_categories.rhtml:

<%= collection_select(‘item’, ‘category_ids’, @all_categories, :id, :long_name, {}, {:multiple => true, :size => ’10’} ) %>

Then in both app/views/items/new.rhtml and app/views/items/edit.rhtml, add the following to display the select list:

<%=render(:partial => “select_categories”) %>

We now have a pretty strong start to some RESTful management screens for our two resources. We didn’t have to manually code the select lists, and things like displaying the selected categories for existing item objects is done for us. To see it in action, kick of the WEBrick server:

script/server

…and for now, manually go to the management screens for these resources:

  • http://localhost:3000/categories
  • http://localhost:3000/items

You probably also want to modify the index.rhtml and show.rhtml views for each resource to properly display categories, but that, as they say, is an exercise for the reader. To really do some interesting things with this basic approach to categories, explore the resources below.

Resources

  • Acts As Tree API documentation
  • API documentation on form option helpers in Rails.
  • Select helper methods in Ruby on Rails: The single best resource I found describing the choices among various form option helpers, and their use.
  • Creating Dynamic Drop-Downs: Good examples of using form option helpers from the Rails wiki.
  • Category Tree Using Acts As Tree: Another Rails wiki entry showing a different way to display categories using acts_as_tree.

Plugins for hierarchical/nested data structures.

  • Acts As Tree
  • Acts As Ordered Tree
  • Acts As Nested Set
  • Better Nested Set
  • Acts As Threaded

Related Post