views:

157

answers:

3

I want to have some kind of single list that is initialized in a seperate module, then can be included in a controller and modified at the controller-class level, and accessed at the controller-instance level. I thought class variables would work here, but something strange is happening, they don't seem to be being initialized within my ending class.

More specifically:

I have many controllers all including some default functionality, in a module.

class BlahController < ApplicationController
  include DefaultFunctionality
end

class FooController < ApplicationController
  include DefaultFunctionality
end

 module DefaultFunctionality 
   def show 
     render 'shared/show'
   end 
   def model
     controller_name
   end
 end

, for instance. This isn't the actual code, but that's the most interaction it has at the moment.

I'd like to extend this with some other functionality (a sortable interface for lists,) like so [note I'd like to be able to swap out the sort-order-list functionality on a class by class basis]:

module DefaultFunctionality
 module Sortable
  def sort_params
    params.slice(:order, :sort_direction).reverse_merge(default_sort_params)
  end
  def default_sort_params
    @@sorts.first
  end
  def set_sorts(sorts = []) #sorts = [{:order => "most_recent", :sort_direction => :desc},...]
     @@sorts = sorts
  end
 end
 include Sortable
 set_sorts([{:order => :alphabetical, :sort_direction => :asc}] #never run?
end

The idea is to make sure that I'm able to swap out the set of all possible sorts on a class by class basis, like so:

class FooController < ApplicationController
  include DefaultFunctionality #calls the default set_sorts
  set_sorts([{:order => :most_recent, :sort_direction => :asc}]) 
end

And also to make nice links in the views, like below, except that I'm getting an error.

___/blah/1 => shared/show.html.erb__
<%= link_to("upside down", polymorphic_path(model, sort_params) %><%#BOOOM uninitialized class variable @@sorts for BlahController %>

I figure the class_var is a bad call, but I can't think of what else I might use. (a class instance var?)

+1  A: 

Class instance vars are the way to go, for sure. Rarely do you actually need to use a class variable.

In specific answer to your problem, keep in mind that any code defined in your module is executed only once when the module is loaded not when the module is included. This distinction may not be obvious, especially when people consider "include" to be equivalent to "require".

What you need is this:

module DefaultFunctionality
  def sort_params
    params.slice(:order, :sort_direction).reverse_merge(default_sort_params)
  end

  def default_sort_params
    @sorts.first
  end

  def sorts=(sorts = nil)
    @sorts = sorts || [{:order => "most_recent", :sort_direction => :desc}]
  end

  def self.included(base_class)
    self.sorts = ([{:order => :alphabetical, :sort_direction => :asc}]
  end
end

The way you catch a class including your module is to define Module.included accordingly. In this case, set_sorts is called each time this module is included, and it is in the context of the calling class.

I've modified this a bit to include a few specific style changes:

  • Declaring your defaults inside the method instead of in the declaration. Avoids producing unreadably long lines, or having to one-line otherwise complicated data structures.
  • Using class instance variables which will do the job in this case.
  • Using a Ruby-style var= mutator method instead of set_var()
tadman
Note that I wanted @sorts to be class-level, not instance level. See below for the answer I ended up using.The reason I left out any self.included() calls in my question - instance and class variables get really dirty when including submodules. So I'm giving you the upvote since you moved the methods into DefaultFunctionality, effectively removing the submodule issue that was causing untold headaches.
Tim Snowhite
I agree about the defaults in the declaration, I just wanted people to be aware of what the datastructure looked like. I should have commented it out to the side of the declaration, as it now appears.
Tim Snowhite
In my original code I was making individual SortOrder objects, so having a make_sorts! method to convert between [order, dir, name] arrays and SortOrder.new(order, dir, name)'s was useful. Obviously, here that is not the case. I've modified my answer to include your self.sorts style; it seems best for the example.
Tim Snowhite
If you need the @sorts variable to be consistent across instances where it has been included, what you might do is create a container module with module methods to manipulate it. Instead of using @sorts, you'd use something like DefaultFunctionality.sorts instead.
tadman
A: 

You have to add a included method to your module for this to work.

module DefaultFunctionality

 def self.included(base)
   base.include(Sortable)
   set_sorts([{:order => :alphabetical, :sort_direction => :asc}] #never run?
 end

 module Sortable
  # other methods

  def set_sorts(sorts = [{:order => "most_recent", :sort_direction => :desc}])
     @@sorts = sorts
  end
 end 
end
KandadaBoggu
A: 

I ended up using a class level instance variable and included() with no autoincluded submodules, before noticing that @tadman had responded.

It ended up looking like this:

class BlahController < ApplicationController
  include DefaultControllerFunctionality
  include DefaultControllerFunctionality::Sortable
end

class FooController < ApplicationController
  include DefaultControllerFunctionality
  include DefaultControllerFunctionality::Sortable
  sorts=([{:order => :most_recent, :sort_direction => :desc}])
end

in the controllers, and under /lib

lib/default_controller_functionality.rb

 module DefaultFunctionality 
   def show 
     render 'shared/show'
   end 
   def model
     controller_name
   end
 end

lib/default_controller_functionality/sortable.rb

 module DefaultControllerFunctionality
   module Sortable
    def self.included(base)
      base.helper_method :sort_params
      base.class_eval <<-END
          @sorts=[]
          class << self; attr_accessor :sorts end
       END
       #this line ^ makes it so that the module 
       #doesn't work when included in any other 
       #module than the class you want @sorts to end up in. 
       #So don't include(Sortable) in DefaultControllerFunctionality and expect .sorts to work in FooController.

      base.sorts= [{:order => :alphabetical, :sort_direction => :asc}]
    end
    def sort_params
      params.slice(:order, :sort_direction).reverse_merge(default_sort_params)
    end
    def default_sort_params
      self.class.sorts.first
    end
   end
 end
Tim Snowhite