Skip to content

Latest commit

 

History

History
254 lines (172 loc) · 8.64 KB

README.textile

File metadata and controls

254 lines (172 loc) · 8.64 KB

ViewModels for Rails

Previous contributors

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

Installation

Rails Plugin

script/plugin install git://github.com/floere/view_models.git

Gem for use in Rails

gem install view_models

and then adding the line

config.gem 'view_models'

in your environment.rb.

Description

A possible view_model solution, i.e. no view logic in model code.

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.

Problem

Display Methods are not well placed either in

  • models: Violation of the MVC principle.
  • helpers: No Polymorphism.

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

Examples & What you can do

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.

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.

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.

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.

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

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.

Controller Delegate Methods

Use controller_method(*args).

Delegates current_user and logger on the view_model to the controller: controller_method :current_user, :logger

The Generator

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

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