The following blog post contains material either currently found or soon to be incorporated into my new book, "Easy Active Record for Rails Developers". Among many other topics, you'll learn about model generation, migrations, validations, associations, scopes, joins, includes, forms integration, nested forms, and model testing with RSpec and FactoryGirl. The book is now available, head over to this website's home page to learn more.


Whether your application is focused on TODO list management or fantasy football, users will appreciate the ability to sort and reorder various lists of items to their liking. Logically the desired ordering will need to be persisted so the user doesn’t have to bother with reordering items upon each return to your website. But what is the most effective way to perform list sorting and reordering when working with an ActiveRecord model? How might you for instance move a record “up” a list while also ensuring the record is is replacing is properly moved “down”? What is the best approach for retrieving an item deemed to be “higher” up the list than another? How about the “lower” item? What do terms such as “higher” and “lower” even mean? In this post I’ll introduce you to a great gem called ActsAsList that largely removes the complexities of dealing with lists of items in these manners and more.

Installing ActsAsList

ActsAsList was created by none other than the father of Ruby on Rails, David Heinemeier Hansson and now maintained by Swanand Pagnis. It’s a standard Ruby gem, meaning installing it is as easy as adding it to your project Gemfile:

gem 'acts_as_list'

After adding this line, save the changes to Gemfile and run bundle to install the gem. Once installed you’ll need to make a slight modification to any models you intend to sort and reorder. We’ll do this next.

Creating the Game Model

For the sake of example I’ll create a model named Game which manages a list of retro arcade games:

$ rails g model Game name:string year:integer
  invoke  active_record
  create    db/migrate/20141111120350_create_games.rb
  create    app/models/game.rb
  invoke    test_unit
  create      test/models/game_test.rb
  create      test/fixtures/games.yml

After creating the model run the corresponding migration to create the table:

$ bundle exec rake db:migrate

Next we’ll create the User model for housing the application user profile data.

Creating the User Model

Each user should be able to maintain his own separate list of sorted games, and so we’ll need to associate a User model with the Game model using a has_many :through relation. The has_many :through relation is required because we’re going to add an additional attribute to the User <–> Game relation, something that isn’t possible when using a has_and_belongs_to_many (HABTM) relation. Let’s create the User model now. In a real-world situation you’ll probably use the Devise gem (Devise is introduced in great detail in Chapter 7) to manage users however for the sake of illustration I’ll keep this fairly simple:

$ rails g model User username:string
  invoke  active_record
  create    db/migrate/20141111133734_create_users.rb
  create    app/models/user.rb
  invoke    test_unit
  create      test/models/user_test.rb
  create      test/fixtures/users.yml

After creating the model run the migration to add the table to the database:

$ bundle exec rake db:migrate

Adding Some Sample Data

With the Game and User models in place let’s add some sample data using the db/seeds.rb file:

games = Game.create([
  { name: '1942', year: 1984 },
  { name: 'Berzerk', year: 1980 },
  { name: 'Airwolf', year: 1987 },
  { name: 'Frogger', year: 1981 },
  { name: 'Asteroids', year: 1979 },
  { name: 'Golden Axe', year: 1989 }
]);

users = User.create([
  { username: 'wjgilmore' },
  { username: 'johnqmember' },
  { username: 'sallyride' },
  { username: 'johnglenn' }
]);

Add the data using the db:seed command:

$ bundle exec rake db:seed

Incidentally, one easy way to conveniently view table data without logging into your database is via the great table_print gem (discussed in Chapter 6). I’ll use this gem by logging into the Rails console and having a look at the games table contents:

$ rails console
>> tp Game.all
  Game Load (1.1ms)  SELECT "games".* FROM "games"
ID | NAME       | YEAR | CREATED_AT              | UPDATED_AT              
---|------------|------|-------------------------|-------------------------
1  | 1942       | 1984 | 2014-11-11 13:19:43     | 2014-11-11 13:19:43     
2  | Berzerk    | 1980 | 2014-11-11 13:19:43     | 2014-11-11 13:19:43    
3  | Airwolf    | 1987 | 2014-11-11 13:19:43     | 2014-11-11 13:19:43   
4  | Frogger    | 1981 | 2014-11-11 13:19:43     | 2014-11-11 13:19:43   
5  | Asteroids  | 1979 | 2014-11-11 13:19:43     | 2014-11-11 13:19:43   
6  | Golden Axe | 1989 | 2014-11-11 13:19:43     | 2014-11-11 13:19:43   

Creating the User’s Personal Game Library

OK we’re making some serious progress. Next we’ll create the model used to manage each user’s personal game library. We’ll call this model Library, and it will contain references to the Game and User models, and additionally an attribute named position which will be used to manage the position of each user’s game list:

$ rails g model Library game:references user:references position:integer
...
$ bundle exec rake db:migrate

Next we’ll need to establish the relationship between the User and Game models using the has_many :through association. Open the Game model first and add the following lines:

class Game < ActiveRecord::Base

  has_many :library, ->{ order 'position' }
  has_many :users, through: :libraries

end

Next, add a similar relation to the User model:

class User < ActiveRecord::Base

  has_many :library, ->{ order 'position' }, class_name: 'Library' 
  has_many :games, through: :library

end

In this example I buck Rails convention by using a singular form for the has_many relation (library instead of libraries) because syntax such as user.libraries sound weird; instead I’d rather use user.library.

Finally, open the Library model and define the belongs_to relation:

class Library < ActiveRecord::Base
  belongs_to :game
  belongs_to :user
end

With the has_many :through relation in place we’re finally ready to use the ActsAsList gem! Let’s quickly add a few games to a user’s library:

$ rails console
>> u = User.find_by(name: 'wjgilmore')
>> u.games << Game.find_by(name: 'Asteroids')
>> u.games << Game.find_by(name: 'Frogger')
>> u.games << Game.find_by(name: 'Airwolf')
>> u.games << Game.find_by(name: 'Golden Axe')

With a few games assigned we can confirm the results again using the great table_print gem:

>> tp u.games
ID | NAME       | YEAR | CREATED_AT              | UPDATED_AT
---|------------|------|-------------------------|------------------------
23 | Asteroids  | 1979 | 2014-11-11 17:29:22     | 2014-11-11 17:29:22
22 | Frogger    | 1981 | 2014-11-11 17:29:22     | 2014-11-11 17:29:22
21 | Airwolf    | 1987 | 2014-11-11 17:29:22     | 2014-11-11 17:29:22
24 | Golden Axe | 1989 | 2014-11-11 17:29:22     | 2014-11-11 17:29:22

Using the ActsAsList Gem

Because the Library model represents the user’s list of games, we’ll need to annotate the Library model, adding ActsAsList functionality to it:

class Library < ActiveRecord::Base
  belongs_to :game
  belongs_to :user
  acts_as_list scope: :user
end

With this declaration in place, it’s now possible to sort and reorder a user’s games using ActsAsList syntax. Let’s look at the library as it currently stands:

>> tp u.library
ID | GAME_ID | USER_ID | POSITION | CREATED_AT              | UPDATED_AT
---|---------|---------|----------|-------------------------|------------------------
5  | 23      | 5       | 1        | 2014-11-11 17:30:10     | 2014-11-11 17:30:10
6  | 22      | 5       | 2        | 2014-11-11 17:30:16     | 2014-11-11 17:30:16
7  | 21      | 5       | 3        | 2014-11-11 17:30:22     | 2014-11-11 17:30:22
8  | 24      | 5       | 4        | 2014-11-11 17:30:27     | 2014-11-11 17:30:27

As you can see the games have been positioned in accordance with the order in which they were added to the user’s library. To move the top game downwards you can use the move_lower method:

>> u.library.first.move_lower

Because I’m demonstrating these examples in the Rails console I’ll need to reload the User object to demonstrate how ActsAsList reorders the results:

>> u.reload
>> tp u.library
ID | GAME_ID | USER_ID | POSITION | CREATED_AT              | UPDATED_AT
---|---------|---------|----------|-------------------------|------------------------
6  | 22      | 5       | 1        | 2014-11-11 17:30:16     | 2014-11-11 17:56:15
5  | 23      | 5       | 2        | 2014-11-11 17:30:10     | 2014-11-11 17:56:15
7  | 21      | 5       | 3        | 2014-11-11 17:30:22     | 2014-11-11 17:53:12
8  | 24      | 5       | 4        | 2014-11-11 17:30:27     | 2014-11-11 17:30:27

Notice how the 22 and 23 records have swapped position. The move_lower method is just one of several that will move a record and reorder results. Try experimenting with move_higher, move_to_bottom, and move_to_top.

You also have access to a number of methods useful for retrieving records relative to a specific position. For instance you can retrieve all records lower than the first item. In this example I’ll include the SQL used to retrieve this information so you can get a feel for what ActsAsList is doing under the covers:

>> u.library.first.lower_items
 (0.2ms)  SELECT COUNT(*) FROM "libraries"  WHERE ("libraries"."user_id" = 5)
Library Load (0.2ms)  SELECT  "libraries".* FROM "libraries"  
  WHERE ("libraries"."user_id" = 5) AND (position > 1) AND (position <= 5)
  ORDER BY libraries.position ASC LIMIT 4

Other methods are available for achieving similar outcomes, such as higher_item, lower_item, and higher_items.

Summary

ActsAsList is one of those powerful gems which is limited in scope yet offers immediate access to otherwise complex list ordering and sorting features. If you’re doing something interesting with ActsAsList or any similar gem, be sure to tell us about it in the comments!

Comments