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