require 'cells' require 'onfire' require 'hooks' require 'apotomo/tree_node' require 'apotomo/event' require 'apotomo/event_methods' require 'apotomo/transition' require 'apotomo/caching' require 'apotomo/widget_shortcuts' require 'apotomo/rails/view_helper' module Apotomo class Widget < Cell::Base include Hooks # 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] define_hook :after_initialize define_hook :has_widgets define_hook :after_add attr_accessor :opts attr_writer :visible attr_writer :controller attr_accessor :version #class << self # include WidgetShortcuts #end include TreeNode include Onfire include EventMethods include Transition include Caching include WidgetShortcuts helper Apotomo::Rails::ViewHelper # Runs callbacks for +name+ hook in instance context. def run_widget_hook(name, *args) self.class.callbacks_for_hook(name).each { |blk| instance_exec(*args, &blk) } end def add_has_widgets_blocks(*) run_widget_hook(:has_widgets, 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 run_hook(:after_initialize, id, start_state, opts) 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 # Wraps the rendered content in a replace statement targeted at your +Apotomo.js_framework+ setting. # Use +:selector+ to change the selector. # # Example: # # Assuming you set # Apotomo.js_framework = :jquery # # and call replace in a state # # replace :view => :squeak, :selector => "div#mouse" # #=> "$(\"div#mouse\").replaceWith(\"