Using Tire with Draper

mattiassvedhem edited this page Mar 28, 2013 · 33 revisions

There's many reasons for wanting to augment either the Collection or the Item returned by Tire.

For example:

  1. Add additional view logic that you don't want to put in a helper
  2. Reuse view logic between ActiveRecord/Mongoid & Tire.
  3. Just to add additional methods to the collection or item.

You can do this using Draper which provides View-Models/Decorators.

There's two ways to go about it.

1. If we don't need to access additional methods on the collection

We could define just a ordinary Draper::Decorator:

class Article < ActiveRecord::Base
  include Tire::Model::Search
  include Tire::Model::Callbacks
end
class ArticleDecorator < Draper::Decorator
  # We can either delegate all methods not defined in our decorator
  #   or just specific methods that we want to pass on to the Tire::Results::Item instance. 
  delegate_all

  def caption
    source.title + '-' + source.description
  end
end

We can then use it like this

articles = Article.search(query: 'Draper')
decorated_articles = ArticleDecorator.decorate_collection(articles)

That would give us an instance of Draper::CollectionDecorator which is the default collection decorator in Draper. So we could then do:

decorated_articles.first.caption
=> 'Using Tire with Draper - To augment Collection and Item'

# We also still have the methods from Tire::Results::Item at arms length 
#   since we used delegate_all in our ArticleDecorator.
decorated_articles.first._score
=> '0.30685282'

However, what we cannot do with this approach is to get to any facets or other methods defined on the Tire::Results::Collection object.

decorated_articles.facets
=> NoMethodError: undefined method `facets' for #<Draper::CollectionDecorator:0x007f836d9888f0>

That's because the default collection decorator does not automatically delegate those methods to the source Tire::Search::Collection instance. So if we don't need to access those properties, we are fine with this implementation.

2. If we need to access methods on the collection (or define new ones), for example facets.

We also need to define a Draper::CollectionDecorator

ArticlesDecorator < Draper::CollectionDecorator
  # There's no delegate_all here so we'll need to delegate each 
  #   methods we want to pass on to the Tire::Search::Collection
  delegate :facets, :total

  def some_method_that_deals_with_the_collection
    # we can access the Tire::Search::Collection instance via the method `source`.
    'something'
  end
end

We can now use this collection decorator directly to decorate both the collection and the items. Draper will automatically look for a decorator named ArticleDecorator, loop through our hits and wrap them with it.

articles = Article.search(query: 'Draper')
decorated_articles = ArticlesDecorator.new(articles)

# We can now access the method we delegated
decorated_articles.facets
=> { hash with the facets }

# Or the ones we defined in our ArticlesDecorator
decorated_articles.some_method_that_deals_with_the_collection
=> 'something'

# Each of our items are decorated with ArticleDecorator
decorated_articles.first.caption
=> 'Using Tire with Draper - To augment Collection and Item'

# We also still have the methods from Tire::Results::Item at arms length 
#   since we used delegate_all in our ArticleDecorator.
decorated_articles.first._score
=> '0.30685282'

Alternatively

If we don't need to add any methods to the collection, but we do want to access the facets (or other methods), we can define just one collection decorator to use with Tire. We can reuse this collection decorator for multiple Tire enabled models.

class ResultsDecorator < Draper::CollectionDecorator
  delegate :facets
end

# We can then specify which item decorator we want to use for each item.
articles = Article.search(query: 'Draper')
decorated_articles = ResultsDecorator.new(articles, with: ArticleDecorator)

We can now reuse our view models between Tire and ActiveRecord/Mongoid, as long as they adhere to the same interface (attributes).

Decorating embedded nodes

Sometimes you embed a child node in your documents, as Tire automatically wraps them with the Item class, you can also decorate them easily.

For example, if we embed a single Article in a Blog document, we can use the same ArticleDecorator that we used when interacting with the article.

class BlogDecorator < Draper::Decorator
  decorates_association :article, with: ArticleDecorator
end
blogs = Blog.search(query: 'The blog')
decorated_blogs = BlogsDecorator.new(blogs)
decorated_article = decorated_blogs.first.article

# Our embedded Article ´Item´ is now decorated with our ArticleDecorator
decorated_article.caption
=> 'Using Tire with Draper - To augment Collection and Item'

Now, the with: option is needed since Draper can not perform an automatic lookup here.

For more information on draper, read the Draper readme