I've been frantically trying to accomplish this same problem for the past week and have finally managed it. Since I found this question amidst my troubles, I figured that I'd share the answer here just in case anyone else finds themselves faced with a similar problem.
As rewritten stated in his answer, you can't set an instance variable on your model from the scope as there is no model instance yet. The scope is just for building up the sql expression that will be evaluated when you try to iterate over the result of the scope. What does exist, however, is the ActiveRecord::Relation object. This is the thing that holds the details of your scope. By putting the variables you want to keep on that object, they'll be still accessible later.
So, how to get the variable on to the Relation object? Monkey patching comes to your rescue here:
/lib/core_ext/add_scope_context_to_activerecord_relation.rb:
module ActiveRecord
class Relation
attr_writer :scope_context
def scope_context
@scope_context ||= {}
end
end
module SpawnMethods
alias_method :old_merge, :merge
def merge(r)
merged = old_merge(r)
merged.scope_context.deep_merge! r.scope_context
merged
end
end
end
Now, in all of your scopes, you have a scope_context variable. The SpawnMethods#merge malarky is to make sure that the scope_context is kept along a chain of scopes (eg Foo.search(xxx).sort(xxx).paginate(xxx)
)
/app/concerns/searchable.rb:
require 'active_support/concern'
module Searchable
extend ActiveSupport::Concern
included do
self.scope :search, Proc.new { |params|
s = scoped
s.scope_context[:searchable] = {}
s.scope_context[:searchable][:params] = parse_params params
s.scope_context[:searchable][:params].each do |param|
s = s.where generate_where_expression param
end
s
} do
def search_val col_name
param = scope_context[:searchable][:params].find do |param|
param[:col_name] == field.to_s
end
param.nil? ? '' : param[:val]
end
end
end
module ClassMethods
def parse_params params
# parse and return the params
end
def generate_where_expression param
"#{param[:col_name]} like '%#{param[:val]}%'"
end
end
end
Now, our controller, model and view will look like the following:
/app/controllers/foo_controller.rb:
class FooController < ApplicationController
def index
@foos = Foo.search(params[:search])
end
end
/app/models/foo.rb:
class Foo < ActiveRecord::Base
include Searchable
attr_accessor :name, :description
end
/app/views/foos/index.html.erb:
<p>You searched for:</p>
<table>
<tr>
<th>Name</th>
<td><%= @foos.search_val :name %>
</tr>
<tr>
<th>Description</th>
<td><%= @foos.search_val :description %>
</tr>
</table>
<p>And you got:</p>
<table>
<% @foos.each do |foo| %>
<tr> xxx </tr>
<% end %>
</table>
Now, you may have noticed the core_ext and concerns directories alluded to above. Your application will need to be made aware of these:
/config/application.rb:
# Should be fairly obvious where to put this line
config.autoload_paths += Dir[Rails.root.join('app', 'concerns', '{**}')]
/config/initializers/load_extensions.rb:
Dir[File.join(Rails.root, "lib", "core_ext", "*.rb")].each {|l| require l }
Don't forget to restart your server and away you go (assuming I haven't forgotten to include anything).
Oh, and I've come up with this solution with Rails 3.2.13 and Ruby 1.9.3; no idea how it behaves with other versions.