# encoding: utf-8
require 'action_controller/base'
module Cells
module Cell
# == Basic overview
#
# A Cell is the central notion of the cells plugin. A cell acts as a
# lightweight controller in the sense that it will assign variables and
# render a view. Cells can be rendered from other cells as well as from
# regular controllers and views (see ActionView::Base#render_cell and
# ControllerMethods#render_cell)
#
# == A render_cell() cycle
#
# A typical render_cell state rendering cycle looks like this:
# render_cell :blog, :newest_article, {...}
# - an instance of the class BlogCell is created, and a hash containing
# arbitrary parameters is passed
# - the state method newest_article is executed and assigns instance
# variables to be used in the view
# - Usually the state method will call #render and return
# - #render will retrieve the corresponding view
# (e.g. app/cells/blog/newest_article.html. [erb|haml|...]),
# renders this template and returns the markup.
#
# == Design Principles
# A cell is a completely autonomous object and it should not know or have to know
# from what controller it is being rendered. For this reason, the controller's
# instance variables and params hash are not directly available from the cell or
# its views. This is not a bug, this is a feature! It means cells are truly
# reusable components which can be plugged in at any point in your application
# without having to think about what information is available at that point.
# When rendering a cell, you can explicitly pass variables to the cell in the
# extra opts argument hash, just like you would pass locals in partials.
# This hash is then available inside the cell as the @opts instance variable.
#
# == Directory hierarchy
#
# To get started creating your own cells, you can simply create a new directory
# structure under your app directory called cells. Cells are
# ruby classes which end in the name Cell. So for example, if you have a
# cell which manages all user information, it would be called UserCell.
# A cell which manages a shopping cart could be called ShoppingCartCell.
#
# The directory structure of this example would look like this:
# app/
# models/
# ..
# views/
# ..
# helpers/
# application_helper.rb
# product_helper.rb
# ..
# controllers/
# ..
# cells/
# shopping_cart_cell.rb
# shopping_cart/
# status.html.erb
# product_list.html.erb
# empty_prompt.html.erb
# user_cell.rb
# user/
# login.html.erb
# layouts/
# box.html.erb
# ..
#
# The directory with the same name as the cell contains views for the
# cell's states. A state is an executed method along with a
# rendered view, resulting in content. This means that states are to
# cells as actions are to controllers, so each state has its own view.
# The use of partials is deprecated with cells, it is better to just
# render a different state on the same cell (which also works recursively).
#
# Anyway, render :partial in a cell view will work, if the
# partial is contained in the cell's view directory.
#
# As can be seen above, Cells also can make use of helpers. All Cells
# include ApplicationHelper by default, but you can add additional helpers
# as well with the ::Cell::Base.helper class method:
# class ShoppingCartCell < ::Cell::Base
# helper :product
# ...
# end
#
# This will make the ProductHelper from app/helpers/product_helper.rb
# available from all state views from our ShoppingCartCell.
#
# == Cell inheritance
#
# Unlike controllers, Cells can form a class hierarchy. When a cell class
# is inherited by another cell class, its states are inherited as regular
# methods are, but also its views are inherited. Whenever a view is looked up,
# the view finder first looks for a file in the directory belonging to the
# current cell class, but if this is not found in the application or any
# engine, the superclass' directory is checked. This continues all the
# way up until it stops at ::Cell::Base.
#
# For instance, when you have two cells:
# class MenuCell < ::Cell::Base
# def show
# end
#
# def edit
# end
# end
#
# class MainMenuCell < MenuCell
# .. # no need to redefine show/edit if they do the same!
# end
# and the following directory structure in app/cells:
# app/cells/
# menu/
# show.html.erb
# edit.html.erb
# main_menu/
# show.html.erb
# then when you call
# render_cell :main_menu, :show
# the main menu specific show.html.erb (app/cells/main_menu/show.html.erb)
# is rendered, but when you call
# render_cell :main_menu, :edit
# cells notices that the main menu does not have a specific view for the
# edit state, so it will render the view for the parent class,
# app/cells/menu/edit.html.erb
#
#
# == Gettext support
#
# Cells support gettext, just name your views accordingly. It works exactly equivalent
# to controller views.
#
# cells/user/user_form.html.erb
# cells/user/user_form_de.html.erb
#
# If gettext is set to DE_de, the latter view will be chosen.
class Base
include ::ActionController::Helpers
include ::ActionController::RequestForgeryProtection
include ActiveHelper
class_inheritable_array :view_paths, :instance_writer => false
write_inheritable_attribute(:view_paths, ActionView::PathSet.new) # Force use of a PathSet in this attribute, self.view_paths = ActionView::PathSet.new would still yield in an array
class << self
attr_accessor :request_forgery_protection_token
# Use this if you want Cells to look up view templates
# in directories other than the default.
def view_paths=(paths)
self.view_paths.clear.concat(paths) # don't let 'em overwrite the PathSet.
end
# A template file will be looked for in each view path. This is typically
# just RAILS_ROOT/app/cells, but you might want to add e.g.
# RAILS_ROOT/app/views.
def add_view_path(path)
path = ::Rails.root.join(path) if defined?(::Rails)
self.view_paths << path unless self.view_paths.include?(path)
end
# Creates a cell instance of the class nameCell, passing through
# opts.
def create_cell_for(controller, name, opts={})
class_from_cell_name(name).new(controller, opts)
end
# Declare a controller method as a helper. For example,
# helper_method :link_to
# def link_to(name, options) ... end
# makes the link_to controller method available in the view.
def helper_method(*methods)
methods.flatten.each do |method|
master_helper_module.module_eval <<-end_eval
def #{method}(*args, &block)
@cell.send(:#{method}, *args, &block)
end
end_eval
end
end
# Return the default view for the given state on this cell subclass.
# This is a file with the name of the state under a directory with the
# name of the cell followed by a template extension.
def view_for_state(state)
"#{cell_name}/#{state}"
end
# Find a possible template for a cell's current state. It tries to find a
# template file with the name of the state under a subdirectory
# with the name of the cell under the app/cells directory.
# If this file cannot be found, it will try to call this method on
# the superclass. This way you only have to write a state template
# once when a more specific cell does not need to change anything in
# that view.
def find_class_view_for_state(state)
return [view_for_state(state)] if superclass == ::Cell::Base
superclass.find_class_view_for_state(state) << view_for_state(state)
end
# Get the name of this cell's class as an underscored string,
# with _cell removed.
#
# Example:
# UserCell.cell_name
# => "user"
def cell_name
name.underscore.sub(/_cell/, '')
end
# Given a cell name, finds the class that belongs to it.
#
# Example:
# ::Cell::Base.class_from_cell_name(:user)
# => UserCell
def class_from_cell_name(cell_name)
"#{cell_name}_cell".classify.constantize
end
def state2view_cache
@state2view_cache ||= {}
end
def cache_configured?
::ActionController::Base.cache_configured?
end
end
class_inheritable_accessor :allow_forgery_protection
self.allow_forgery_protection = true
class_inheritable_accessor :default_template_format
self.default_template_format = :html
delegate :params, :session, :request, :logger, :to => :controller
attr_accessor :controller
attr_reader :state_name
def initialize(controller, options={})
@controller = controller
@opts = options
end
def cell_name
self.class.cell_name
end
# Render the given state. You can pass the name as either a symbol or
# a string.
def render_state(state)
@cell = self
@state_name = state
content = dispatch_state(state)
return content if content.kind_of? String
render_view_for_backward_compat(content, state)
end
# Call the state method.
def dispatch_state(state)
send(state)
end
# We will soon remove the implicit call to render_view_for, but here it is for your convenience.
def render_view_for_backward_compat(opts, state)
::ActiveSupport::Deprecation.warn "You either didn't call #render or forgot to return a string in the state method '#{state}'. However, returning nil is deprecated for the sake of explicitness"
render_view_for(opts, state)
end
# Renders the view for the current state and returns the markup for the component.
# Usually called and returned at the end of a state method.
#
# ==== Options
# * :view - Specifies the name of the view file to render. Defaults to the current state name.
# * :template_format - Allows using a format different to :html.
# * :layout - If set to a valid filename inside your cell's view_paths, the current state view will be rendered inside the layout (as known from controller actions). Layouts should reside in app/cells/layouts.
# * :locals - Makes the named parameters available as variables in the view.
# * :text - Just renders plain text.
# * :inline - Renders an inline template as state view. See ActionView::Base#render for details.
# * :file - Specifies the name of the file template to render.
# * :nothing - Will make the component kinda invisible and doesn't invoke the rendering cycle.
# * :state - Instantly invokes another rendering cycle for the passed state and returns.
# Example:
# class MyCell < ::Cell::Base
# def my_first_state
# # ... do something
# render
# end
#
# will just render the view my_first_state.html.
#
# def my_first_state
# # ... do something
# render :view => :my_first_state, :layout => 'metal'
# end
#
# will also use the view my_first_state.html as template and even put it in the layout
# metal that's located at $RAILS_ROOT/app/cells/layouts/metal.html.erb.
#
# def say_your_name
# render :locals => {:name => "Nick"}
# end
#
# will make the variable +name+ available in the view say_your_name.html.
#
# def say_your_name
# render :nothing => true
# end
#
# will render an empty string thus keeping your name a secret.
#
#
# ==== Where have all the partials gone?
# In Cells we abandoned the term 'partial' in favor of plain 'views' - we don't need to distinguish
# between both terms. A cell view is both, a view and a kind of partial as it represents only a small
# part of the page.
# Just use :view and enjoy.
def render(opts={})
render_view_for(opts, @state_name) ### FIXME: i don't like the magic access to @state_name here. ugly!
end
# Render the view belonging to the given state. Will raise ActionView::MissingTemplate
# if it can not find one of the requested view template. Note that this behaviour was
# introduced in cells 2.3 and replaces the former warning message.
def render_view_for(opts, state)
return '' if opts[:nothing]
action_view = setup_action_view
### TODO: dispatch dynamically:
if opts[:text]
elsif opts[:inline]
elsif opts[:file]
elsif opts[:state]
opts[:text] = render_state(opts[:state])
else
# handle :layout, :template_format, :view
opts = defaultize_render_options_for(opts, state)
# set instance vars, include helpers:
prepare_action_view_for(action_view, opts)
template = find_family_view_for_state_with_caching(opts[:view], action_view)
opts[:file] = template
end
opts = sanitize_render_options(opts)
action_view.render_for(opts)
end
# Defaultize the passed options from #render.
def defaultize_render_options_for(opts, state)
opts[:template_format] ||= self.class.default_template_format
opts[:view] ||= state
opts
end
def prepare_action_view_for(action_view, opts)
# make helpers available:
include_helpers_in_class(action_view.class)
import_active_helpers_into(action_view) # in Cells::Cell::ActiveHelper.
action_view.assigns = assigns_for_view # make instance vars available.
action_view.template_format = opts[:template_format]
end
def setup_action_view
view_class = Class.new(::Cells::Cell::View)
action_view = view_class.new(self.class.view_paths, {}, @controller)
action_view.cell = self
action_view
end
# Prepares opts to be passed to ActionView::Base#render by removing
# unknown parameters.
def sanitize_render_options(opts)
opts.except!(:view, :state)
end
# Climbs up the inheritance hierarchy of the Cell, looking for a view
# for the current state in each level.
# As soon as a view file is found it is returned as an ActionView::Template
# instance.
### DISCUSS: moved to Cell::View#find_template in rainhead's fork:
def find_family_view_for_state(state, action_view)
missing_template_exception = nil
possible_paths_for_state(state).each do |template_path|
# we need to catch MissingTemplate, since we want to try for all possible
# family views.
begin
if view = action_view.try_picking_template_for_path(template_path)
return view
end
rescue ::ActionView::MissingTemplate => missing_template_exception
end
end
raise missing_template_exception
end
# In production mode, the view for a state/template_format is cached.
### DISCUSS: ActionView::Base already caches results for #pick_template, so maybe
### we should just cache the family path for a state/format?
def find_family_view_for_state_with_caching(state, action_view)
return find_family_view_for_state(state, action_view) unless self.class.cache_configured?
# in production mode:
key = "#{state}/#{action_view.template_format}"
state2view = self.class.state2view_cache
state2view[key] || state2view[key] = find_family_view_for_state(state, action_view)
end
# Find possible files that belong to the state. This first tries the cell's
# #view_for_state method and if that returns a true value, it
# will accept that value as a string and interpret it as a pathname for
# the view file. If it returns a falsy value, it will call the Cell's class
# method find_class_view_for_state to determine the file to check.
#
# You can override the ::Cell::Base#view_for_state method for a particular
# cell if you wish to make it decide dynamically what file to render.
def possible_paths_for_state(state)
self.class.find_class_view_for_state(state).reverse!
end
# Prepares the hash {instance_var => value, ...} that should be available
# in the ActionView when rendering the state view.
def assigns_for_view
assigns = {}
(self.instance_variables - ivars_to_ignore).each do |k|
assigns[k[1..-1]] = instance_variable_get(k)
end
assigns
end
# When passed a copy of the ActionView::Base class, it
# will mix in all helper classes for this cell in that class.
def include_helpers_in_class(view_klass)
view_klass.send(:include, self.class.master_helper_module)
end
# Defines the instance variables that should not be copied to the
# View instance.
def ivars_to_ignore; ['@controller']; end
### TODO: allow log levels.
def log(message)
return unless @controller.logger
@controller.logger.debug(message)
end
end
end
end