h1. ViewModels for Rails h2. Previous contributors Code: "Kaspar":http://github.com/kschiess @http://github.com/kschiess@, for the first, and foundation-laying version. "Niko":http://github.com/niko @http://github.com/niko@, for handling collections and better Helpers handling. "Andi":http://github.com/andi @http://github.com/andi@, for refactoring it into a cleaner structure. Inspiration: "Severin":http://github.com/severin @http://github.com/severin@, for bits and pieces here and there. "rainhead":http://github.com/rainhead @http://github.com/rainhead@, for the idea to subclass ActionView::Base. Important note: These guys rock! :) h2. Installation h3. Rails Plugin @script/plugin install git://github.com/floere/view_models.git@ h3. Gem for use in Rails @gem install view_models@ and then adding the line @config.gem 'view_models'@ in your environment.rb. h2. Description A possible view_model solution, i.e. no view logic in model code. h2. Feedback Ask/Write florian.hanke@gmail.com if you have questions/feedback, thanks! :) Fork if you have improvements. Send me a pull request, it is much appreciated. h2. Problem Display Methods are not well placed either in * models: Violation of the MVC principle. * helpers: No Polymorphism. h2. Solution A thin proxy layer over a model, with access to the controller, used by the view or controller. @The view@ -> to @the view model@ which in turn -> @the model@ and -> @the controller@ h2. Examples & What you can do h3. A quick one In the view:
user = view_model_for @user
%h1= "You, #{user.full_name}, the user"
%h2 This is how you look as a search result:
= user.render_as :result
%h2 This is how you look as a Vcard:
= user.render_as :vcard
In the view model:
class ViewModels::User < ViewModels::Project
  model_reader :first_name, :last_name
  def full_name
    "#{last_name}, #{first_name}"
  end
end
In the model:
class User < ActiveRecord::Base
end
Also, there are two partials in @app/views/view_models/user/@, @_result.html.haml@ and @_vcard.html.haml@ that define how the result of @user.render_as :result@ and @user.render_as :vcard@ look. The ViewModel can be accessed inside the view by using the local variable @view_model@. h3. Getting a view_model in a view or a controller. Call view_model_for: @view_model_instance = view_model_for model_instance@ By convention, uses @ViewModels::Model::Class::Name@, thus prefixing @ViewModels::@ to the model class name. Note: You can override @specific_view_model_class_for@ to change the mapping of model to class, or make it dynamic. h3. Getting a collection view model in a view. The collection view_model renders each of the given items with its view_model. Call collection_view_model_for: @collection_view_model_instance = collection_view_model_for enumerable_containing_model_instances@ Rendering a list: @collection_view_model_instance.list@ Rendering a collection: @collection_view_model_instance.collection@ Rendering a table: @collection_view_model_instance.table@ Rendering a pagination: @collection_view_model_instance.pagination@ Note: Only works if the passed parameter for @collection_view_model_for@ is a @PaginationEnumeration@. Important note: As of yet it is needed to copy the templates/views/view_models/collection directory to the corresponding location in app/views/view_models/collection. This is only needed if you wish to use the collection view model. The collections are automatically copied if you use the generator. Note: Rewrite the collection templates as needed, they are rather basic. h3. Writing filtered delegate methods on the view model. Will create two delegate methods first_name and last_name that delegate to the model: @model_reader :first_name, :last_name@ Will create a description delegate method that filters the model value through h: @model_reader :description, :filter_through => :h@ Will create a description delegate method that filters the model value through first textilize, then h: @model_reader :description, :filter_through => [:textilize, :h]@ Will create both a first_name and last_name delegate method that filters the model value through first textilize, then h: @model_reader :first_name, :last_name, :filter_through => [:textilize, :h]@ Note: Filter methods can be any method on the view_model with arity 1. h3. Rendering view model templates Use @render_as(template_name, options)@. Gets a @ViewModels::Model::Class@ instance: @view_model = view_model_for Model::Class.new@ Gets a @ViewModels::@ instance: @view_model = view_model_for model_instance@ Renders the 'example' partial in view_models/model/class: @view_model.render_as :example@ Note: Renders a format depending on the request. ../index.text will render example.text.erb. Renders the 'example.text.erb' partial in view_models/model/class: @view_model.render_as :example, :format => :text@ Note: If the partial cannot be found, it will traverse the view model hierarchy upwards to find a partial template. Locals can be passed through as usual: @view_model.render_as :example, :format => :text, :locals => { :name => value }@ h3. Rails Helpers in ViewModels Use @helper@ as you would in the controller. @helper ActionView::Helpers::UrlHelper@ @helper ApplicationHelper@ Note: It is helpful to create a superclass to all view models in the project with generally used helpers. We use @ViewModels::Project@ a lot, for example. See example below. h3. Controller Delegate Methods Use @controller_method(*args)@. Delegates current_user and logger on the view_model to the controller: @controller_method :current_user, :logger@ h2. The Generator Generates view model class, spec, and views. Use as follows: @script/generate view_models @ @script/generate view_models User@ @script/generate view_models @ @script/generate view_models User compact extensive list_item table_item@ h2. One Big Fat Example The following classes all have specs of course ;) But are not shown since they don't help the example. @ViewModels@ superclass for this project. We include all of Rails' helpers for the view models in this project. Also, we include the @ApplicationHelper@. We delegate @logger@ and @current_user@ calls in the view models to the active controller.

class ViewModels::Project < ViewModels::Base
  
  # Our ApplicationHelper.
  #
  helper ApplicationHelper
  
  # We want to be able to call view_model_for in our view_models.
  #
  helper ViewModels::Helpers::Rails
  
  # Include all common view helpers.
  #
  helper ViewModels::Helpers::View
  
  # We want to be able to use id, dom_id, to_param on the view model.
  #
  # Note: Overrides the standard dom_id method from the RecordIdentificationHelper.
  #
  include ViewModels::Extensions::ActiveRecord
  
  controller_method :logger, :current_user
  
end

# All items have a description that needs to be filtered by textilize.
#
class ViewModels::Item < ViewModels::Project
  model_reader :description, :filter_through => :textilize
  # Use price in the view as follows:
  # = view_model.price - will display e.g. 16.57 CHF, since it is filtered first through localize_currency
  model_reader :price, :filter_through => :localize_currency
  
  # Converts a database price tag to the users chosen value, with the users preferred currency appended.
  # If the user is Swiss, localize_currency will convert 10 Euros to "16.57 CHF" 
  #
  def localize_currency(price_in_euros)
    converted_price = current_user.convert_price(price_in_euros)
    "#{converted_price} #{current_user.currency.to_s}"
  end
end

# This class also has partial templates in the directory
#   app/views/view_models/book
# that are called
#   _cart_item.html.haml
#   _cart_item.text.erb
#
# Call view_model_for on a book in the view or controller to get this view_model.
#
class ViewModels::Book < ViewModels::Item
  model_reader :author, :title, :pages
  model_reader :excerpt, :filter_through => :textilize
  
  def header
    content_tag(:h1, "#{author} – #{title}")
  end
  
  def full_description
    content_tag(:p, "#{excerpt} #{description}", :class => 'description full')
  end
end

# This class also has partial templates in the directory
#   app/views/view_models/toy
# that are called
#   _cart_item.html.haml
#   _cart_item.text.erb
#
# Call view_model_for on a toy in the view or controller to get this view_model.
#
class ViewModels::Toy < ViewModels::Item
  model_reader :starting_age, :small_dangerous_parts
  
  def obligatory_parental_warning
    "Warning, this toy can only be used by kids ages #{starting_age} and up. Your department of health. Thank you."
  end
  
end