In this lab, we’ll create a database-driven Resources page for our site that presents a list of links. We’ll also illustrate how you can use Ajax to allow the sorting of the list to be changed without reloading the page.
1. Creating the links and categories database
a. Creating the models
We’re going to need two models: one for the links, and one for the categories. As in the previous lab, we’ll use the scaffold command to create the models and the first cut at the admin pages for them.
For the links, we need a name, a URL, and a description, so issue the following command (in a terminal window at the root of your application):
ruby script/generate scaffold link name:string url:string description:text
The categories model needs only a name, so issue this command:
ruby script/generate scaffold category name:string
b. Creating the join table
A link can have many categories, and a category can have many links, so we need a many-to-many relationship, which we’ll implement with has_and_belongs_to_many. To do so, we need a join table, which records the relationships between the two models.
Rails does not provide a generator for join tables, so we need to create a migration and then enter into the migration file the specifications for the join table. In NetBeans, you can right-click on the project name, choose Generate, and select Migration from the pop-up menu. Then enter the name for the migration; it doesn’t matter what this is, but we’d suggest something like add_link_category_join
. (If you want to do this from the terminal, enter the command ruby script/generate migration add_link_category_join
.)
Now open the migrations folder (Database Migrations in the NetBeans projects view, or db/migrate in the file system), and edit the empty migration file that the generator created. It should be named something like 006_add_link_category_join.rb
. (If you’re using NetBeans, this file will be automatically opened for you.)
In the self.up
method, insert the following code:
create_table :categories_links, :id => false do |t| t.integer :link_id t.integer :category_id end
The first line of this migration code tells Rails the name of the table you want to create, and that you don’t want it to automatically generate an ID field. Join tables don’t have an ID of their own, just the two foreign IDs of the tables they are linking together. The do |t|
begins the code block that defines the table, and gives the arbitrary name t
to the object that is used to create the table (the internals of this are a little complex, and you can just think of this as a magic incantation that you can use in every migration).
The names of the table and the fields are not arbitrary, but are required for Rails to be able to make all the connections. This is an example of convention over configuration. The table name is the two model names, joined by an underscore, in alphabetical order. The field names are the model names with _id
appended.
Now add this line to the self.down
method:
drop_table :categories_links
This allows you to migrate back down if desired.
Now run all three migrations. (In NetBeans, right-click on the project name, choose Migrate Database > To Current Version, or in the terminal enter rake db:migrate
).
c. Setting up the association
To make the association work, we need to tell Rails how we want these two models associated. To do so, add this line to models/link.rb:
has_and_belongs_to_many :categories
and this line to models/category.rb:
has_and_belongs_to_many :links
That’s all you need to do for Rails to know to use the join table and associate the two models. Whenever you save a Link object for which one or more categories has been specified, the appropriate entries will automatically be written to the join table.
2. Fleshing out the link admin
a. Adding a category picker to the link views
The scaffolded views that Rails produces for us don’t deal with associations between models; they just work with one model at a time. This is fine for the category admin pages, which allow us to create and edit category names. But for the links admin pages, we need to add the ability to choose categories to associate with each link.
We want to add a select element to the new link form that lists all the categories. Here’s the line of code that generates the desired select, which you should add to the file views/links/new.html.erb:
<p><b>Category</b><br /> <%= f.collection_select :category_ids, @categories, :id, :name, {}, {:name => 'link[category_ids][]', :multiple => true} %>
This is perhaps the most obscure bit of code we’ve seen yet, and it is something that is not as straightforward in Rails as it should be. The collection_select helper creates a select field, using an array of objects as the source for the select list. It’s a powerful helper, but the parameters are a little obtuse.
The first parameter, “:category_ids”, specifies the field that we want to set. In this case, since what we want to set is actually the id in a join table, the peculiar syntax of “category_ids” instead of “categories” is required. The second parameter, “@categories”, is the name of the array that has the objects used to populate the select list. We’ll set this in the controller. The next two fields specify the attributes of the objects in this array to be used for the value returned by the select list, and for the text to be displayed in the select list. The empty hash is a placeholder for an options parameter that we’re not using.
The final parameter is the HTML options hash. First, we set the name that will be used when posting the form data. This obscure syntax is what is required for the Rails “save” operation (“@link.save” in the controller) to properly update the join table. This is a peculiarity of using a has_and_belongs_to_many relationship with a select list. The final item is setting the standard HTML option to allow multiple items in the select list to be chosen, so a link can be assigned to multiple categories.
How would you figure all this out? It’s tricky. We couldn’t figure it out from the documentation. But some google searching on terms such as “habtm collection_select” found some helpful notes. Sometimes that’s what it takes. You can also ask questions about such things in the rubyonrails-talk Google group.
Add the category selector to the edit view as well, so you can edit existing categories.
Note: in the code on the USB Flash drive, there is an error in this code in the “new” view, but it is correct in the “edit” view.
If you try to use these views now, you’ll get an error, because we haven’t yet set the instance variable “@categories”.
b. Providing the list of categories in the links controller
In the links controller, we need to prepare the list of categories to be used in the select list. Just add this line of code to the “new” method in /controllers/links_controller.rb:
@categories = Category.find(:all, :order => :name)
This statement finds all the categories, sorts them by name, and puts them (as an array) in the instance variable “@categories”. The select list in the view then draws from this instance variable to populate the select.
Since we use the same instance variable in the edit view, you need to add this line of code to the edit action in the links_controller as well. And there’s two more places where we need it too: at the start of the “else” clause in the create action and the update action. That’s because, if there’s a validation failure when creating or editing the link, the form will be displayed again, and it will need the instance variable to populate the select list.
c. Adding links on the admin pages
To provide easy access to our new link and category admin, let’s add a couple links to our admin page. Add to the list in /views/static/admin.html.erb:
<li><%= link_to 'Links Admin', links_path %></li> <li><%= link_to 'Categories Admin', categories_path %></li>
To test all this, create a few categories, and then create a few links. You should see the list of categories in the select list.
d. Protecting the link and category admin pages
One last step for the link and category admin: we don’t want site visitors to access these admin pages, so just as we did for the other administrative controllers, add this line at the top of the links and categories controllers:
before_filter :login_required
3. Creating the Resources page from the database
a. Adding the resources_page view
Now our admin features for links are in place, and we need to generate a page that displays them. We’d like to add this to our links controller and views, to keep the link-related functions together. But the view that shows a list of links, “index”, is an admin view. So we need to add a new view for the public part of the site, which we’ll call “resources_page”.
Create a new file in the views/links folder, called “resources_page.html.erb” (remember that NetBeans adds the “.erb” for you when you create a new erb file). The simplest thing we can do is to just list out all the links, assuming we have an instance variable that the controller has prepared for us:
<h1>Resources</h1> <ul> <% @resources.each do |resource| %> <li><%= link_to resource.name, resource.url %><br /> <%= resource.description %></li> <% end %> </ul>
This code uses the “each” iterator to step through each of the items in the @links array, and then uses the “link_to” helper to create a link to each one. We use the name attribute as the link text, and the url attribute as the actual link, and then show the description on the next line. (Note that this code assumes that your links, as entered into the database, include “http://”.)
(An aside: We named the instance variable “@resource” so it didn’t get confusing with too many things called “link”, but you could just as well use “link” everywhere we’ve used “resource”, and in retrospect this might have been better. It is a Link object we’re accessing.)
Now, since we want the resources page to be visible to anyone, even though the other actions, in the resources page are for admin use only, modify the before_filter we added previously to the links_controller as follows:
before_filter :login_required, :except => 'resources_page'
b. Updating the links controller
Our newly-created resources_page view expects an instance variable called @resources with an array of Link objects, so we need to create that in the controller. We’ve added a new view, so we need to add a new method to the controller. Open the file controllers/links_controller.rb, and add the following method (it can go anywhere, but we’d put it at the end).
def resources_page @page_title = "Resources" @current_tab = 'resources_tab' @resources = Link.find(:all) end
In addition to setting the @resources instance variable, we’ve set the variables that control the navigation tab highlighting and the page title.
c. Enabling the non-RESTful route
We need to do one more thing to make this all work. We’ve added a non-RESTful action (that is, not one of the core group of standard REST actions) to the links controller. The routing for this controller is set with the statement map.resource :links
in the routes.rb file, and this only supports the standard routes. So we need to tell the routing that we’ve added another action. Modify that line in config/routes.rb so that it reads:
map.resources :links, :collection => {:resources_page => :get}
This tells the routing system that we’ve added another action, which acts on the collection of links (rather than on a single link), that the action is called “resources_page”, and that the HTTP method used to access it is “get”. (Alas, our use of the instance variable name “@resources” might cause a little more confusion here; that use of the word resources has nothing to do with “map.resources”. In that context, resources is referring to the standard set of routes for RESTful resources.)
With this change, you should now be able to browse to //localhost:3000/links/resources_page and see the list of links that you entered earlier.
d. Cleaning out the old static page
Our navigation button labeled “Resources” still points to the static page we created in Lab 1, so let’s change that.
Open views/layouts/application.html.erb, and find the “link_to_” statement that creates the “Resources” navigation button. Change that line to read:
<li><%= link_to 'Resources', {:controller => 'links', :action => 'resources_page'}, :id => 'resources_nav' %></li>
Finally, delete the resources action from the static controller, since it is no longer used. Now you should be able to click on the Resources navigation button and see the dynamically generated list of links, complete with descriptions (assuming you entered some).
(A debugging note: if you find that your resources links don’t work when you click on them, check that you have prefaced them with http:// when you entered them in the database. Otherwise, they will be viewed as relative URLs on your site, not as external sites.)
3. Sorting the list by category
So far, we haven’t done anything with the categories we have assigned to the links. Let’s sort the links by category, and display them under a heading for each category.
Replace the code in views/links/resources_page.html.erb with the following:
<% @categories.each do |category| %> <h2><%= category.name %></h2> <ul> <% category.links.each do |resource| %> <li><%= link_to resource.name, resource.url %><br /> <%= resource.description %></li> <% end %> </ul> <% end %>
We’ve added an outer loop that iterates through the categories. For each category, we display the category name, and then we iterate through each of the links, using “category.links” to find the links associated with the category. This method is added to the category object because we included the has_and_belongs_to_many :links
statement in the category model.
This code needs the list of categories, so add this line to the resources_page action in the links controller:
@categories = Category.find(:all, :order => :name)
Now refresh the resources page, and you’ll see the links listed by category. If a link is assigned to more than one category, it will appear in more than one place.
3. Adding a touch of Ajax
Suppose you wanted to give the viewer the option of dividing the list by category or not. We could create two different resources pages with the two different views. Or we could pass a query string parameter to the view that would choose one option or the other. But we’ll take a more sophisticated approach, which may be overkill in this case but allows us to illustrate how Ajax works: updating how the list is displayed, based on a check box, without reloading the entire page.
a. Adding a sorting option to the view
As a first step, let’s put in both forms of our view code, with a parameter choosing which is displayed. The new view (except for the H1 heading at the top) looks like this:
<% if params[:divide_by_category] == '1' %> <% @categories.each do |category| %> <h2><%= category.name %></h2> <ul> <% category.links.each do |resource| %> <li><%= link_to resource.name, resource.url %><br /> <%= resource.description %></li> <% end %> </ul> <% end %> <% else %> <ul> <% @resources.each do |resource| %> <li><%= link_to resource.name, resource.url %><br /> <%= resource.description %></li> <% end %> </ul> <% end %>
This is simply our two versions of the view code, with an if statement choosing which version is used.
Now when you browse to the Resources page, you’ll see the list as we first rendered it, without a division into categories. But if you append the parameter to the URL, so the complete URL is now:
//localhost:3000/links/resources_page?divide_by_category=1
Then you’ll see the page divided into categories. We haven’t done any Ajax here — we’re just passing a query string parameter (the thing after the question mark in the URL) to the view. Such parameters are provided in the params hash, as you can see in the first line of our updated view.
b. Moving the list into a partial
As a first step toward turning this into an Ajax function, let’s move the generation of the list into a partial. Cut all the text that you just put into the view, so there’s nothing there but the H1 heading. Create a new file in views/links, called “_list.html.erb”. The leading underscore is what identifies this file as a partial, rather than a full view. Now paste the text you just deleted from the links/resource_page view into the new _list view (or copy it from the listing above).
To reference the partial from the resources_page view, add this line after the H1 heading:
<%= render :partial => 'list' %>
Note that you don’t include the underscore, or the suffixes, when you reference a partial.
Make sure the modified view files are saved, and then refresh the Resources page. It should behave just as before. We haven’t changed the behavior yet, we’ve just moved the rendering of the list into a partial, for reasons that will become apparent shortly.
c. Updating the view for Ajax
To turn this into an Ajax view, we need to do three things:
- Add a mini-form with a check box to select the view
- Wrap the partial in a div so we can apply an ID and thus be able to modify that div using JavaScript
- Add an observer to monitor the checkbox, request an updated list from the server, and generate a new list
Here’s full code for the new view:
<% form_tag '#' do %> <%= check_box_tag 'divide_by_category' %><label>Divide by Category</label> <% end %> <div id='list'> <%= render :partial => 'list' %> </div> <%= observe_field 'divide_by_category', :on => 'click', :url => {:controller => 'links', :action => 'get_list' }, :with => 'divide_by_category', :update => 'list' %>
We’ve created a form that has no action associated with it, because the form will never be submitted (it has no submit button). Inside this form, we have just the check box.
Then we have the same partial we used in the previous step, just wrapped in a div so we can replace it using JavaScript.
And finally, the observe_field helper provides all the Ajax magic. This is a very powerful helper, and it can take a lot of parameters. Here’s the explanation for each of the parameters we’ve used:
‘divide_by_category’
is the ID of the field that we want to observe:on => ‘click’
specifies that it is the click event on the check box that we want to have trigger the Ajax request.:url => {:controller => ‘links’, :action => ‘get_list’ }
specifies the controller and action to which the request should be sent when the field changes:with => ‘divide_by_category’
indicates that we want the value of this field sent to the controller when the Ajax request is sent:update => ‘list’
indicates that the div with the ID ‘list’ should be updated with the Ajax response
This observe_field helper is our first bit of JavaScript, using the Prototype framework, and we haven’t yet included that code in our HTML. So open up layouts/application.html.erb, and add the following line in the head section:
<%= javascript_include_tag :defaults %>
This helper creates HTMLscript
tags to include all the standard Rails JavaScript files.
You can try the new page, but the check box won’t do anything. Why? We haven’t yet provided the get_list
method that the Ajax action is invoking. If you are using Firefox and have Firebug installed, open the Firebug window and click the Console tab with the Resources page displayed, and then click the check box. You’ll see that a POST action occurred (that’s that XMLHTTP request generated by the observe_field helper) but the result was a 404 error, because there is no such action.
d. Adding the Ajax controller action
Now that we have everything in place, we just need an action that will return the list.
Add the following method to the end of the links controller:
def get_list @resources = Link.find(:all) @categories = Category.find(:all) render :partial => 'list', :layout => false end
This is the action that will be invoked by the Ajax request. It sets up the two instance variables required by the list partial, and then renders that partial. We need to add the option :layout => false
because we want just the bare results from this view, not wrapped in any layout file, since we aren’t using it to render a page; we’re just returning the HTML as the result of the Ajax request, and it will be used to modify the contents of the list div.
Now try the check box in the Resources page again. It should control whether or not the view is sorted by category or not. Note that the URL does not change, and there is no page refresh. When the get_list
action returns the HTML for the list to the JavaScript code generated by the observer_field
helper, the JavaScript code replaces the contents of the “list” div with this new HTML. You can use the Firebug console to see the Ajax request and response each time you click on the check box.
Next Steps
You’ve now created a database to manage a set of links, sorted by category, and a page that allows them to be displayed with or without category sorting using Ajax.
Here’s some enhancements you might consider (and which we’ll put into our final version of the sample application):
- Don’t display the category name if there are no links in that category
- Validate that there is at least a name and a URL for each link
- Validate that the category name is not blank
- Validate the format of the URL
- Make the simple URL“/resources” work for the new resources page