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::
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