<commit_before>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.

"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.

Important note: These guys rock! :)

h2. Installation

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

h2. Description

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

h2. Feedback

Ask/Write <EMAIL> 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:
<pre><code>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</code></pre>
In the view model:
<pre><code>class ViewModels::User < ViewModels::Project
  model_reader :first_name, :last_name
  def full_name
    "#{last_name}, #{first_name}"
  end
end</code></pre>
In the model:
<pre><code>class User < ActiveRecord::Base
end</code></pre>
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@
Note: By convention, uses @ViewModels::Model::Class::Name@, thus prefixing @ViewModels::@ to the model class name.

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, format = nil)@.

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, :text@

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

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.

<pre>
<code>
class ViewModels::Project < ViewModels::Base
  
  # All of Rails' standard helpers.
  helper ActionView::Helpers::ActiveRecordHelper
  helper ActionView::Helpers::TagHelper
  helper ActionView::Helpers::FormTagHelper
  helper ActionView::Helpers::FormOptionsHelper
  helper ActionView::Helpers::FormHelper
  helper ActionView::Helpers::UrlHelper
  helper ActionView::Helpers::AssetTagHelper
  helper ActionView::Helpers::PrototypeHelper
  helper ActionView::Helpers::TextHelper
  
  helper ApplicationHelper
  
  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
</code>
</pre><commit_msg>+ README updated to reflect API changes<commit_after>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.

"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.

Important note: These guys rock! :)

h2. Installation

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

h2. Description

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

h2. Feedback

Ask/Write <EMAIL> 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:
<pre><code>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</code></pre>
In the view model:
<pre><code>class ViewModels::User < ViewModels::Project
  model_reader :first_name, :last_name
  def full_name
    "#{last_name}, #{first_name}"
  end
end</code></pre>
In the model:
<pre><code>class User < ActiveRecord::Base
end</code></pre>
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@
Note: By convention, uses @ViewModels::Model::Class::Name@, thus prefixing @ViewModels::@ to the model class name.

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

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

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.

<pre>
<code>
class ViewModels::Project < ViewModels::Base
  
  # All of Rails' standard helpers.
  helper ActionView::Helpers::ActiveRecordHelper
  helper ActionView::Helpers::TagHelper
  helper ActionView::Helpers::FormTagHelper
  helper ActionView::Helpers::FormOptionsHelper
  helper ActionView::Helpers::FormHelper
  helper ActionView::Helpers::UrlHelper
  helper ActionView::Helpers::AssetTagHelper
  helper ActionView::Helpers::PrototypeHelper
  helper ActionView::Helpers::TextHelper
  
  helper ApplicationHelper
  
  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
</code>
</pre>
{'fraction_non_alphanumeric': 0.08603318250377073, 'fraction_numerical': 0.003861236802413273, 'mean_word_length': 5.0014482259232445, 'pattern_counts': {'":': 8, '<': 57, '<?xml version=': 0, '>': 69, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4574641', 'n_tokens_mistral': 4987, 'n_tokens_neox': 4956, 'n_words': 1873}