apotomo 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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