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 @@
1
+ <%= @content.to_s %>
@@ -0,0 +1 @@
1
+ <%= @content.to_s %>
@@ -0,0 +1,27 @@
1
+ class Apotomo::DeepLinkWidget < Apotomo::StatefulWidget
2
+
3
+ transition :from => :setup, :to => :process
4
+ transition :in => :process
5
+
6
+ def setup
7
+ root.respond_to_event :externalChange, :on => 'deep_link', :with => :process
8
+ root.respond_to_event :internalChange, :on => 'deep_link', :with => :process
9
+
10
+ render
11
+ end
12
+
13
+ def process
14
+ # find out what changed in the deep link
15
+ # find the update root (### DISCUSS: this might be more than one root, as in A--B)
16
+ #path = param(:deep_link) # path is #tab=users/icon=3
17
+
18
+ update_root = root.find {|w| w.responds_to_url_change? and w.responds_to_url_change_for?(url_fragment)} ### DISCUSS: we just look for one root here.
19
+
20
+ if update_root
21
+ controller.logger.debug "deep_link#process: `#{update_root.name}` responds to :urlChange"
22
+ update_root.trigger(:urlChange)
23
+ end
24
+
25
+ render :nothing => true
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ <%= javascript_tag "
2
+ SWFAddress.onInternalChange = function() {
3
+ //alert('internalChange');
4
+ #{remote_function :url => address_to_event(:type=>:internalChange), :with => '\'deep_link=\'+SWFAddress.getPath()'}
5
+ }
6
+
7
+ SWFAddress.onExternalChange = function() {
8
+ //alert(SWFAddress.getPath()+SWFAddress.getQueryString());
9
+ #{remote_function :url => address_to_event(:type=>:internalChange), :with => '\'deep_link=\'+SWFAddress.getPath()'}
10
+ }
11
+
12
+
13
+ SWFAddress.onChange = function() {
14
+ }
15
+
16
+ SWFAddress.onInit = function() {
17
+ #{remote_function :url => address_to_event(:type=>:internalChange), :with => '\'deep_link=\'+SWFAddress.getPath()'}
18
+ //alert('init');
19
+ }
20
+ " %>
@@ -0,0 +1,12 @@
1
+ module Apotomo
2
+
3
+ ### TODO: if a state doesn't return anything, the view-finding is invoked, which
4
+ ### is nonsense in a JS widget. current work-around: return render :js => ""
5
+
6
+ class JavaScriptWidget < StatefulWidget
7
+ def frame_content(content)
8
+ content
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,87 @@
1
+ module Apotomo
2
+ class TabPanelWidget < StatefulWidget
3
+ transition :from => :display, :to => :switch
4
+ transition :in => :switch
5
+
6
+ attr_accessor :current_child_id
7
+
8
+
9
+ # Called in StatefulWidget's constructor.
10
+ def initialize_deep_link_for(id, start_states, opts)
11
+ return unless opts[:is_url_listener]
12
+
13
+ respond_to_event :urlChange, :from => self.name, :with => :switch
14
+ end
15
+
16
+
17
+
18
+ def display
19
+ respond_to_event(:switchChild, :with => :switch)
20
+
21
+
22
+ @current_child_id = find_current_child.name
23
+
24
+ render :locals => {:tabs => children}
25
+ end
26
+
27
+
28
+ def switch
29
+ @current_child_id = find_current_child.name
30
+
31
+ render :view => :display, :locals => {:tabs => children}
32
+ end
33
+
34
+
35
+ def children_to_render
36
+ [children.find{ |c| c.name == @current_child_id } ]
37
+ end
38
+
39
+ ### DISCUSS: use #find_param instead of #param to provide a cleaner parameter retrieval?
40
+ def find_current_child
41
+ if responds_to_url_change?
42
+ child_id = url_fragment[param_name]
43
+ else
44
+ child_id = param(param_name)
45
+ end
46
+
47
+ find_child(child_id) || find_child(@current_child_id) || default_child
48
+ end
49
+
50
+ def default_child; children.first; end
51
+
52
+
53
+ def find_child(id)
54
+ children.find { |c| c.name.to_s == id }
55
+ end
56
+
57
+ def param_name; name; end
58
+
59
+
60
+ # Called by deep_link_widget#process to query if we're involved in an URL change.
61
+ def responds_to_url_change_for?(fragment)
62
+ # don't respond to an empty/invalid/ fragment as we don't get any information from it:
63
+ return if fragment[param_name].blank?
64
+
65
+ fragment[param_name] != @current_child_id
66
+ end
67
+
68
+ def local_fragment
69
+ "#{param_name}=#{current_child_id}"
70
+ end
71
+
72
+
73
+ # Used in view to create the tab link in deep-linking mode.
74
+ def url_fragment_for_tab(tab)
75
+ url_fragment_for("#{param_name}=#{tab.name}")
76
+ end
77
+
78
+
79
+ def address(way={}, target=self, state=nil)
80
+ way.merge!( local_address(target, way, state) )
81
+
82
+ return way if isRoot?
83
+
84
+ return parent.address(way, target)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,57 @@
1
+ <style type="text/css">
2
+ .TabPanel ul {
3
+ display: block;
4
+ list-style: none;
5
+ padding: 0;
6
+ margin: 0 0 -1px 0;
7
+ height: 100%;
8
+ font-size: 14px;
9
+ border-left: 1px solid #888888;
10
+
11
+ }
12
+
13
+ .TabPanel ul li {
14
+ float: left;
15
+ margin: 0;
16
+ padding: 5px;
17
+ background-color: #dbdbdb;
18
+ color: #888888;
19
+ border-top: 1px solid #888888;
20
+ border-right: 1px solid #888888;
21
+ border-bottom: 1px solid #888888;
22
+ }
23
+
24
+ .TabPanel ul li.active {
25
+ border-bottom: 1px solid #ffffff;
26
+ background-color: #ffffff;
27
+ color: #000000;
28
+
29
+ }
30
+
31
+ .TabPanel ul li a, .TabPanel ul li a:visited {
32
+ color: #888888
33
+ }
34
+ </style>
35
+
36
+
37
+ <div class="TabPanel">
38
+ <ul>
39
+ <% tabs.each do |tab|
40
+ tab_class = ""
41
+ tab_class = "active" if tab.name == @current_child_id
42
+ %>
43
+ <li class="<%= tab_class %>">
44
+ <%- if @cell.responds_to_url_change? %>
45
+ <%= link_to_function tab.title, update_url(@cell.url_fragment_for_tab(tab)) %>
46
+ <%- else %>
47
+ <%= link_to_event tab.title, {:type => :switchChild, @cell.param_name => tab.name} %>
48
+ <% end %>
49
+ </li>
50
+ <% end -%>
51
+
52
+ <div style="clear: both;" />
53
+ </ul>
54
+ </div>
55
+ <div style="clear: both;"></div>
56
+
57
+ <%= rendered_children.first %>
@@ -0,0 +1,18 @@
1
+ module Apotomo
2
+ class TabWidget < StatefulWidget
3
+
4
+ attr_accessor :title
5
+
6
+ def initialize(*args)
7
+ super(*args)
8
+
9
+ @title = @opts[:title] || self.name.to_s
10
+ end
11
+
12
+
13
+ def display
14
+ render
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1 @@
1
+ <%= rendered_children.collect{|e| e.last}.join("") %>
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ map.apotomo_event ':controller/render_event_response', :action => 'render_event_response'
3
+ end
@@ -0,0 +1,15 @@
1
+ Description:
2
+ Stubs out a new cell widget, its state views and a functional test.
3
+ Pass the cell name, either CamelCased or under_scored, and a list
4
+ of states as arguments.
5
+
6
+ This generates a cell class in app/cells and view templates in
7
+ app/cells/cell_name.
8
+
9
+ Example:
10
+ './script/generate widget Posting new'
11
+
12
+ This will create an Apotomo Posting cell:
13
+ Cell: app/cells/posting_cell.rb
14
+ Views: app/cells/posting/new.html.erb
15
+ Test: test/functional/test_posting_cell.rb
@@ -0,0 +1,8 @@
1
+ require "test_helper"
2
+
3
+ class <%= class_name %>Test < Test::Unit::TestCase
4
+ test "a first test" do
5
+ html = widget(:<%= file_name %>, :<%= states.first %>, 'my_<%= file_name %>').invoke
6
+ assert_selekt html, "p"
7
+ end
8
+ end
@@ -0,0 +1,2 @@
1
+ <h1><%= class_name %>#<%= action %></h1>
2
+ <p>Find me in <%= path %></p>
@@ -0,0 +1,3 @@
1
+ %h1 "<%= class_name %>#<%= action %>"
2
+ %p
3
+ Find me in <%= path %>
@@ -0,0 +1,8 @@
1
+ class <%= class_name %> < Apotomo::Widget
2
+ <% for action in actions -%>
3
+ def <%= action %>
4
+ render
5
+ end
6
+
7
+ <% end -%>
8
+ end
@@ -0,0 +1,34 @@
1
+ require 'rails_generator/generators/components/controller/controller_generator'
2
+
3
+ class WidgetGenerator < ControllerGenerator
4
+ def add_options!(opt)
5
+ opt.on('--haml') { |value| options[:view_format] = 'haml' }
6
+ end
7
+
8
+ def manifest
9
+ options.reverse_merge! :view_format => 'erb'
10
+
11
+ record do |m|
12
+ # Check for class naming collisions.
13
+ m.class_collisions class_path, "#{class_name}"
14
+
15
+ # Directories
16
+ m.directory File.join('app/cells', class_path)
17
+ m.directory File.join('app/cells', class_path, file_name)
18
+ m.directory File.join('test/widgets')
19
+
20
+ # Widget
21
+ m.template 'widget.rb', File.join('app/cells', class_path, "#{file_name}.rb")
22
+
23
+ # View template for each state.
24
+ format = options[:view_format]
25
+ actions.each do |state|
26
+ path = File.join('app/cells', class_path, file_name, "#{state}.html.#{format}")
27
+ m.template "view.html.#{format}", path, :assigns => { :action => state, :path => path }
28
+ end
29
+
30
+ # Functional test for the widget.
31
+ m.template 'functional_test.rb', File.join('test/widgets/', "#{file_name}_test.rb"), :assigns => {:states => actions}
32
+ end
33
+ end
34
+ end
data/lib/apotomo.rb ADDED
@@ -0,0 +1,59 @@
1
+ # Copyright (c) 2007-2010 Nick Sutterer <apotonick@gmail.com>
2
+ #
3
+ # The MIT License
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+ #
23
+
24
+
25
+ require 'cells'
26
+ require 'onfire'
27
+
28
+ module Apotomo
29
+ class << self
30
+ def js_framework=(js_framework)
31
+ @js_framework = js_framework
32
+ @js_generator = ::Apotomo::JavascriptGenerator.new(js_framework)
33
+ end
34
+
35
+ attr_reader :js_generator, :js_framework
36
+
37
+ # Apotomo setup/configuration helper for initializer.
38
+ #
39
+ # == Usage/Examples:
40
+ #
41
+ # Apotomo.setup do |config|
42
+ # config.js_framework = :jquery
43
+ # end
44
+ def setup
45
+ yield self
46
+ end
47
+ end
48
+ end
49
+
50
+ ### FIXME: move to rails.rb
51
+
52
+ require 'apotomo/javascript_generator'
53
+ Apotomo.js_framework = :prototype ### DISCUSS: move to rails.rb
54
+
55
+ ### DISCUSS: move to 'apotomo/widgets'?
56
+ require 'apotomo/widget'
57
+ require 'apotomo/stateful_widget'
58
+ require 'apotomo/container_widget'
59
+ require 'apotomo/widget_shortcuts'
@@ -0,0 +1,37 @@
1
+ # Introduces caching of rendered state views into the StatefulWidget.
2
+ module Apotomo::Caching
3
+
4
+ def self.included(base) #:nodoc:
5
+ base.class_eval do
6
+ extend ClassMethods
7
+ end
8
+ end
9
+
10
+
11
+ module ClassMethods
12
+ # If <tt>version_proc</tt> is omitted, Apotomo provides some basic caching
13
+ # mechanism: the state view rendered for <tt>state</tt> will be cached as long
14
+ # as you (or e.g. an EventHandler) calls #dirty!. It will then be re-rendered
15
+ # and cached again.
16
+ # You may override that to provide fine-grained caching, with multiple cache versions
17
+ # for the same state.
18
+ def cache(state, version_proc=:cache_version)
19
+ super(state, version_proc)
20
+ end
21
+ end
22
+
23
+ def cache_version
24
+ @version ||= 0
25
+ {:v => @version}
26
+ end
27
+
28
+ def increment_version
29
+ @version += 1
30
+ end
31
+
32
+ # Instruct caching to re-render all cached state views.
33
+ def dirty!
34
+ increment_version
35
+ end
36
+
37
+ end
@@ -0,0 +1,10 @@
1
+ require 'apotomo/widget'
2
+
3
+ module Apotomo
4
+ class ContainerWidget < Widget
5
+ def display
6
+ content = render_children.collect{ |v| v.last }.join("\n")
7
+ render :text => "<div id=\"#{self.name}\">#{content}</div>"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,90 @@
1
+ module Apotomo
2
+ module DeepLinkMethods
3
+ def self.included(base)
4
+ base.initialize_hooks << :initialize_deep_link_for
5
+ end
6
+
7
+
8
+ # Called in StatefulWidget's constructor.
9
+ def initialize_deep_link_for(id, start_states, opts)
10
+ #add_deep_link if opts[:is_url_listener] ### DISCUSS: remove #add_de
11
+ end
12
+
13
+ def responds_to_url_change?
14
+ evt_table.all_handlers_for(:urlChange, name).size > 0
15
+ end
16
+
17
+
18
+ ### DISCUSS: private? rename to compute_url_fragment_for ?
19
+ # Computes the fragment part of the widget's url by querying all widgets up to root.
20
+ # Widgets managing a certain state will usually insert state recovery information
21
+ # via local_fragment.
22
+ def url_fragment_for(local_portion=nil, portions=[])
23
+ local_portion = local_fragment if responds_to_url_change? and local_portion.nil?
24
+
25
+ portions.unshift(local_portion) # prepend portions as we move up.
26
+
27
+ return portions.compact.join("/") if root?
28
+
29
+ parent.url_fragment_for(nil, portions)
30
+ end
31
+
32
+
33
+ # Called when widget :is_url_listener. Adds the local url fragment portion to the url.
34
+ def local_fragment
35
+ #"#{local_fragment_key}=#{state_name}"
36
+ end
37
+
38
+ # Key found in the url fragment, pointing to the local fragment.
39
+ #def local_fragment_key
40
+ # name
41
+ #end
42
+
43
+
44
+ # Called by DeepLinkWidget#process to query if we're involved in an URL change.
45
+ # Do return false if you're not interested in the change.
46
+ #
47
+ # This especially means:
48
+ # * the fragment doesn't include you or is empty
49
+ # fragment[name].blank?
50
+ # * your portion in the fragment didn't change
51
+ # tab=first/content=html vs. tab=first/content=markdown
52
+ # fragment[:tab] != @active_tab
53
+ def responds_to_url_change_for?(fragment)
54
+ end
55
+
56
+
57
+
58
+ class UrlFragment
59
+ attr_reader :fragment
60
+
61
+ def initialize(fragment)
62
+ @fragment = fragment || ""
63
+ end
64
+
65
+ def to_s
66
+ fragment.to_s
67
+ end
68
+
69
+ def blank?
70
+ fragment.blank?
71
+ end
72
+
73
+ ### TODO: make path separator configurable.
74
+ def [](key)
75
+ if path_portion = fragment.split("/").find {|i| i.include?(key.to_s)}
76
+ return path_portion.sub("#{key}=", "")
77
+ end
78
+
79
+ nil
80
+ end
81
+ end
82
+
83
+ # Query object for the url fragment. Use this to retrieve state information from the
84
+ # deep link.
85
+ def url_fragment
86
+ UrlFragment.new(param(:deep_link))
87
+ end
88
+
89
+ end
90
+ end