require 'onfire'
require 'apotomo/tree_node'
require 'apotomo/event'
require 'apotomo/event_methods'
require 'apotomo/transition'
require 'apotomo/caching'
require 'apotomo/deep_link_methods'
require 'apotomo/widget_shortcuts'
require 'apotomo/rails/view_helper'
### TODO: use load_hooks when switching to rails 3.
# wycats@gmail.com: ActiveSupport.run_load_hooks(:name)
# (21:01:17) wycats@gmail.com: ActiveSupport.on_load(:name) { … }
#require 'active_support/lazy_load_hooks'
module Apotomo
class Widget < Cell::Base
class_inheritable_array :initialize_hooks, :instance_writer => false
self.initialize_hooks = []
attr_accessor :opts
attr_writer :visible
attr_writer :controller
attr_accessor :version
### DISCUSS: extract to has_widgets_methods for both Widget and Controller?
#class_inheritable_array :has_widgets_blocks
class << self
include WidgetShortcuts
def has_widgets_blocks
@has_widgets_blocks ||= []
end
def has_widgets(&block)
has_widgets_blocks << block
end
# Use this for setup code you're calling in every state. Almost like a +before_filter+ except that it's
# invoked after the initialization in #has_widgets.
#
# Example:
#
# class MouseWidget < Apotomo::Widget
# after_initialize :setup_cheese
#
# # we need @cheese in every state:
# def setup_cheese(*)
# @cheese = Cheese.find @opts[:cheese_id]
def after_initialize(method)
self.initialize_hooks << method
end
end
include TreeNode
include Onfire
include EventMethods
include Transition
include Caching
include DeepLinkMethods
include WidgetShortcuts
helper Apotomo::Rails::ViewHelper
def add_has_widgets_blocks(*)
self.class.has_widgets_blocks.each { |block| block.call(self) }
end
after_initialize :add_has_widgets_blocks
# Constructor which needs a unique id for the widget and one or multiple start states.
# start_state may be a symbol or an array of symbols.
def initialize(id, start_state, opts={})
@opts = opts
@name = id
@start_state = start_state
@visible = true
@version = 0
@cell = self
process_initialize_hooks(id, start_state, opts)
end
def process_initialize_hooks(*args)
self.class.initialize_hooks.each { |method| send(method, *args) }
end
def last_state
@state_name
end
def visible?
@visible
end
# Defines the instance vars that should not survive between requests,
# which means they're not frozen in Apotomo::StatefulWidget#freeze.
def ivars_to_forget
unfreezable_ivars
end
def unfreezable_ivars
[:@childrenHash, :@children, :@parent, :@controller, :@cell, :@invoke_block, :@rendered_children, :@page_updates, :@opts,
:@suppress_javascript ### FIXME: implement with ActiveHelper and :locals.
]
end
# Defines the instance vars which should not be copied to the view.
# Called in Cell::Base.
def ivars_to_ignore
[]
end
### FIXME:
def logger; self; end
def debug(*args); puts args; end
# Returns the rendered content for the widget by running the state method for state.
# This might lead us to some other state since the state method could call #jump_to_state.
def invoke(state=nil, &block)
@invoke_block = block ### DISCUSS: store block so we don't have to pass it 10 times?
logger.debug "\ninvoke on #{name} with #{state.inspect}"
if state.blank?
state = next_state_for(last_state) || @start_state
end
logger.debug "#{name}: transition: #{last_state} to #{state}"
logger.debug " ...#{state}"
render_state(state)
end
# called in Cell::Base#render_state
def dispatch_state(state)
send(state, &@invoke_block)
end
# Render the view for the current state. Usually called 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.
# * :render_children - If false, automatic rendering of child widgets is turned off. Defaults to true.
# * :invoke - Explicitly define the state to be invoked on a child when rendering.
# * see Cell::Base#render for additional options
#
# Note that :text => ... and :update => true will turn off :frame.
#
# Example:
# class MouseCell < Apotomo::StatefulWidget
# def eating
# # ... do something
# render
# end
#
# will just render the view eating.html.
#
# def eating
# # ... do something
# render :view => :bored, :layout => "metal"
# end
#
# will use the view bored.html as template and even put it in the layout
# metal that's located at $RAILS_ROOT/app/cells/layouts/metal.html.erb.
#
# render :js => "alert('SQUEAK!');"
#
# issues a squeaking alert dialog on the page.
def render(options={}, &block)
if options[:nothing]
return ""
end
if options[:text]
options.reverse_merge!(:render_children => false)
end
options.reverse_merge! :render_children => true,
:locals => {},
:invoke => {},
:suppress_js => false
rendered_children = render_children_for(options)
options[:locals].reverse_merge!(:rendered_children => rendered_children)
@controller = controller # that dependency SUCKS.
@suppress_js = options[:suppress_js] ### FIXME: implement with ActiveHelper and :locals.
render_view_for(options, @state_name) # defined in Cell::Base.
end
alias_method :emit, :render
def replace(options={})
content = render(options)
Apotomo.js_generator.replace(self.name, content)
end
def update(options={})
content = render(options)
Apotomo.js_generator.update(self.name, content)
end
# Force the FSM to go into state, regardless whether it's a valid
# transition or not.
### TODO: document the need for return.
def jump_to_state(state)
logger.debug "STATE JUMP! to #{state}"
render_state(state)
end
def visible_children
children.find_all { |kid| kid.visible? }
end
def render_children_for(options)
return {} unless options[:render_children]
render_children(options[:invoke])
end
def render_children(invoke_options={})
returning rendered_children = ActiveSupport::OrderedHash.new do
visible_children.each do |kid|
child_state = decide_state_for(kid, invoke_options)
logger.debug " #{kid.name} -> #{child_state}"
rendered_children[kid.name] = render_child(kid, child_state)
end
end
end
def render_child(cell, state)
cell.invoke(state)
end
def decide_state_for(child, invoke_options)
invoke_options.stringify_keys[child.name.to_s]
end
### DISCUSS: use #param only for accessing request data.
def param(name)
params[name]
end
# Returns the address hash to the event controller and the targeted widget.
#
# Reserved options for way:
# :source explicitly specifies an event source.
# The default is to take the current widget as source.
# :type specifies the event type.
#
# Any other option will be directly passed into the address hash and is
# available via StatefulWidget#param in the widget.
#
# Can be passed to #url_for.
#
# Example:
# address_for_event :type => :squeak, :volume => 9
# will result in an address that triggers a :click event from the current
# widget and also provides the parameter :item_id.
def address_for_event(options)
raise "please specify the event :type" unless options[:type]
options[:source] ||= self.name
options
end
# Returns the widget named widget_id as long as it is below self or self itself.
def find_widget(widget_id)
find {|node| node.name.to_s == widget_id.to_s}
end
def controller
root? ? @controller : root.controller
end
end
end