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,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