draper icon indicating copy to clipboard operation
draper copied to clipboard

How to decorate class methods?

Open yaegashi opened this issue 8 years ago • 1 comments

Is there a preferred / recommended way to do that with (or without) draper?

I wanted to add some view-related class methods to all models without touching their definitions in app/models. So I attempted to add such methods to ActiveRecord::BaseDecorator and call them through decorator_class. However, it didn't work as expected for models without decorator definitions:

class Foo < ActiveRecord::Base
end

class Bar < ActiveRecord::Base
end

class ActiveRecord::BaseDecorator < Draper::Decorator
  def self.sorted_column_names
    cols = object_class.column_names.sort
    %w(id).each do |c|
      cols.unshift(c) if cols.delete(c)
    end
    %w(created_at updated_at).each do |c|
      cols.push(c) if cols.delete(c)
    end
    cols
  end

  def common_instance_method
    # ....
  end
end

class FooDecorator < ActiveRecord::BaseDecorator
  def common_instance_method
    # Override common_instance_method for Foo
  end
end
irb(main):001:0> Foo.decorator_class
=> FooDecorator
irb(main):002:0> Foo.decorator_class.sorted_column_names
=> ["id", "foo_column", "created_at", "updated_at"]
irb(main):003:0> Bar.decorator_class
=> ActiveRecord::BaseDecorator
irb(main):004:0> Bar.decorator_class.sorted_column_names
NoMethodError: undefined method `abstract_class?' for Object:Class
...

I'd like to avoid defining decorator classes for all models explicitly.

yaegashi avatar Mar 09 '16 23:03 yaegashi

I've written a monkey patch in config/initializers/draper_patch.rb:

module Draper
  module Decoratable
    module ClassMethods
      def decorator_class
        prefix = respond_to?(:model_name) ? model_name : name
        decorator_name = "#{prefix}Decorator"
        decorator_name.constantize
      rescue NameError => error
        if superclass.respond_to?(:decorator_class)
          klass = Class.new(superclass.decorator_class)
          Object.const_set(decorator_name, klass)
        else
          raise unless error.missing_name?(decorator_name)
          raise Draper::UninferrableDecoratorError.new(self)
        end
      end
    end
  end
end

It creates a derived decorator class on the fly when model has no decorator class with the inferred name (BarDecorator), but its superclass has one (ActiveRecord::BaseDecorator):

irb(main):001:0> BarDecorator
NameError: uninitialized constant BarDecorator
...
irb(main):002:0> Bar.decorator_class
=> BarDecorator
irb(main):003:0> Bar.decorator_class.sorted_column_names
=> ["id", "bar_column", "created_at", "updated_at"]
irb(main):004:0> Bar.decorator_class.superclass
=> ActiveRecord::BaseDecorator

Could you support this as the standard feature of draper?

yaegashi avatar Mar 10 '16 03:03 yaegashi