Code:
Kaspar http://github.com/kschiess
, for the first, and foundation-laying version.
Niko http://github.com/niko
, for handling collections and better Helpers handling.
Andi http://github.com/andi
, for refactoring it into a cleaner structure.
Inspiration:
Severin http://github.com/severin
, for bits and pieces here and there.
rainhead http://github.com/rainhead
, for the idea to subclass ActionView::Base.
Important note: These guys rock! :)
script/plugin install git://github.com/floere/view_models.git
gem install view_models
and then adding the line
config.gem 'view_models'
in your environment.rb.
A possible view_model solution, i.e. no view logic in model code.
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.
Display Methods are not well placed either in
- models: Violation of the MVC principle.
- helpers: No Polymorphism.
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
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
.
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.
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.
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.
Use render_as(template_name, options)
.
Gets a ViewModels::Model::Class
instance: view_model = view_model_for Model::Class.new
Gets a ViewModels::<model_instance.class.name>
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 }
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.
Use controller_method(*args)
.
Delegates current_user and logger on the view_model to the controller: controller_method :current_user, :logger
Generates view model class, spec, and views. Use as follows:
script/generate view_models <view model class name>
script/generate view_models User
script/generate view_models <view model class name> <partials for render_as, separated by space>
script/generate view_models User compact extensive list_item table_item
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