volt 0.2.3

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