Positionable

Built on top of the rails acts-as-list plugin bringing forth concepts from Sean Huber's sortable. This gem brings positionable extension to controllers and helpers.

Install

Specify it in your Rails config.

config.gem 'aguids-positionable', :lib => 'positionable', :source => 'http://gems.github.com'

Then install it.

rake gems:install

Usage

The first step is to declare a model that acts-as-positionable:

class ListItem < ActiveRecord::Base
  acts_as_positionable
end

Then we are set to use any of the familiar acts-as-list public methods on an instance of the model.

list_item.move_to_bottom
list_item.move_higher

The plain acts-as-positionable declaration although useful is somewhat lacking. To better suit your needs it accepts some useful options.

Column

It is expected that the model possess a position column set to integer. If the position column is not available, say you are working with a legacy app, then you can define which column to use as the list's position with this option:

acts_as_positionable :column => 'my_different_attribute'

Scope

Rarely we will want a list that cover all possible records of a model. Most of the times our lists makes sense only on behalf of another resource. Maybe our products are grouped under categories, or our list items are grouped under lists. Thus the scope option to group records onto their appropriate lists.

class List < ActiveRecord::Base
  has_many :list_items, :order => 'position'
end

class ListItem < ActiveRecord::Base
  belongs_to :list
  acts_as_positionable :scope => :list_id
end

With this option set, list_items with the same list_id would be grouped under the same list, and list_items with a nil list_id would also me grouped under a list.

Record Updates

What if my record is updated and its scoped value changes? No need to worry. The record will be transparently removed from the previous list and inserted at the bottom of the new one.

Multiple Scopes

Our schema might be a little bit complex and out list items are grouped not only by list but also by status, a string that might be 'todo' or 'done'. The only difference here would be to declare the scopes as an array, in which case the scope option accepts as many scopes as we want, and there is no need for them to be an integer.

acts_as_positionable :scope => [:list_id, :status]

Conditions

Some times we just want some specific records to be grouped under a list and this restriction doesn't maps well to the scoping concept. Say that we want to group in a list all our list_items that are flagged as todo and it doesn't makes sense to us to have a list of our done items.

acts_as_positionable :conditions => {:status => 'todo'}

With this declaration only items with todo status would be grouped on lists, all other items would have their list position marked as nil.

The positionable conditions option accepts most of the activerecord find conditions syntax, thus the previous example could also be written as:

acts_as_positionable :conditions => ["status = ?", 'todo']
acts_as_positionable :conditions => "status = 'todo'"

Record Updates

What if my record is updated and it no longer meets the conditions set for the list? As those records that don't meet conditions don't belong in any list, if a record no longer meets the list's conditions after an update it will be transparently removed from the list. The opposite holds true for a record that doesn't meet the conditions before an update but does after the update.

List Name

We might want to set the same record on two separate lists. To accomplish that all we need are two columns available to store the lists' position, and two declarations of acts-as-positionable.

acts_as_positionable
acts_as_positionable :list_name => :other, :column => 'other_position'

The first declaration will set the list named :default, using the :position column. With these declarations in place we can now use the positionable api in a different way:

list_item.move_up(:other)           # This would only update the :other list
list_item.move_to_bottom(:default)  # This would only update the :default list

When dealing with only one list, there is no need to pass the list name as the :default is assumed.

Positionable Features

Controller and Helper

In the best case scenario you won't need to write a single line of controller or helper code. There is no need to create routes whatsoever as the available route will take care of all the cases. All positionable controller actions are available under:

:resource/:id/:list_name/position/:action

But most of the time you won't need to address this route and use the available helpers for the move and insert methods:

move_up(record)   # Would output record_class/record_id/default/position/move_up

You might not even need to touch this helper as there is even a higher level helper to address the common links or buttons for the move actions:

positionable_links(record)
positionable_buttons(record)

Both these helpers would output links/buttons for the move method if the record is on the list, or the insert methods otherwise. If you would like to internationalize the links and buttons generated, just define translations for the actions under the positionable namespace:

positionable:
  move_to_top: 'Top'

ActiveRecord Named Scopes

A positionable model has two activerecord named scopes for easy manipulation of the lists. They order the record either by ascending or by descending list position.

ListItem.ascending
ListItem.descending(:other)

They can be used with associations too:

list.list_items.ascending
list.list_item.descending

Another named scope is added but it only makes sense using if we set the conditions option on the acts-as-positionable declaration:

ListItem.conditions(:other)

The effect would be to narrow the records to only those that meet the conditions.

Methods

List of the methods added with the acts-as-positionable declaration. All of them accept an optional list_name param.

# Insert the item at the given position (defaults to the top position)
insert_at(position)

insert_at_top

insert_at_bottom

# Swap positions with the next lower item, if one exists.
move_lower
move_down

# Swap positions with the next higher item, if one exists.
move_higher
move_up

# Move to the bottom of the list.
move_to_bottom

# Move to the top of the list.
move_to_top

# True if the record is the first in the list.
first?

# True if the record is the last in the list.
last?

# Returns the next higher item in the list.
higher_item
previous_item

# Returns the next lower item in the list.
lower_item
next_item

# True if the record is in the list.
in_list?

# Returns the record list position for the list.
list_position

# Removes the record from the list and shift other items accordingly.
remove_from_list

Credits

Positionable is a mix of the Rails Core acts-as-list plugin with the gap aware fork by Ryan Bates and the features from sortable by Sean Huber

Author

Felipe Doria

License

Positionable is available under the MIT license.