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. ### 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. [acts-as-list]: http://github.com/rails/acts_as_list/tree [sortable]: http://github.com/shuber/sortable/tree [fork]: http://github.com/ryanb/acts-as-list/tree