apotomo 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/Gemfile +10 -0
  2. data/Gemfile.lock +47 -0
  3. data/README +141 -0
  4. data/README.rdoc +141 -0
  5. data/Rakefile +78 -0
  6. data/TODO +36 -0
  7. data/app/cells/apotomo/child_switch_widget/switch.html.erb +1 -0
  8. data/app/cells/apotomo/child_switch_widget/switch.rhtml +1 -0
  9. data/app/cells/apotomo/deep_link_widget.rb +27 -0
  10. data/app/cells/apotomo/deep_link_widget/setup.html.erb +20 -0
  11. data/app/cells/apotomo/java_script_widget.rb +12 -0
  12. data/app/cells/apotomo/tab_panel_widget.rb +87 -0
  13. data/app/cells/apotomo/tab_panel_widget/display.html.erb +57 -0
  14. data/app/cells/apotomo/tab_widget.rb +18 -0
  15. data/app/cells/apotomo/tab_widget/display.html.erb +1 -0
  16. data/config/routes.rb +3 -0
  17. data/generators/widget/USAGE +15 -0
  18. data/generators/widget/templates/functional_test.rb +8 -0
  19. data/generators/widget/templates/view.html.erb +2 -0
  20. data/generators/widget/templates/view.html.haml +3 -0
  21. data/generators/widget/templates/widget.rb +8 -0
  22. data/generators/widget/widget_generator.rb +34 -0
  23. data/lib/apotomo.rb +59 -0
  24. data/lib/apotomo/caching.rb +37 -0
  25. data/lib/apotomo/container_widget.rb +10 -0
  26. data/lib/apotomo/deep_link_methods.rb +90 -0
  27. data/lib/apotomo/event.rb +9 -0
  28. data/lib/apotomo/event_handler.rb +23 -0
  29. data/lib/apotomo/event_methods.rb +102 -0
  30. data/lib/apotomo/invoke_event_handler.rb +24 -0
  31. data/lib/apotomo/javascript_generator.rb +57 -0
  32. data/lib/apotomo/persistence.rb +139 -0
  33. data/lib/apotomo/proc_event_handler.rb +18 -0
  34. data/lib/apotomo/rails/controller_methods.rb +161 -0
  35. data/lib/apotomo/rails/view_helper.rb +95 -0
  36. data/lib/apotomo/rails/view_methods.rb +7 -0
  37. data/lib/apotomo/request_processor.rb +92 -0
  38. data/lib/apotomo/stateful_widget.rb +8 -0
  39. data/lib/apotomo/transition.rb +46 -0
  40. data/lib/apotomo/tree_node.rb +186 -0
  41. data/lib/apotomo/version.rb +5 -0
  42. data/lib/apotomo/widget.rb +289 -0
  43. data/lib/apotomo/widget_shortcuts.rb +36 -0
  44. data/rails/init.rb +0 -0
  45. data/test/fixtures/application_widget_tree.rb +2 -0
  46. data/test/rails/controller_methods_test.rb +206 -0
  47. data/test/rails/rails_integration_test.rb +99 -0
  48. data/test/rails/view_helper_test.rb +77 -0
  49. data/test/rails/view_methods_test.rb +40 -0
  50. data/test/rails/widget_generator_test.rb +47 -0
  51. data/test/support/assertions_helper.rb +13 -0
  52. data/test/support/test_case_methods.rb +68 -0
  53. data/test/test_helper.rb +77 -0
  54. data/test/unit/apotomo_test.rb +20 -0
  55. data/test/unit/container_test.rb +20 -0
  56. data/test/unit/event_handler_test.rb +67 -0
  57. data/test/unit/event_methods_test.rb +83 -0
  58. data/test/unit/event_test.rb +30 -0
  59. data/test/unit/invoke_test.rb +123 -0
  60. data/test/unit/javascript_generator_test.rb +90 -0
  61. data/test/unit/onfire_integration_test.rb +19 -0
  62. data/test/unit/persistence_test.rb +240 -0
  63. data/test/unit/render_test.rb +203 -0
  64. data/test/unit/request_processor_test.rb +178 -0
  65. data/test/unit/stateful_widget_test.rb +135 -0
  66. data/test/unit/test_addressing.rb +111 -0
  67. data/test/unit/test_caching.rb +54 -0
  68. data/test/unit/test_jump_to_state.rb +89 -0
  69. data/test/unit/test_tab_panel.rb +72 -0
  70. data/test/unit/test_widget_shortcuts.rb +45 -0
  71. data/test/unit/transition_test.rb +33 -0
  72. data/test/unit/widget_shortcuts_test.rb +68 -0
  73. data/test/unit/widget_test.rb +24 -0
  74. metadata +215 -0
@@ -0,0 +1,289 @@
1
+ require 'onfire'
2
+ require 'apotomo/tree_node'
3
+
4
+
5
+ require 'apotomo/event'
6
+ require 'apotomo/event_methods'
7
+ require 'apotomo/transition'
8
+ require 'apotomo/caching'
9
+ require 'apotomo/deep_link_methods'
10
+ require 'apotomo/widget_shortcuts'
11
+ require 'apotomo/rails/view_helper'
12
+
13
+ ### TODO: use load_hooks when switching to rails 3.
14
+ # wycats@gmail.com: ActiveSupport.run_load_hooks(:name)
15
+ # (21:01:17) wycats@gmail.com: ActiveSupport.on_load(:name) { … }
16
+ #require 'active_support/lazy_load_hooks'
17
+
18
+ module Apotomo
19
+ class Widget < Cell::Base
20
+
21
+ class_inheritable_array :initialize_hooks, :instance_writer => false
22
+ self.initialize_hooks = []
23
+
24
+ attr_accessor :opts
25
+ attr_writer :visible
26
+
27
+ include TreeNode
28
+
29
+
30
+ include Onfire
31
+ include EventMethods
32
+
33
+ include Transition
34
+ include Caching
35
+
36
+ include DeepLinkMethods
37
+ include WidgetShortcuts
38
+
39
+ helper Apotomo::Rails::ViewHelper
40
+
41
+
42
+ attr_writer :controller
43
+ attr_accessor :version
44
+
45
+ ### DISCUSS: extract to has_widgets_methods for both Widget and Controller?
46
+ #class_inheritable_array :has_widgets_blocks
47
+
48
+ class << self
49
+ include WidgetShortcuts
50
+
51
+ def has_widgets_blocks
52
+ @has_widgets_blocks ||= []
53
+ end
54
+
55
+ def has_widgets(&block)
56
+ has_widgets_blocks << block
57
+ end
58
+ end
59
+ self.initialize_hooks << :add_has_widgets_blocks
60
+
61
+ def add_has_widgets_blocks(*)
62
+ self.class.has_widgets_blocks.each { |block| block.call(self) }
63
+ end
64
+
65
+
66
+ # Constructor which needs a unique id for the widget and one or multiple start states.
67
+ # <tt>start_state</tt> may be a symbol or an array of symbols.
68
+ def initialize(id, start_state, opts={})
69
+ @opts = opts
70
+ @name = id
71
+ @start_state = start_state
72
+
73
+ @visible = true
74
+ @version = 0
75
+
76
+ @cell = self
77
+
78
+ process_initialize_hooks(id, start_state, opts)
79
+ end
80
+
81
+ def process_initialize_hooks(*args)
82
+ self.class.initialize_hooks.each { |method| send(method, *args) }
83
+ end
84
+
85
+ def last_state
86
+ @state_name
87
+ end
88
+
89
+ def visible?
90
+ @visible
91
+ end
92
+
93
+ # Defines the instance vars that should <em>not</em> survive between requests,
94
+ # which means they're not frozen in Apotomo::StatefulWidget#freeze.
95
+ def ivars_to_forget
96
+ unfreezable_ivars
97
+ end
98
+
99
+ def unfreezable_ivars
100
+ [:@childrenHash, :@children, :@parent, :@controller, :@cell, :@invoke_block, :@rendered_children, :@page_updates, :@opts,
101
+ :@suppress_javascript ### FIXME: implement with ActiveHelper and :locals.
102
+
103
+ ]
104
+ end
105
+
106
+ # Defines the instance vars which should <em>not</em> be copied to the view.
107
+ # Called in Cell::Base.
108
+ def ivars_to_ignore
109
+ []
110
+ end
111
+
112
+ ### FIXME:
113
+ def logger; self; end
114
+ def debug(*args); puts args; end
115
+
116
+ # Returns the rendered content for the widget by running the state method for <tt>state</tt>.
117
+ # This might lead us to some other state since the state method could call #jump_to_state.
118
+ def invoke(state=nil, &block)
119
+ @invoke_block = block ### DISCUSS: store block so we don't have to pass it 10 times?
120
+ logger.debug "\ninvoke on #{name} with #{state.inspect}"
121
+
122
+ if state.blank?
123
+ state = next_state_for(last_state) || @start_state
124
+ end
125
+
126
+ logger.debug "#{name}: transition: #{last_state} to #{state}"
127
+ logger.debug " ...#{state}"
128
+
129
+ render_state(state)
130
+ end
131
+
132
+
133
+
134
+ # called in Cell::Base#render_state
135
+ def dispatch_state(state)
136
+ send(state, &@invoke_block)
137
+ end
138
+
139
+
140
+ # Render the view for the current state. Usually called at the end of a state method.
141
+ #
142
+ # ==== Options
143
+ # * <tt>:view</tt> - Specifies the name of the view file to render. Defaults to the current state name.
144
+ # * <tt>:template_format</tt> - Allows using a format different to <tt>:html</tt>.
145
+ # * <tt>:layout</tt> - 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 <tt>app/cells/layouts</tt>.
146
+ # * <tt>:render_children</tt> - If false, automatic rendering of child widgets is turned off. Defaults to true.
147
+ # * <tt>:invoke</tt> - Explicitly define the state to be invoked on a child when rendering.
148
+ # * see Cell::Base#render for additional options
149
+ #
150
+ # Note that <tt>:text => ...</tt> and <tt>:update => true</tt> will turn off <tt>:frame</tt>.
151
+ #
152
+ # Example:
153
+ # class MouseCell < Apotomo::StatefulWidget
154
+ # def eating
155
+ # # ... do something
156
+ # render
157
+ # end
158
+ #
159
+ # will just render the view <tt>eating.html</tt>.
160
+ #
161
+ # def eating
162
+ # # ... do something
163
+ # render :view => :bored, :layout => "metal"
164
+ # end
165
+ #
166
+ # will use the view <tt>bored.html</tt> as template and even put it in the layout
167
+ # <tt>metal</tt> that's located at <tt>$RAILS_ROOT/app/cells/layouts/metal.html.erb</tt>.
168
+ #
169
+ # render :js => "alert('SQUEAK!');"
170
+ #
171
+ # issues a squeaking alert dialog on the page.
172
+ def render(options={}, &block)
173
+ if options[:nothing]
174
+ return ""
175
+ end
176
+
177
+ if options[:text]
178
+ options.reverse_merge!(:render_children => false)
179
+ end
180
+
181
+ options.reverse_merge! :render_children => true,
182
+ :locals => {},
183
+ :invoke => {},
184
+ :suppress_js => false
185
+
186
+
187
+ rendered_children = render_children_for(options)
188
+
189
+ options[:locals].reverse_merge!(:rendered_children => rendered_children)
190
+
191
+ @controller = controller # that dependency SUCKS.
192
+ @suppress_js = options[:suppress_js] ### FIXME: implement with ActiveHelper and :locals.
193
+
194
+
195
+ render_view_for(options, @state_name) # defined in Cell::Base.
196
+ end
197
+
198
+ alias_method :emit, :render
199
+
200
+
201
+ def replace(options={})
202
+ content = render(options)
203
+ Apotomo.js_generator.replace(self.name, content)
204
+ end
205
+
206
+ def update(options={})
207
+ content = render(options)
208
+ Apotomo.js_generator.update(self.name, content)
209
+ end
210
+
211
+ # Force the FSM to go into <tt>state</tt>, regardless whether it's a valid
212
+ # transition or not.
213
+ ### TODO: document the need for return.
214
+ def jump_to_state(state)
215
+ logger.debug "STATE JUMP! to #{state}"
216
+
217
+ render_state(state)
218
+ end
219
+
220
+
221
+ def visible_children
222
+ children.find_all { |kid| kid.visible? }
223
+ end
224
+
225
+ def render_children_for(options)
226
+ return {} unless options[:render_children]
227
+
228
+ render_children(options[:invoke])
229
+ end
230
+
231
+ def render_children(invoke_options={})
232
+ returning rendered_children = ActiveSupport::OrderedHash.new do
233
+ visible_children.each do |kid|
234
+ child_state = decide_state_for(kid, invoke_options)
235
+ logger.debug " #{kid.name} -> #{child_state}"
236
+
237
+ rendered_children[kid.name] = render_child(kid, child_state)
238
+ end
239
+ end
240
+ end
241
+
242
+ def render_child(cell, state)
243
+ cell.invoke(state)
244
+ end
245
+
246
+ def decide_state_for(child, invoke_options)
247
+ invoke_options.stringify_keys[child.name.to_s]
248
+ end
249
+
250
+
251
+ ### DISCUSS: use #param only for accessing request data.
252
+ def param(name)
253
+ params[name]
254
+ end
255
+
256
+
257
+ # Returns the address hash to the event controller and the targeted widget.
258
+ #
259
+ # Reserved options for <tt>way</tt>:
260
+ # :source explicitly specifies an event source.
261
+ # The default is to take the current widget as source.
262
+ # :type specifies the event type.
263
+ #
264
+ # Any other option will be directly passed into the address hash and is
265
+ # available via StatefulWidget#param in the widget.
266
+ #
267
+ # Can be passed to #url_for.
268
+ #
269
+ # Example:
270
+ # address_for_event :type => :squeak, :volume => 9
271
+ # will result in an address that triggers a <tt>:click</tt> event from the current
272
+ # widget and also provides the parameter <tt>:item_id</tt>.
273
+ def address_for_event(options)
274
+ raise "please specify the event :type" unless options[:type]
275
+
276
+ options[:source] ||= self.name
277
+ options
278
+ end
279
+
280
+ # Returns the widget named <tt>widget_id</tt> as long as it is below self or self itself.
281
+ def find_widget(widget_id)
282
+ find {|node| node.name.to_s == widget_id.to_s}
283
+ end
284
+
285
+ def controller
286
+ root? ? @controller : root.controller
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,36 @@
1
+ module Apotomo
2
+ # Shortcut methods for creating widget trees.
3
+ module WidgetShortcuts
4
+ # Creates an instance of <tt>class_name</tt> with the id <tt>id</tt> and start state <tt>state</tt>.
5
+ # Default start state is <tt>:display</tt>.
6
+ # Yields self if a block is passed.
7
+ # Example:
8
+ # widget(:form, 'uploads', :build_form) do |form|
9
+ # form << widget(:upload_field)
10
+ def widget(class_name, id, state=:display, *args)
11
+ object = class_name.to_s.classify.constantize.new(id, state, *args)
12
+ yield object if block_given?
13
+ object
14
+ end
15
+
16
+ def container(id, *args, &block)
17
+ widget('apotomo/container_widget', id, *args, &block)
18
+ end
19
+
20
+ def section(*args)
21
+ container(*args)
22
+ end
23
+
24
+ def cell(base_name, states, id, *args)
25
+ widget(base_name.to_s + '_cell', states, id, *args)
26
+ end
27
+
28
+ def tab_panel(id, *args)
29
+ widget('apotomo/tab_panel_widget', :display, id, *args)
30
+ end
31
+
32
+ def tab(id, *args)
33
+ widget('apotomo/tab_widget', :display, id, *args)
34
+ end
35
+ end
36
+ end
data/rails/init.rb ADDED
File without changes
@@ -0,0 +1,2 @@
1
+ class ApplicationWidgetTree < Apotomo::WidgetTree
2
+ end
@@ -0,0 +1,206 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. test_helper])
2
+
3
+ class ControllerMethodsTest < ActionController::TestCase
4
+ context "A Rails controller" do
5
+ setup do
6
+ barn_controller!
7
+ end
8
+
9
+ context "responding to #apotomo_root" do
10
+ should "initially return a root widget" do
11
+ assert_equal 1, @controller.apotomo_root.size
12
+ end
13
+
14
+ should "allow tree modifications" do
15
+ @controller.apotomo_root << mouse_mock
16
+ assert_equal 2, @controller.apotomo_root.size
17
+ end
18
+ end
19
+
20
+ context "responding to #apotomo_request_processor" do
21
+ should "initially return the processor which has a flushed root" do
22
+ assert_kind_of Apotomo::RequestProcessor, @controller.apotomo_request_processor
23
+ assert_equal 1, @controller.apotomo_request_processor.root.size
24
+ end
25
+ end
26
+
27
+ context "invoking #uses_widgets" do
28
+ setup do
29
+ @controller.class.uses_widgets do |root|
30
+ root << mouse_mock('mum')
31
+ end
32
+ end
33
+
34
+ should "add the widgets to apotomo_root" do
35
+ assert_equal 'mum', @controller.apotomo_root['mum'].name
36
+ end
37
+
38
+ should "add the widgets only once in apotomo_root" do
39
+ @controller.apotomo_root
40
+ assert @controller.apotomo_root['mum']
41
+ end
42
+
43
+ should "allow multiple calls to uses_widgets" do
44
+ @controller.class.uses_widgets do |root|
45
+ root << mouse_mock('kid')
46
+ end
47
+
48
+ assert @controller.apotomo_root['mum']
49
+ assert @controller.apotomo_root['kid']
50
+ end
51
+
52
+ should "inherit uses_widgets blocks to sub-controllers" do
53
+ berry = mouse_mock('berry')
54
+ @sub_controller = Class.new(@controller.class) do
55
+ uses_widgets { |root| root << berry }
56
+ end.new
57
+ @sub_controller.params = {}
58
+ @sub_controller.session = {}
59
+
60
+ assert @sub_controller.apotomo_root['mum']
61
+ assert @sub_controller.apotomo_root['berry']
62
+ end
63
+
64
+ should "be aliased to has_widgets" do
65
+ @controller.class.has_widgets do |root|
66
+ root << mouse_mock('kid')
67
+ end
68
+
69
+ assert @controller.apotomo_root['mum']
70
+ assert @controller.apotomo_root['kid']
71
+ end
72
+ end
73
+
74
+ context "invoking #use_widgets" do
75
+ should "have an empty apotomo_root if no call happened, yet" do
76
+ assert_equal [], @controller.bound_use_widgets_blocks
77
+ assert_equal 1, @controller.apotomo_root.size
78
+ end
79
+
80
+ should "extend the widget family and remember the block with one #use_widgets call" do
81
+ @controller.use_widgets do |root|
82
+ root << mouse_mock
83
+ end
84
+
85
+ assert_equal 1, @controller.bound_use_widgets_blocks.size
86
+ assert_equal 2, @controller.apotomo_root.size
87
+ end
88
+
89
+ should "add blocks only once" do
90
+ block = Proc.new {|root| root << mouse_mock}
91
+
92
+ @controller.use_widgets &block
93
+ @controller.use_widgets &block
94
+
95
+ assert_equal 1, @controller.bound_use_widgets_blocks.size
96
+ assert_equal 2, @controller.apotomo_root.size
97
+ end
98
+
99
+ should "allow multiple calls with different blocks" do
100
+ mum_and_kid!
101
+ @controller.use_widgets do |root|
102
+ root << @mum
103
+ end
104
+ @controller.use_widgets do |root|
105
+ root << mouse_mock('pet')
106
+ end
107
+
108
+ assert_equal 2, @controller.bound_use_widgets_blocks.size
109
+ assert_equal 4, @controller.apotomo_root.size
110
+ end
111
+ end
112
+
113
+ context "invoking #url_for_event" do
114
+ should "compute an url for any widget" do
115
+ assert_equal "/barn/render_event_response?source=mouse&type=footsteps&volume=9", @controller.url_for_event(:footsteps, :source => :mouse, :volume => 9)
116
+ end
117
+ end
118
+
119
+ should "flush its bound_use_widgets_blocks with, guess, #flush_bound_use_widgets_blocks" do
120
+ @controller.bound_use_widgets_blocks << Proc.new {}
121
+ assert_equal 1, @controller.bound_use_widgets_blocks.size
122
+ @controller.flush_bound_use_widgets_blocks
123
+ assert_equal 0, @controller.bound_use_widgets_blocks.size
124
+ end
125
+ end
126
+
127
+ context "invoking #render_widget" do
128
+ setup do
129
+ @mum = mouse_mock('mum', 'snuggle') {def snuggle; render; end}
130
+ end
131
+
132
+ should "render the widget" do
133
+ @controller.apotomo_root << @mum
134
+ assert_equal '<div id="mum"><snuggle></snuggle></div>', @controller.render_widget('mum')
135
+ end
136
+ end
137
+
138
+
139
+
140
+ context "invoking #apotomo_freeze" do
141
+ should "freeze the widget tree to session" do
142
+ assert_equal 0, @controller.session.size
143
+ @controller.send :apotomo_freeze
144
+ assert @controller.session[:apotomo_widget_ivars]
145
+ assert @controller.session[:apotomo_stateful_branches]
146
+ end
147
+ end
148
+
149
+ context "processing an event request" do
150
+ setup do
151
+ @mum = mouse_mock('mum', :eating)
152
+ @mum << @kid = mouse_mock('kid', :squeak)
153
+
154
+ @kid.respond_to_event :doorSlam, :with => :eating, :on => 'mum'
155
+ @kid.respond_to_event :doorSlam, :with => :squeak
156
+ @mum.respond_to_event :doorSlam, :with => :squeak
157
+
158
+ @mum.instance_eval do
159
+ def squeak; render :js => 'squeak();'; end
160
+ end
161
+ @kid.instance_eval do
162
+ def squeak; render :text => 'squeak!', :update => :true; end
163
+ end
164
+ end
165
+
166
+ ### DISCUSS: needed?
167
+ context "in event mode" do
168
+ should_eventually "set the MIME type to text/javascript" do
169
+ @controller.apotomo_root << @mum
170
+
171
+ get :render_event_response, :source => :kid, :type => :doorSlam
172
+
173
+ assert_equal Mime::JS, @response.content_type
174
+ assert_equal "$(\"mum\").replace(\"<div id=\\\"mum\\\">burp!<\\/div>\")\n$(\"kid\").update(\"squeak!\")\nsqueak();", @response.body
175
+ end
176
+ end
177
+ end
178
+
179
+ context "The ProcHash" do
180
+ setup do
181
+ @procs = Apotomo::Rails::ControllerMethods::ProcHash.new
182
+ @b = Proc.new{}; @d = Proc.new{}
183
+ @c = Proc.new{}
184
+ @procs << @b
185
+ end
186
+
187
+ should "return true for procs it includes" do
188
+ assert @procs.include?(@b)
189
+ assert @procs.include?(@d) ### DISCUSS: line nr is id, or do YOU got a better idea?!
190
+ end
191
+
192
+ should "reject unknown procs" do
193
+ assert ! @procs.include?(@c)
194
+ end
195
+ end
196
+
197
+ ### FIXME: could somebody get that working?
198
+ context "Routing" do
199
+ should_eventually "generate routes to the render_event_response action" do
200
+ assert_generates "/barn/render_event_response?type=squeak", { :controller => "barn", :action => "render_event_response", :type => "squeak" }
201
+
202
+ assert_recognizes({ :controller => "apotomo", :action => "render_event_response", :type => "squeak" }, "/apotomo/render_event_response?type=squeak")
203
+ end
204
+ end
205
+
206
+ end