artemave
artemave

Reputation: 6946

conditional view/layout

Say there is a product controller which you want to have an index (list products) action. Easy. Now say you have an admin and store parts in your project. Both need to list products, but in a slightly different manner (store's one shouldn't have this edit product link for instance). They also use different layouts.

So far my idea is to have two product controllers under different namespaces - app/controllers/admin/products_controller.rb and app/controllers/store/products_controller.rb - each then having its own views and layouts. But I suspect this may lead to WET code. Or to references to other controller views (which imo breaks modularity and hence should be avoided).

So, the actual question: is there a more DRY (or in fact proper) way to achieve the above?

I'm not sure the title actually reflects the question. But, on the other hand, if it were, I could probably google the answer.

EDIT As of 3.1, Rails supports template inheritance.

Upvotes: 2

Views: 3937

Answers (4)

artemave
artemave

Reputation: 6946

If only there were some kind of view inheritance... So that one can subclass controller without need to supply all its views. Good thing is that there is this patch. Bad thing is that it can't make it to the core for quite a while.

Having applied it to my rails 2.2, I managed to have the following answer to the original question.

Subclassing controller

ProductController has been blessed with the twins:

class Products::AdminController < ProductsController
  layout 'admin'
  before_filter :authenticate
end

and

class Products::StoreController < ProductsController
  layout 'store'
  before_filter :find_cart
end

This itself looks quite nice since each of them as well carries its own initialization part.

Changing routes

  map.resources :products, :controller => 'products/admin', :path_prefix => 'admin',
    :name_prefix => 'admin_'
  map.resources :products, :controller => 'products/store', :path_prefix => 'store',
    :only => [:show, :index], :name_prefix => 'store_'

Not an easy route, defo. But, hey, after this point everything just works (assuming you fixed path helpers) with ProductController views and partials.

Shared views changes

Each subclass controller has its own version of index.html.erb. Everything else is shared in a base class.

Speaking about path helpers in shared templates. What used to be

<% form_for @product ... %>

becomes

<% form_for [controller_name, @product] ... %>

and thins like

<%= link_to products_path %>

turn into

<%= link_to send("#{controller_name}_products_path") %>

I don't know if it is all worth it, but that is a way. Anyone knows why if there are plans to include this patch in rails soon?

Upvotes: 0

If the way you're displaying products between the admin section and the store section is constant except for the admin links (Create, Edit, Destroy), then I think it would be easiest to create a partial for your product. I assume you have a way of telling whether the user is an admin or not (I'll just use admin? for simplicity below). Inside your partial you do something like this...

<div class="product">
    <div class="productheader">
        <%=h product.title %>
    </div>
    <div class="productdescription>
        <%=h product.description %>
    </div>
    <% if admin? %>
    <div class="productadmin">
        <%= link_to "Delete", destroy_product_url %>
        <%= link_to "Edit", edit_product_url %>
    </div>
    <% end %>
</div>

Be sure to name this partial _product.html.erb (the underscore tells rails that the template is a partial). Create a folder in the app/views directory of your application called shared and store the partial there.

To render this partial in your other views, simply call the render method and pass the partial parameter.

A single product:

<%= render(:partial => "shared/product", :object => @a_product) %>

Multiple products:

<%= render(:partial => "shared/product", :collection => @products) %>

Layouts can be applied to partials by adding the layout parameter. Partial layouts must be prefixed with the underscore but stored in the app/views directory associated with the controller.

<%= render(:partial => "shared/product", :object => @a_product, :layout => "somelayout" %>

Upvotes: 2

tvanfosson
tvanfosson

Reputation: 532545

The approach that I take is to have a single controller for products and add code to it to detect the role that the user plays and conditionally set view data based on that role. This includes both actual model data and data used only by the view to determine which bits of the interface to display. The view itself, then, contains some small amount of code that is able to act on the role-based data and render only those bits that are relevant to the particular role. One might argue that this is injecting either some small bit of business logic into the view or some small bit of display logic into the controller -- and those arguments have some validity. However, I find that it's really more of a balancing act between principles and I prefer value DRY over MVC-purity.

Upvotes: 1

tpdi
tpdi

Reputation: 35151

You're describing the Model-View-Controller Pattern, in which models views and controllers can vary orthogonally (or more or less orthogonally, depending on how its implemented).

Very basically, you have one View that allows edits and one that doesn't. Again, depending on implementation, the editable view may derived form the uneditable view. In either case, either the controller or some higher-level code will conditionally chose the right view.

Upvotes: 0

Related Questions