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,95 @@
1
+ module Apotomo
2
+ module Rails
3
+ module ViewHelper
4
+ # needs :@controller
5
+
6
+ # Returns the app JavaScript generator.
7
+ def js_generator
8
+ Apotomo.js_generator
9
+ end
10
+
11
+
12
+ # Generates the JavaScript code to report an event of <tt>type</tt> to Apotomo with AJAX.
13
+ # As always per default the event source is the currently rendered widget.
14
+ # Internally this method just uses <tt>remote_function</tt> for JS output.
15
+ #
16
+ # Example:
17
+ #
18
+ # <%= image_tag "cheese.png", :onMouseover => trigger_event(:mouseAlarm) %>
19
+ #
20
+ # will trigger the event <tt>:mouseAlarm</tt> when moving the mouse over the cheese image.
21
+ def trigger_event(type, options={})
22
+ js_generator.xhr(url_for_event(type, options))
23
+ end
24
+
25
+ # Creates a link that triggers an event via AJAX.
26
+ # This link will <em>only</em> work in JavaScript-able browsers.
27
+ #
28
+ # Note that the link is created using #link_to_remote.
29
+ def link_to_event(title, type, options={}, html_options={})
30
+ link_to_remote(title, {:url => url_for_event(type, options)}, html_options)
31
+ end
32
+
33
+ # Creates a form tag that triggers an event via AJAX when submitted.
34
+ # See StatefulWidget::address_for_event for options.
35
+ #
36
+ # The values of form elements are available via StatefulWidget#param.
37
+ def form_to_event(type, options={}, html_options={}, &block)
38
+ return multipart_form_to_event(type, options, html_options, &block) if options.delete(:multipart)
39
+
40
+ form_remote_tag({:url => url_for_event(type, options), :html => html_options}, &block)
41
+ ### FIXME: couldn't get obstrusive js working, i don't understand rails helpers.
42
+ #html_options[:onSubmit] = js_generator.escape(js_generator.xhr(url_for_event(type, options)))
43
+ #puts html_options.inspect
44
+ #form_tag(url_for_event(type, options), html_options, &block)
45
+ end
46
+
47
+ # Creates a form that submits itself via an iFrame and executes the response
48
+ # in the parent window. This is needed to upload files via AJAX.
49
+ #
50
+ # Better call <tt>#form_to_event :multipart => true</tt> and stay forward-compatible.
51
+ def multipart_form_to_event(type, options={}, html_options={}, &block)
52
+ options.reverse_merge! :apotomo_iframe => true
53
+ html_options.reverse_merge! :target => :apotomo_iframe, :multipart => true
54
+
55
+ # i hate rails:
56
+ concat('<iframe id="apotomo_iframe" name="apotomo_iframe" style="display: none;"></iframe>') << form_tag(url_for_event(type, options), html_options, &block)
57
+ end
58
+
59
+ # Returns the url to trigger a +type+ event from the currently rendered widget.
60
+ # The source can be changed with +:source+. Additional +options+ will be appended to the query string.
61
+ #
62
+ # Note that this method will use the framework's internal routing if available (e.g. #url_for in Rails).
63
+ #
64
+ # Example:
65
+ # url_for_event(:paginate, :page => 2)
66
+ # #=> http://apotomo.de/mouse/process_event_request?type=paginate&source=mouse&page=2
67
+ def url_for_event(type, options={})
68
+ options.reverse_merge! :source => @cell.name
69
+ @controller.url_for_event(type, options)
70
+ end
71
+
72
+ ### TODO: test me.
73
+ def update_url(fragment)
74
+ 'SWFAddress.setValue("' + fragment + '");'
75
+ end
76
+
77
+ ### TODO: test me.
78
+ ### DISCUSS: rename to rendered_children ?
79
+ def content
80
+ @rendered_children.collect{|e| e.last}.join("\n")
81
+ end
82
+
83
+ # needs: suppress_javascript
84
+ def widget_javascript(*args, &block)
85
+ return if @suppress_js ### FIXME: implement with ActiveHelper and :locals.
86
+
87
+ javascript_tag(*args, &block)
88
+ end
89
+
90
+ def widget_div(*args, &block)
91
+ content_tag(:div, :id => @cell.name, :class => :widget, &block)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,7 @@
1
+ module Apotomo
2
+ module Rails
3
+ module ViewMethods
4
+ delegate :render_widget, :url_for_event, :to => :controller
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,92 @@
1
+ module Apotomo
2
+ class RequestProcessor
3
+ include WidgetShortcuts
4
+
5
+ attr_reader :session, :root
6
+
7
+ def initialize(session, options={}, uses_widgets_blocks=[])
8
+ @session = session
9
+ @widgets_flushed = false
10
+
11
+ @root = widget('apotomo/widget', 'root')
12
+ uses_widgets_blocks.each { |blk| blk.call(@root) } # add stateless widgets.
13
+
14
+ if options[:flush_widgets].blank? and ::Apotomo::StatefulWidget.frozen_widget_in?(session)
15
+ @root = ::Apotomo::StatefulWidget.thaw_for(session, @root)
16
+ else
17
+ #@root = flushed_root
18
+
19
+ flushed_root ### FIXME: set internal mode to flushed
20
+ end
21
+
22
+ #handle_version!(options[:version])
23
+ end
24
+
25
+
26
+ def flushed_root
27
+ StatefulWidget.flush_storage(session)
28
+ @widgets_flushed = true
29
+ #widget('apotomo/widget', 'root')
30
+ end
31
+
32
+ ### DISCUSS: do we need the version feature, or should we push that into user code?
33
+ def handle_version!(version)
34
+ return if version.blank?
35
+ return if root.version == version
36
+
37
+ @root = flushed_root
38
+ @root.version = version
39
+ end
40
+
41
+ def widgets_flushed?; @widgets_flushed; end
42
+
43
+ # Fires the request event in the widget tree and collects the rendered page updates.
44
+ def process_for(request_params, controller)
45
+ ### TODO: move controller dependency to rails/merb/sinatra layer only!
46
+ self.root.controller = controller
47
+
48
+ source = self.root.find_widget(request_params[:source]) or raise "Source #{request_params[:source].inspect} non-existent."
49
+
50
+ source.fire(request_params[:type].to_sym)
51
+ source.root.page_updates ### DISCUSS: that's another dependency.
52
+ end
53
+
54
+ ### FIXME: remove me!
55
+ def render_page_updates(page_updates)
56
+ page_updates.join("\n")
57
+ end
58
+
59
+ # Serializes the current widget tree to the storage that was passed in the constructor.
60
+ # Call this at the end of a request.
61
+ def freeze!
62
+ Apotomo::StatefulWidget.freeze_for(@session, root)
63
+ end
64
+
65
+ # Renders the widget named <tt>widget_id</tt>, passing optional <tt>opts</tt> and a block to it.
66
+ # Use this in your #render_widget wrapper.
67
+ def render_widget_for(widget_id, opts, controller, &block)
68
+ if widget_id.kind_of?(::Apotomo::Widget)
69
+ widget = widget_id
70
+ else
71
+ widget = root.find_widget(widget_id)
72
+ raise "Couldn't render non-existent widget `#{widget_id}`" unless widget
73
+ end
74
+
75
+
76
+ ### TODO: pass options in invoke.
77
+ widget.opts = opts unless opts.empty?
78
+
79
+ ### TODO: move controller dependency to rails/merb/sinatra layer only!
80
+ widget.root.controller = controller
81
+
82
+ widget.invoke(&block)
83
+ end
84
+
85
+ # Computes the address hash for a +:source+ widget and an event +:type+.
86
+ # Additional parameters will be merged.
87
+ def address_for(options)
88
+ raise "You forgot to provide :source or :type" unless options.has_key?(:source) and options.has_key?(:type)
89
+ options
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,8 @@
1
+ require 'apotomo/widget'
2
+ require 'apotomo/persistence'
3
+
4
+ module Apotomo
5
+ class StatefulWidget < Widget
6
+ include Persistence
7
+ end
8
+ end
@@ -0,0 +1,46 @@
1
+ module Apotomo::Transition
2
+
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ # Defines a transition for an implicit invoke.
9
+ #
10
+ # Usually when a container widget renders its kids there is no <tt>state</tt> passed to the
11
+ # kid's #invoke ("implicit invoke") and thus the kid will enter its start state again.
12
+ # You can customize that behaviour by setting a transition to let the widget jump to the
13
+ # defined <tt>:to</tt> state in place of the start state.
14
+ #
15
+ # Example:
16
+ # class Kid < MouseWidget
17
+ # transition :from => :sleep, :to => :snore
18
+ #
19
+ # Next time when mum renders and kid is in <tt>:sleep</tt> state kid will not return to its
20
+ # start state but invoke <tt>:snore</tt>.
21
+ #
22
+ # class Kid < MouseWidget
23
+ # transition :in => :snore
24
+ #
25
+ # In subsequent render cycles from mum kid will keep snoring whereas other kids would go back
26
+ # to the start state.
27
+ def transition(options)
28
+ if from = options[:from]
29
+ class_transitions[from] = options[:to]
30
+ elsif loop = options[:in]
31
+ transition :from => loop, :to => loop
32
+ end
33
+ end
34
+
35
+ def class_transitions
36
+ @class_transitions ||= {}
37
+ end
38
+ end
39
+
40
+ protected
41
+ # Returns the next state for <tt>state</tt> or nil. A next state must have been defined
42
+ # with #transition.
43
+ def next_state_for(state)
44
+ self.class.class_transitions[state]
45
+ end
46
+ end
@@ -0,0 +1,186 @@
1
+ # stolen from ... ? couldn't find the original lib on the net.
2
+ ### TODO: insert copyright notice!
3
+
4
+ module TreeNode
5
+ include Enumerable
6
+
7
+ attr_reader :content, :name, :parent
8
+ attr_writer :content, :parent
9
+
10
+ def self.included(base)
11
+ base.initialize_hooks << :initialize_tree_node_for
12
+ end
13
+
14
+ # Constructor which expects the name of the node
15
+ #
16
+ # name of the node is expected to be unique across the
17
+ # tree.
18
+ def initialize_tree_node_for(name, *args)
19
+ self.setAsRoot!
20
+
21
+ @childrenHash = Hash.new
22
+ @children = []
23
+ end
24
+
25
+ # Print the string representation of this node.
26
+ def to_s
27
+ s = size()
28
+ "Node ID: #{@name} Content: #{@content} Parent: " +
29
+ (root?() ? "ROOT" : "#{@parent.name}") +
30
+ " Children: #{@children.length}" +
31
+ " Total Nodes: #{s}"
32
+ end
33
+
34
+ # Convenience synonym for Tree#add method.
35
+ # This method allows a convenient method to add
36
+ # children hierarchies in the tree.
37
+ # E.g. root << child << grand_child
38
+ def <<(child)
39
+ add(child)
40
+ end
41
+
42
+ # Adds the specified child node to the receiver node.
43
+ # The child node's parent is set to be the receiver.
44
+ # The child is added as the last child in the current
45
+ # list of children for the receiver node.
46
+ def add(child)
47
+ raise "Child already added" if @childrenHash.has_key?(child.name)
48
+
49
+ @childrenHash[child.name] = child
50
+ @children << child
51
+ child.parent = self
52
+ return child
53
+ end
54
+
55
+ # Removes the specified child node from the receiver node.
56
+ # The removed children nodes are orphaned but available
57
+ # if an alternate reference exists.
58
+ # Returns the child node.
59
+ def remove!(child)
60
+ @childrenHash.delete(child.name)
61
+ @children.delete(child)
62
+ child.setAsRoot! unless child == nil
63
+ return child
64
+ end
65
+
66
+ # Removes this node from its parent. If this is the root node,
67
+ # then does nothing.
68
+ def removeFromParent!
69
+ @parent.remove!(self) unless root?
70
+ end
71
+
72
+ # Removes all children from the receiver node.
73
+ def remove_all!
74
+ for child in @children
75
+ child.setAsRoot!
76
+ end
77
+ @childrenHash.clear
78
+ @children.clear
79
+ self
80
+ end
81
+
82
+
83
+ # Private method which sets this node as a root node.
84
+ def setAsRoot!
85
+ @parent = nil
86
+ end
87
+
88
+ def root!
89
+ setAsRoot!
90
+ end
91
+
92
+ # Indicates whether this node is a root node. Note that
93
+ # orphaned children will also be reported as root nodes.
94
+ def root?
95
+ @parent == nil
96
+ end
97
+
98
+ # Returns an array of all the immediate children.
99
+ # If a block is given, yields each child node to the block.
100
+ def children
101
+ if block_given?
102
+ @children.each {|child| yield child}
103
+ else
104
+ @children
105
+ end
106
+ end
107
+
108
+ # Returns every node (including the receiver node) from the
109
+ # tree to the specified block.
110
+ def each &block
111
+ yield self
112
+ children { |child| child.each(&block) }
113
+ end
114
+
115
+ # Returns the requested node from the set of immediate
116
+ # children.
117
+ #
118
+ # If the key is _numeric_, then the in-sequence array of
119
+ # children is accessed (see Tree#children).
120
+ # If the key is not _numeric_, then it is assumed to be
121
+ # the *name* of the child node to be returned.
122
+ def [](key)
123
+ raise "Key needs to be provided" if key == nil
124
+
125
+ if key.kind_of?(Integer)
126
+ @children[key]
127
+ else
128
+ @childrenHash[key]
129
+ end
130
+ end
131
+
132
+ # Returns the total number of nodes in this tree, rooted
133
+ # at the receiver node.
134
+ def size
135
+ @children.inject(1) {|sum, node| sum + node.size}
136
+ end
137
+
138
+ # Pretty prints the tree starting with the receiver node.
139
+ def printTree(tab = 0)
140
+ puts((' ' * tab) + self.to_s)
141
+ children {|child| child.printTree(tab + 4)}
142
+ end
143
+
144
+ # Returns the root for this node.
145
+ def root
146
+ root = self
147
+ root = root.parent while !root.root?
148
+ root
149
+ end
150
+
151
+ # Provides a comparision operation for the nodes. Comparision
152
+ # is based on the natural character-set ordering for the
153
+ # node names.
154
+ def <=>(other)
155
+ return +1 if other == nil
156
+ self.name <=> other.name
157
+ end
158
+
159
+ protected :parent=, :setAsRoot!
160
+
161
+ def find_by_path(selector)
162
+ next_node = self
163
+ last = nil # prevents self-finding loop.
164
+ selector.to_s.split(/ /).each do |node_id|
165
+ last = next_node = next_node.find {|n|
166
+ n.name.to_s == node_id.to_s and not n==last
167
+ }
168
+ end
169
+
170
+ return next_node
171
+ end
172
+
173
+
174
+ # Returns the path from the widget to root, encoded as
175
+ # a string of slash-seperated names.
176
+ def path
177
+ path = [name]
178
+ ancestor = parent
179
+ while ancestor
180
+ path << ancestor.name
181
+ ancestor = ancestor.parent
182
+ end
183
+
184
+ path.reverse.join("/")
185
+ end
186
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module Apotomo
4
+ VERSION = '0.1.1'
5
+ end