volt 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +37 -0
  6. data/Guardfile +9 -0
  7. data/LICENSE.txt +22 -0
  8. data/Rakefile +23 -0
  9. data/Readme.md +34 -0
  10. data/VERSION +1 -0
  11. data/bin/volt +4 -0
  12. data/docs/GETTING_STARTED.md +7 -0
  13. data/docs/GUIDE.md +33 -0
  14. data/lib/volt.rb +15 -0
  15. data/lib/volt/benchmark/benchmark.rb +25 -0
  16. data/lib/volt/cli.rb +34 -0
  17. data/lib/volt/console.rb +19 -0
  18. data/lib/volt/controllers/model_controller.rb +29 -0
  19. data/lib/volt/extra_core/array.rb +10 -0
  20. data/lib/volt/extra_core/blank.rb +88 -0
  21. data/lib/volt/extra_core/extra_core.rb +7 -0
  22. data/lib/volt/extra_core/numeric.rb +9 -0
  23. data/lib/volt/extra_core/object.rb +36 -0
  24. data/lib/volt/extra_core/string.rb +29 -0
  25. data/lib/volt/extra_core/stringify_keys.rb +7 -0
  26. data/lib/volt/extra_core/true_false.rb +44 -0
  27. data/lib/volt/extra_core/try.rb +31 -0
  28. data/lib/volt/models.rb +5 -0
  29. data/lib/volt/models/array_model.rb +37 -0
  30. data/lib/volt/models/model.rb +210 -0
  31. data/lib/volt/models/model_wrapper.rb +23 -0
  32. data/lib/volt/models/params.rb +67 -0
  33. data/lib/volt/models/url.rb +192 -0
  34. data/lib/volt/page/url_tracker.rb +36 -0
  35. data/lib/volt/reactive/array_extensions.rb +13 -0
  36. data/lib/volt/reactive/event_chain.rb +126 -0
  37. data/lib/volt/reactive/events.rb +283 -0
  38. data/lib/volt/reactive/object_tracker.rb +99 -0
  39. data/lib/volt/reactive/object_tracking.rb +15 -0
  40. data/lib/volt/reactive/reactive_array.rb +222 -0
  41. data/lib/volt/reactive/reactive_tags.rb +64 -0
  42. data/lib/volt/reactive/reactive_value.rb +368 -0
  43. data/lib/volt/reactive/string_extensions.rb +34 -0
  44. data/lib/volt/router/routes.rb +83 -0
  45. data/lib/volt/server.rb +121 -0
  46. data/lib/volt/server/binding_setup.rb +2 -0
  47. data/lib/volt/server/channel_handler.rb +31 -0
  48. data/lib/volt/server/component_handler.rb +88 -0
  49. data/lib/volt/server/if_binding_setup.rb +29 -0
  50. data/lib/volt/server/request_handler.rb +16 -0
  51. data/lib/volt/server/scope.rb +43 -0
  52. data/lib/volt/server/source_map_server.rb +31 -0
  53. data/lib/volt/server/template_parser.rb +452 -0
  54. data/lib/volt/store/mongo.rb +5 -0
  55. data/lib/volt/templates/attribute_binding.rb +110 -0
  56. data/lib/volt/templates/base_binding.rb +37 -0
  57. data/lib/volt/templates/channel.rb +48 -0
  58. data/lib/volt/templates/content_binding.rb +35 -0
  59. data/lib/volt/templates/document_events.rb +80 -0
  60. data/lib/volt/templates/each_binding.rb +115 -0
  61. data/lib/volt/templates/event_binding.rb +51 -0
  62. data/lib/volt/templates/if_binding.rb +74 -0
  63. data/lib/volt/templates/memory_test.rb +26 -0
  64. data/lib/volt/templates/page.rb +146 -0
  65. data/lib/volt/templates/reactive_template.rb +38 -0
  66. data/lib/volt/templates/render_queue.rb +5 -0
  67. data/lib/volt/templates/sub_context.rb +23 -0
  68. data/lib/volt/templates/targets/attribute_section.rb +33 -0
  69. data/lib/volt/templates/targets/attribute_target.rb +18 -0
  70. data/lib/volt/templates/targets/base_section.rb +14 -0
  71. data/lib/volt/templates/targets/binding_document/base_node.rb +3 -0
  72. data/lib/volt/templates/targets/binding_document/component_node.rb +112 -0
  73. data/lib/volt/templates/targets/binding_document/html_node.rb +11 -0
  74. data/lib/volt/templates/targets/dom_section.rb +147 -0
  75. data/lib/volt/templates/targets/dom_target.rb +11 -0
  76. data/lib/volt/templates/template_binding.rb +159 -0
  77. data/lib/volt/templates/template_renderer.rb +50 -0
  78. data/spec/models/event_chain_spec.rb +129 -0
  79. data/spec/models/model_spec.rb +340 -0
  80. data/spec/models/old_model_spec.rb +109 -0
  81. data/spec/models/reactive_array_spec.rb +262 -0
  82. data/spec/models/reactive_tags_spec.rb +35 -0
  83. data/spec/models/reactive_value_spec.rb +336 -0
  84. data/spec/models/string_extensions_spec.rb +57 -0
  85. data/spec/router/routes_spec.rb +24 -0
  86. data/spec/server/template_parser_spec.rb +50 -0
  87. data/spec/spec_helper.rb +20 -0
  88. data/spec/store/mongo_spec.rb +4 -0
  89. data/spec/templates/targets/binding_document/component_node_spec.rb +18 -0
  90. data/spec/templates/template_binding_spec.rb +98 -0
  91. data/templates/.gitignore +12 -0
  92. data/templates/Gemfile.tt +8 -0
  93. data/templates/app/.empty_directory +0 -0
  94. data/templates/app/home/config/routes.rb +1 -0
  95. data/templates/app/home/controllers/index_controller.rb +5 -0
  96. data/templates/app/home/css/.empty_directory +0 -0
  97. data/templates/app/home/models/.empty_directory +0 -0
  98. data/templates/app/home/views/index/about.html +9 -0
  99. data/templates/app/home/views/index/home.html +7 -0
  100. data/templates/app/home/views/index/index.html +28 -0
  101. data/templates/config.ru +4 -0
  102. data/templates/public/css/ansi.css +0 -0
  103. data/templates/public/css/bootstrap-theme.css +459 -0
  104. data/templates/public/css/bootstrap.css +7098 -0
  105. data/templates/public/css/jumbotron.css +79 -0
  106. data/templates/public/fonts/glyphicons-halflings-regular.eot +0 -0
  107. data/templates/public/fonts/glyphicons-halflings-regular.svg +229 -0
  108. data/templates/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  109. data/templates/public/fonts/glyphicons-halflings-regular.woff +0 -0
  110. data/templates/public/index.html +25 -0
  111. data/templates/public/js/bootstrap.js +0 -0
  112. data/templates/public/js/jquery-2.0.3.js +8829 -0
  113. data/templates/public/js/sockjs-0.2.1.min.js +27 -0
  114. data/templates/spec/spec_helper.rb +20 -0
  115. data/volt.gemspec +41 -0
  116. metadata +412 -0
@@ -0,0 +1,11 @@
1
+ require 'volt/templates/targets/binding_document/base_node'
2
+
3
+ class HtmlNode < BaseNode
4
+ def initialize(html)
5
+ @html = html
6
+ end
7
+
8
+ def to_html
9
+ @html
10
+ end
11
+ end
@@ -0,0 +1,147 @@
1
+ require 'volt/templates/targets/base_section'
2
+
3
+ class DomSection < BaseSection
4
+ def initialize(binding_name)
5
+ @start_node = find_by_comment("$#{binding_name}")
6
+ @end_node = find_by_comment("$/#{binding_name}")
7
+ end
8
+
9
+ def find_by_comment(text, in_node=`document`)
10
+ node = nil
11
+
12
+ %x{
13
+ node = document.evaluate("//comment()[. = ' " + text + " ']", in_node, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null).iterateNext();
14
+ }
15
+ return node
16
+ end
17
+
18
+ def text=(value)
19
+ %x{
20
+ this.$range().deleteContents();
21
+ this.$range().insertNode(document.createTextNode(#{value}));
22
+ }
23
+ end
24
+
25
+ def remove
26
+ range = self.range()
27
+
28
+ %x{
29
+ range.deleteContents();
30
+ }
31
+ end
32
+
33
+ def remove_anchors
34
+ %x{
35
+ this.start_node.parentNode.removeChild(this.start_node);
36
+ this.end_node.parentNode.removeChild(this.end_node);
37
+ }
38
+ @start_node = nil
39
+ @end_node = nil
40
+ end
41
+
42
+ def insert_anchor_before_end(binding_name)
43
+ Element.find(@end_node).before("<!-- $#{binding_name} --><!-- $/#{binding_name} -->")
44
+ end
45
+
46
+ # Takes in an array of dom nodes and replaces the current content
47
+ # with the new nodes
48
+ def nodes=(nodes)
49
+ range = self.range()
50
+
51
+ %x{
52
+ range.deleteContents();
53
+
54
+ for (var i=nodes.length-1; i >= 0; i--) {
55
+ var node = nodes[i];
56
+
57
+ node.parentNode.removeChild(node);
58
+ range.insertNode(node);
59
+ }
60
+ }
61
+ end
62
+
63
+ # Takes in our html and bindings, and rezero's the comment names, and the
64
+ # bindings. Returns an updated bindings hash
65
+ def set_content_and_rezero_bindings(html, bindings)
66
+ sub_nodes = nil
67
+ temp_div = nil
68
+
69
+ %x{
70
+ temp_div = document.createElement('div');
71
+ var doc = jQuery.parseHTML(html);
72
+
73
+ for (var i=0;i < doc.length;i++) {
74
+ temp_div.appendChild(doc[i]);
75
+ }
76
+ }
77
+
78
+ new_bindings = {}
79
+ # Loop through the bindings, and rezero.
80
+ bindings.each_pair do |name,binding|
81
+ new_name = @@binding_number
82
+
83
+ if name.cur.is_a?(String)
84
+ if name[0..1] == 'id'
85
+ # Find by id
86
+ %x{
87
+ var node = temp_div.querySelector('#' + name);
88
+ node.setAttribute('id', 'id' +new_name);
89
+ }
90
+
91
+ new_bindings["id#{new_name}"] = binding
92
+ else
93
+ # Assume a fixed id
94
+ # TODO: We should raise an exception if this id is already on the page
95
+ new_bindings[name] = binding
96
+ end
97
+ else
98
+ # Change the comment ids
99
+ start_comment = find_by_comment("$#{name}", temp_div)
100
+ end_comment = find_by_comment("$/#{name}", temp_div)
101
+
102
+ %x{
103
+ start_comment.textContent = " $" + new_name + " ";
104
+ end_comment.textContent = " $/" + new_name + " ";
105
+ }
106
+
107
+ new_bindings[new_name] = binding
108
+ end
109
+
110
+
111
+ @@binding_number += 1
112
+ end
113
+
114
+
115
+ children = nil
116
+ %x{
117
+ children = temp_div.childNodes;
118
+ }
119
+
120
+ # Update the nodes
121
+ self.nodes = children
122
+
123
+ %x{
124
+ temp_div = null;
125
+ }
126
+
127
+ return new_bindings
128
+ end
129
+
130
+ private
131
+
132
+ def range
133
+ return @range if @range
134
+
135
+ range = nil
136
+ %x{
137
+ range = document.createRange();
138
+ range.setStartAfter(this.start_node);
139
+ range.setEndBefore(this.end_node);
140
+ }
141
+
142
+ @range = range
143
+
144
+ return range
145
+ end
146
+
147
+ end
@@ -0,0 +1,11 @@
1
+ require 'volt/templates/targets/base_section'
2
+ require 'volt/templates/targets/dom_section'
3
+
4
+ # DomTarget's provide an interface that can render bindings into
5
+ # the dom. Currently only one "dom" is supported, but multiple
6
+ # may be allowed in the future (iframes?)
7
+ class DomTarget < BaseSection
8
+ def section(*args)
9
+ return DomSection.new(*args)
10
+ end
11
+ end
@@ -0,0 +1,159 @@
1
+ require 'volt/templates/base_binding'
2
+ require 'volt/templates/template_renderer'
3
+
4
+ class TemplateBinding < BaseBinding
5
+ def initialize(target, context, binding_name, binding_in_path, getter)
6
+ # puts "New template binding: #{context.inspect} - #{binding_name.inspect} - #{getter.inspect}"
7
+ super(target, context, binding_name)
8
+
9
+ # Binding in path is the path for the template this binding is in
10
+ setup_path(binding_in_path)
11
+
12
+ @current_template = nil
13
+
14
+ # puts "GETTER: #{value_from_getter(getter).inspect}"
15
+
16
+ # Find the source for the getter binding
17
+ @path, section = value_from_getter(getter)
18
+
19
+ if section.is_a?(String)
20
+ # Render this as a section
21
+ @section = section
22
+ else
23
+ # Use the value passed in as the default model
24
+ @model = section
25
+ end
26
+
27
+ # Run the initial render
28
+ update
29
+
30
+ @path_changed_listener = @path.on('changed') { update } if @path.reactive?
31
+ @section_changed_listener = @section.on('changed') { update } if @section && @section.reactive?
32
+ end
33
+
34
+ def setup_path(binding_in_path)
35
+ path_parts = binding_in_path.split('/')
36
+ @collection_name = path_parts[0]
37
+ @controller_name = path_parts[1]
38
+ @page_name = path_parts[2]
39
+ end
40
+
41
+ # Returns true if there is a template at the path
42
+ def check_for_template?(path)
43
+ $page.templates[path]
44
+ end
45
+
46
+ # Takes in a lookup path and returns the full path for the matching
47
+ # template. Also returns the controller name if applicable.
48
+ #
49
+ # Looking up a path is fairly simple. There are 4 parts needed to find
50
+ # the html to be rendered. File paths look like this:
51
+ # app/{component}/views/{controller_name}/{view}.html
52
+ # Within the html file may be one or more sections.
53
+ # 1. component (app/{comp})
54
+ # 2. controller
55
+ # 3. view
56
+ # 4. sections
57
+ #
58
+ # When searching for a file, the lookup starts at the section, and moves up.
59
+ # when moving up, default values are provided for the section, then view/section, etc..
60
+ # until a file is either found or the component level is reached.
61
+ #
62
+ # The defaults are as follows:
63
+ # 1. component - home
64
+ # 2. controller - index
65
+ # 3. view - index
66
+ # 4. section - body
67
+ def path_for_template(lookup_path, force_section=nil)
68
+ parts = lookup_path.split('/')
69
+ parts_size = parts.size
70
+
71
+ default_parts = ['home', 'index', 'index', 'body']
72
+
73
+ # When forcing a sub template, we can default the sub template section
74
+ default_parts[-1] = force_section if force_section
75
+
76
+ (5 - parts_size).times do |path_position|
77
+ # If they passed in a force_section, we can skip the first
78
+ next if force_section && path_position == 0
79
+
80
+ full_path = [@collection_name, @controller_name, @page_name, nil]
81
+
82
+ offset = 0
83
+ start_at = full_path.size - parts_size - path_position
84
+
85
+ full_path.size.times do |index|
86
+ if index >= start_at
87
+ if part = parts[index-start_at]
88
+ full_path[index] = part
89
+ else
90
+ full_path[index] = default_parts[index]
91
+ end
92
+ end
93
+ end
94
+
95
+ path = full_path.join('/')
96
+ if check_for_template?(path)
97
+ controller = nil
98
+ if path_position > 1
99
+ # Lookup the controller
100
+ controller = full_path[1]
101
+ end
102
+ return path, controller
103
+ end
104
+ end
105
+
106
+ return nil
107
+ end
108
+
109
+ def update
110
+ full_path, controller = path_for_template(@path.cur, @section.cur)
111
+
112
+ @current_template.remove if @current_template
113
+
114
+ current_context = @context
115
+
116
+ if @model
117
+ # Load in any procs
118
+ @model.each_pair do |key,value|
119
+ if value.class == Proc
120
+ @model[key] = value.call
121
+ end
122
+ end
123
+ end
124
+
125
+ # TODO: at the moment a :body section and a :title will both initialize different
126
+ # controllers. Maybe we should have a way to tie them together?
127
+ if controller
128
+ args = []
129
+ args << SubContext.new(@model) if @model
130
+ # Initialize the new controller
131
+ current_context = Object.send(:const_get, (controller.camelize + 'Controller').to_sym).new(*args)
132
+ elsif @model
133
+ # Passed in attributes, but there is no controller
134
+ current_context = SubContext.new(@model, current_context)
135
+ end
136
+
137
+ @current_template = TemplateRenderer.new(@target, current_context, @binding_name, full_path)
138
+ end
139
+
140
+ def remove
141
+ if @path_changed_listener
142
+ @path_changed_listener.remove
143
+ @path_changed_listener = nil
144
+ end
145
+
146
+ if @section_changed_listener
147
+ @section_changed_listener.remove
148
+ @section_changed_listener = nil
149
+ end
150
+
151
+ if @current_template
152
+ # Remove the template if one has been rendered, when the template binding is
153
+ # removed.
154
+ @current_template.remove
155
+ end
156
+
157
+ super
158
+ end
159
+ end
@@ -0,0 +1,50 @@
1
+ require 'volt/templates/base_binding'
2
+
3
+ class TemplateRenderer < BaseBinding
4
+ attr_reader :context
5
+ def initialize(target, context, binding_name, template_name)
6
+ # puts "new template renderer: #{context.inspect} - #{binding_name.inspect}"
7
+ super(target, context, binding_name)
8
+
9
+ # puts "Template Name: #{template_name}"
10
+ @template = $page.templates[template_name]
11
+ @sub_bindings = []
12
+
13
+ if @template
14
+ html = @template['html']
15
+ bindings = @template['bindings']
16
+ else
17
+ html = "<div>-- &lt; missing template #{template_name.inspect.gsub('<', '&lt;').gsub('>', '&gt;')} &gt; --</div>"
18
+ bindings = {}
19
+ end
20
+
21
+ bindings = self.section.set_content_and_rezero_bindings(html, bindings)
22
+
23
+ bindings.each_pair do |id,bindings_for_id|
24
+ bindings_for_id.each do |binding|
25
+ @sub_bindings << binding.call(target, context, id)
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ def remove
32
+ # puts "Remove Template: #{self} - #{@sub_bindings.inspect}"
33
+
34
+ # Remove all of the sub-bindings
35
+ # @sub_bindings.each(&:remove)
36
+
37
+ @sub_bindings.each do |binding|
38
+ # puts "REMOVE: #{binding.inspect}"
39
+ binding.remove
40
+ # puts "REMOVED"
41
+ end
42
+
43
+ @sub_bindings = []
44
+ super
45
+ end
46
+
47
+ def remove_anchors
48
+ section.remove_anchors
49
+ end
50
+ end
@@ -0,0 +1,129 @@
1
+ require 'volt/models'
2
+
3
+ describe EventChain do
4
+ before do
5
+ @a = ReactiveValue.new(1)
6
+ @b = ReactiveValue.new(2)
7
+ end
8
+
9
+ it "should chain events when we use add_object" do
10
+
11
+ count = 0
12
+ @b.on('changed') { count += 1 }
13
+ @b.reactive_manager.event_chain.add_object(@a)
14
+ expect(count).to eq(0)
15
+
16
+ @a.trigger!('changed')
17
+ expect(count).to eq(1)
18
+ end
19
+
20
+ it "should chain events after add_object is called" do
21
+ @b.reactive_manager.event_chain.add_object(@a)
22
+
23
+ add_count = 0
24
+ @b.on('added') { add_count += 1 }
25
+ expect(add_count).to eq(0)
26
+ @a.trigger!('added')
27
+
28
+ expect(add_count).to eq(1)
29
+ end
30
+
31
+
32
+ it "should remove events" do
33
+ @b.reactive_manager.event_chain.add_object(@a)
34
+
35
+ add_count = 0
36
+ listener = @b.on('added') { add_count += 1 }
37
+ expect(add_count).to eq(0)
38
+ @a.trigger!('added')
39
+
40
+ expect(add_count).to eq(1)
41
+
42
+ # Make sure the event is registered
43
+ expect(@a.reactive_manager.listeners.size).to eq(1)
44
+ expect(@b.reactive_manager.event_chain.instance_variable_get('@event_chain').values[0].keys.include?(:added)).to eq(true)
45
+
46
+ listener.remove
47
+
48
+ # Make sure its removed
49
+ expect(@a.reactive_manager.listeners.size).to eq(0)
50
+ expect(@b.reactive_manager.event_chain.instance_variable_get('@event_chain').values[0].keys.include?(:added)).to eq(false)
51
+
52
+ @a.trigger!('added')
53
+ expect(add_count).to eq(1)
54
+ end
55
+
56
+ it "should unchain directly" do
57
+ count = 0
58
+ a = ReactiveValue.new(Model.new)
59
+ b = a._name
60
+ listener = b.on('changed') { count += 1 }
61
+
62
+ expect(b.reactive_manager.listeners[:changed].size).to eq(1)
63
+ # TODO: ideally this would only bind 1 to a
64
+ expect(a.reactive_manager.listeners[:changed].size).to eq(1)
65
+
66
+ listener.remove
67
+
68
+ expect(b.reactive_manager.listeners[:changed]).to eq(nil)
69
+ expect(a.reactive_manager.listeners[:changed]).to eq(nil)
70
+ end
71
+
72
+ it "should unchain" do
73
+ count = 0
74
+ @b.on('changed') { count += 1 }
75
+ b_object_listener = @b.reactive_manager.event_chain.add_object(@a)
76
+ expect(count).to eq(0)
77
+
78
+ @a.trigger!('changed')
79
+ expect(count).to eq(1)
80
+
81
+ b_object_listener.remove
82
+
83
+ @a.trigger!('changed')
84
+ expect(count).to eq(1)
85
+ end
86
+
87
+ it "should unchain up the chain" do
88
+ count = 0
89
+ a = ReactiveValue.new(Model.new)
90
+
91
+ b = a._list
92
+ expect(a.reactive_manager.listeners.size).to eq(0)
93
+ listener = b.on('changed') { count += 1 }
94
+
95
+ expect(a.reactive_manager.listeners.size).to eq(1)
96
+
97
+ listener.remove
98
+
99
+ expect(a.reactive_manager.listeners.size).to eq(0)
100
+ end
101
+
102
+ describe "double add/removes" do
103
+ it "should unchain" do
104
+ c = ReactiveValue.new(3)
105
+ count = 0
106
+ @b.on('changed') { count += 1 }
107
+ c.on('changed') { count += 1 }
108
+
109
+
110
+ # Chain b to a
111
+ b_object_listener = @b.reactive_manager.event_chain.add_object(@a)
112
+ c_object_listener = c.reactive_manager.event_chain.add_object(@a)
113
+ expect(count).to eq(0)
114
+
115
+ @a.trigger!('changed')
116
+ expect(count).to eq(2)
117
+
118
+ b_object_listener.remove
119
+
120
+ @a.trigger!('changed')
121
+ expect(count).to eq(3)
122
+
123
+ c_object_listener.remove
124
+
125
+ @a.trigger!('changed')
126
+ expect(count).to eq(3)
127
+ end
128
+ end
129
+ end