volt 0.7.23 → 0.8.0

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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -1
  3. data/CHANGELOG.md +22 -0
  4. data/Gemfile +8 -0
  5. data/Guardfile +2 -2
  6. data/Readme.md +139 -136
  7. data/VERSION +1 -1
  8. data/app/volt/assets/js/setImmediate.js +175 -0
  9. data/app/volt/tasks/live_query/data_store.rb +0 -2
  10. data/app/volt/tasks/live_query/live_query.rb +4 -4
  11. data/docs/GETTING_STARTED.md +24 -3
  12. data/docs/WHY.md +1 -22
  13. data/lib/volt.rb +20 -1
  14. data/lib/volt/console.rb +20 -0
  15. data/lib/volt/controllers/model_controller.rb +25 -11
  16. data/lib/volt/extra_core/object.rb +2 -14
  17. data/lib/volt/extra_core/string.rb +4 -0
  18. data/lib/volt/models.rb +0 -1
  19. data/lib/volt/models/array_model.rb +8 -16
  20. data/lib/volt/models/cursor.rb +1 -1
  21. data/lib/volt/models/model.rb +40 -60
  22. data/lib/volt/models/model_hash_behaviour.rb +10 -24
  23. data/lib/volt/models/model_helpers.rb +2 -2
  24. data/lib/volt/models/model_state.rb +1 -1
  25. data/lib/volt/models/model_wrapper.rb +4 -4
  26. data/lib/volt/models/persistors/array_store.rb +44 -28
  27. data/lib/volt/models/persistors/base.rb +1 -1
  28. data/lib/volt/models/persistors/model_store.rb +1 -1
  29. data/lib/volt/models/persistors/params.rb +5 -1
  30. data/lib/volt/models/persistors/query/query_listener.rb +2 -0
  31. data/lib/volt/models/persistors/store.rb +3 -2
  32. data/lib/volt/models/persistors/store_state.rb +7 -2
  33. data/lib/volt/models/url.rb +35 -29
  34. data/lib/volt/models/validations.rb +7 -17
  35. data/lib/volt/page/bindings/attribute_binding.rb +57 -39
  36. data/lib/volt/page/bindings/base_binding.rb +0 -14
  37. data/lib/volt/page/bindings/content_binding.rb +15 -18
  38. data/lib/volt/page/bindings/each_binding.rb +67 -34
  39. data/lib/volt/page/bindings/if_binding.rb +15 -12
  40. data/lib/volt/page/bindings/template_binding.rb +77 -59
  41. data/lib/volt/page/bindings/template_binding/grouped_controllers.rb +19 -4
  42. data/lib/volt/page/channel.rb +22 -38
  43. data/lib/volt/page/channel_stub.rb +3 -6
  44. data/lib/volt/page/page.rb +24 -26
  45. data/lib/volt/page/string_template_renderer.rb +46 -0
  46. data/lib/volt/page/sub_context.rb +7 -1
  47. data/lib/volt/page/targets/binding_document/component_node.rb +11 -9
  48. data/lib/volt/page/tasks.rb +3 -2
  49. data/lib/volt/page/url_tracker.rb +4 -3
  50. data/lib/volt/reactive/computation.rb +131 -0
  51. data/lib/volt/reactive/dependency.rb +71 -0
  52. data/lib/volt/reactive/eventable.rb +82 -0
  53. data/lib/volt/reactive/hash_dependency.rb +36 -0
  54. data/lib/volt/{controllers → reactive}/reactive_accessors.rb +8 -11
  55. data/lib/volt/reactive/reactive_array.rb +100 -193
  56. data/lib/volt/reactive/reactive_hash.rb +49 -0
  57. data/lib/volt/server/html_parser/attribute_scope.rb +24 -4
  58. data/lib/volt/server/html_parser/if_view_scope.rb +15 -15
  59. data/lib/volt/server/html_parser/view_scope.rb +31 -1
  60. data/spec/apps/kitchen_sink/Gemfile +4 -8
  61. data/spec/apps/kitchen_sink/app/main/config/dependencies.rb +8 -0
  62. data/spec/apps/kitchen_sink/app/main/config/routes.rb +8 -1
  63. data/spec/apps/kitchen_sink/app/main/controllers/main_controller.rb +8 -0
  64. data/spec/apps/kitchen_sink/app/main/views/main/bindings.html +73 -0
  65. data/spec/apps/kitchen_sink/app/main/views/main/index.html +6 -1
  66. data/spec/apps/kitchen_sink/app/main/views/main/main.html +26 -6
  67. data/spec/apps/kitchen_sink/app/main/views/main/store.html +6 -0
  68. data/spec/controllers/reactive_accessors_spec.rb +13 -15
  69. data/spec/integration/bindings_spec.rb +159 -0
  70. data/spec/integration/templates_spec.rb +15 -0
  71. data/spec/models/model_spec.rb +130 -228
  72. data/spec/reactive/computation_spec.rb +63 -0
  73. data/spec/reactive/dependency_spec.rb +5 -0
  74. data/spec/reactive/eventable_spec.rb +48 -0
  75. data/spec/reactive/reactive_array_spec.rb +97 -0
  76. data/spec/router/routes_spec.rb +26 -27
  77. data/spec/server/html_parser/view_parser_spec.rb +3 -21
  78. data/spec/server/rack/asset_files_spec.rb +1 -1
  79. data/templates/project/app/main/views/main/main.html +2 -2
  80. metadata +29 -41
  81. data/lib/volt/extra_core/time.rb +0 -16
  82. data/lib/volt/page/draw_cycle.rb +0 -31
  83. data/lib/volt/page/memory_test.rb +0 -26
  84. data/lib/volt/page/reactive_template.rb +0 -32
  85. data/lib/volt/reactive/array_extensions.rb +0 -12
  86. data/lib/volt/reactive/destructive_methods.rb +0 -19
  87. data/lib/volt/reactive/event_chain.rb +0 -125
  88. data/lib/volt/reactive/events.rb +0 -216
  89. data/lib/volt/reactive/object_tracking.rb +0 -14
  90. data/lib/volt/reactive/reactive_block.rb +0 -88
  91. data/lib/volt/reactive/reactive_generator.rb +0 -44
  92. data/lib/volt/reactive/reactive_tags.rb +0 -71
  93. data/lib/volt/reactive/reactive_value.rb +0 -427
  94. data/lib/volt/reactive/string_extensions.rb +0 -31
  95. data/spec/integration/test_integration_spec.rb +0 -14
  96. data/spec/models/event_chain_spec.rb +0 -150
  97. data/spec/models/model_buffers_spec.rb +0 -9
  98. data/spec/models/old_model_spec.rb +0 -67
  99. data/spec/models/reactive_array_spec.rb +0 -364
  100. data/spec/models/reactive_block_spec.rb +0 -13
  101. data/spec/models/reactive_call_times_spec.rb +0 -28
  102. data/spec/models/reactive_generator_spec.rb +0 -58
  103. data/spec/models/reactive_tags_spec.rb +0 -35
  104. data/spec/models/reactive_value_spec.rb +0 -370
  105. data/spec/models/store_spec.rb +0 -16
  106. data/spec/models/string_extensions_spec.rb +0 -57
@@ -15,7 +15,7 @@ module Persistors
15
15
  changed(attribute_name)
16
16
  end
17
17
 
18
- def event_added(event, scope_provider, first, first_for_event)
18
+ def event_added(event, first, first_for_event)
19
19
  end
20
20
 
21
21
  def event_removed(event, last, last_for_event)
@@ -82,7 +82,7 @@ module Persistors
82
82
  return promise
83
83
  end
84
84
 
85
- def event_added(event, scope_provider, first, first_for_event)
85
+ def event_added(event, first, first_for_event)
86
86
  if first_for_event && event == :changed
87
87
  ensure_setup
88
88
  end
@@ -20,7 +20,11 @@ module Persistors
20
20
  end
21
21
 
22
22
  def run_update
23
- $page.params.trigger!('child_changed') if Volt.client?
23
+ # TODORW:
24
+ # $page.params.trigger!('child_changed') if Volt.client?
25
+ if Volt.client?
26
+ $page.url.update!
27
+ end
24
28
  end
25
29
  end
26
30
  end
@@ -16,12 +16,14 @@ class QueryListener
16
16
  def add_listener
17
17
  @listening = true
18
18
  @tasks.call('QueryTasks', 'add_listener', @collection, @query) do |results, errors|
19
+ # puts "Query Tasks: #{results.inspect} - #{@stores.inspect} - #{self.inspect}"
19
20
  # When the initial data comes back, add it into the stores.
20
21
  @stores.each do |store|
21
22
  # Clear if there are existing items
22
23
  store.model.clear if store.model.size > 0
23
24
 
24
25
  results.each do |index, data|
26
+ # puts "ADD: #{index} - #{data.inspect}"
25
27
  store.add(index, data)
26
28
  end
27
29
 
@@ -27,10 +27,11 @@ module Persistors
27
27
  model = @model.new_array_model([], options)
28
28
  else
29
29
  model = @model.new_model(nil, options)
30
+
31
+ @model.attributes ||= {}
32
+ @model.attributes[method_name] = model
30
33
  end
31
34
 
32
- @model.attributes ||= {}
33
- @model.attributes[method_name] = model
34
35
 
35
36
  return model
36
37
  end
@@ -7,7 +7,10 @@ module StoreState
7
7
  end
8
8
 
9
9
  def state
10
- @state
10
+ @state_dep ||= Dependency.new
11
+ @state_dep.depend
12
+
13
+ return @state
11
14
  end
12
15
 
13
16
  # Called from the QueryListener when the data is loaded
@@ -18,7 +21,7 @@ module StoreState
18
21
  # Trigger changed on the 'state' method
19
22
  unless skip_trigger
20
23
  if old_state != @state
21
- @model.trigger_for_methods!('changed', :state, :loaded?)
24
+ @state_dep.changed! if @state_dep
22
25
  end
23
26
  end
24
27
 
@@ -26,6 +29,8 @@ module StoreState
26
29
  # Trigger each waiting fetch
27
30
  @fetch_promises.compact.each {|fp| fp.resolve(@model) }
28
31
  @fetch_promises = nil
32
+
33
+ stop_listening
29
34
  end
30
35
  end
31
36
 
@@ -1,9 +1,11 @@
1
1
  # The url class handles parsing and updating the url
2
+ require 'volt/reactive/reactive_accessors'
3
+
2
4
  class URL
3
- include ReactiveTags
5
+ include ReactiveAccessors
4
6
 
5
7
  # TODO: we need to make it so change events only trigger on changes
6
- attr_reader :scheme, :host, :port, :path, :query, :params
8
+ reactive_accessor :scheme, :host, :port, :path, :query, :params, :fragment
7
9
  attr_accessor :router
8
10
 
9
11
  def initialize(router=nil)
@@ -13,13 +15,10 @@ class URL
13
15
 
14
16
  # Parse takes in a url and extracts each sections.
15
17
  # It also assigns and changes to the params.
16
- tag_method(:parse) do
17
- destructive!
18
- end
19
18
  def parse(url)
20
19
  if url[0] == '#'
21
20
  # url only updates fragment
22
- @fragment = url[1..-1]
21
+ self.fragment = url[1..-1]
23
22
  update!
24
23
  else
25
24
  host = `document.location.host`
@@ -37,41 +36,45 @@ class URL
37
36
  end
38
37
 
39
38
  matcher = url.match(/^(#{protocol[0..-2]})[:]\/\/([^\/]+)(.*)$/)
40
- @scheme = matcher[1]
41
- @host, @port = matcher[2].split(':')
42
- @port ||= 80
39
+ self.scheme = matcher[1]
40
+ host, port = matcher[2].split(':')
41
+ port ||= 80
42
+
43
+ self.host = host
44
+ self.port = port
43
45
 
44
- @path = matcher[3]
45
- @path, @fragment = @path.split('#', 2)
46
- @path, @query = @path.split('?', 2)
46
+ path = matcher[3]
47
+ path, fragment = path.split('#', 2)
48
+ path, query = path.split('?', 2)
49
+
50
+ self.path = path
51
+ self.fragment = fragment
52
+ self.query = query
47
53
 
48
54
  assign_query_hash_to_params
49
55
  end
50
56
 
51
57
  scroll
52
58
 
53
- trigger_for_methods!('changed', :path)
54
-
55
59
  return true
56
60
  end
57
61
 
58
62
  # Full url rebuilds the url from it's constituent parts
59
63
  def full_url
60
- if @port
61
- host_with_port = "#{@host}:#{@port}"
64
+ if port
65
+ host_with_port = "#{host}:#{port}"
62
66
  else
63
- host_with_port = @host
67
+ host_with_port = host
64
68
  end
65
69
 
66
- path, params = @router.params_to_url(@params.deep_cur.to_h)
70
+ path, params = @router.params_to_url(@params.to_h)
67
71
 
68
- new_url = "#{@scheme}://#{host_with_port}#{(path || @path).chomp('/')}"
72
+ new_url = "#{scheme}://#{host_with_port}#{(path || self.path).chomp('/')}"
69
73
 
70
74
  unless params.empty?
71
75
  new_url += '?'
72
76
  query_parts = []
73
77
  nested_params_hash(params).each_pair do |key,value|
74
- value = value.cur
75
78
  # remove the _ from the front
76
79
  value = `encodeURI(value)`
77
80
  query_parts << "#{key}=#{value}"
@@ -80,7 +83,8 @@ class URL
80
83
  new_url += query_parts.join('&')
81
84
  end
82
85
 
83
- new_url += '#' + @fragment if @fragment
86
+ frag = self.fragment
87
+ new_url += '#' + frag if frag.present?
84
88
 
85
89
  return new_url
86
90
  end
@@ -104,12 +108,13 @@ class URL
104
108
 
105
109
  def scroll
106
110
  if Volt.client?
107
- if @fragment
111
+ frag = self.fragment
112
+ if frag
108
113
  # Scroll to anchor via http://www.w3.org/html/wg/drafts/html/master/browsers.html#scroll-to-fragid
109
114
  %x{
110
- var anchor = $('#' + this.fragment);
115
+ var anchor = $('#' + frag);
111
116
  if (anchor.length == 0) {
112
- anchor = $('*[name="' + this.fragment + '"]:first');
117
+ anchor = $('*[name="' + frag + '"]:first');
113
118
  }
114
119
  if (anchor && anchor.length > 0) {
115
120
  console.log('scroll to: ', anchor.offset().top);
@@ -134,10 +139,10 @@ class URL
134
139
  query_hash = self.query_hash
135
140
 
136
141
  # Get the params that are in the route
137
- new_params = @router.url_to_params(@path)
142
+ new_params = @router.url_to_params(path)
138
143
 
139
144
  if new_params == false
140
- raise "no routes match path: #{@path}"
145
+ raise "no routes match path: #{path}"
141
146
  end
142
147
 
143
148
  query_hash.merge!(new_params)
@@ -153,7 +158,7 @@ class URL
153
158
  def assign_from_old(params, new_params)
154
159
  queued_deletes = []
155
160
 
156
- params.cur.attributes.each_pair do |name,old_val|
161
+ params.attributes.each_pair do |name,old_val|
157
162
  # If there is a new value, see if it has [name]
158
163
  new_val = new_params ? new_params[name] : nil
159
164
 
@@ -188,8 +193,9 @@ class URL
188
193
 
189
194
  def query_hash
190
195
  query_hash = {}
191
- if @query
192
- @query.split('&').reject {|v| v == '' }.each do |part|
196
+ qury = self.query
197
+ if qury
198
+ qury.split('&').reject {|v| v == '' }.each do |part|
193
199
  parts = part.split('=').reject {|v| v == '' }
194
200
 
195
201
  # Decode string
@@ -19,32 +19,22 @@ module Validations
19
19
  base.send :extend, ClassMethods
20
20
  end
21
21
 
22
- # Sometimes we want to skip checking a field until some event
23
- # has happened (usually a field has been typed in or blurred)
24
- def exclude_from_errors!(field_name)
25
- @exclude_from_errors ||= {}
26
- @exclude_from_errors[field_name] = true
27
-
28
- @include_in_errors.delete(field_name) if @include_in_errors
29
-
30
- trigger_for_methods!('changed', :errors, :marked_errors)
31
- end
32
-
33
22
  # Once a field is ready, we can use include_in_errors! to start
34
23
  # showing its errors.
35
24
  def mark_field!(field_name, trigger_changed=true)
36
- @marked_fields ||= {}
37
- @marked_fields[field_name] = true
25
+ marked_fields[field_name] = true
26
+ end
38
27
 
39
- if trigger_changed
40
- trigger_for_methods!('changed', :errors, :marked_errors)
41
- end
28
+ def marked_fields
29
+ @marked_fields ||= ReactiveHash.new
42
30
  end
43
31
 
44
32
  def marked_errors
45
33
  errors(true)
46
34
  end
47
35
 
36
+ # TODO: Errors is being called for any validation change. We should have errors return a
37
+ # hash like object that only calls the validation for each one.
48
38
  def errors(marked_only=false)
49
39
  errors = {}
50
40
 
@@ -62,7 +52,7 @@ module Validations
62
52
  validations.each_pair do |field_name, options|
63
53
  if marked_only
64
54
  # When marked only, skip any validations on non-marked fields
65
- next unless @marked_fields && @marked_fields[field_name]
55
+ next unless marked_fields[field_name]
66
56
  end
67
57
 
68
58
  options.each_pair do |validation, args|
@@ -2,33 +2,39 @@ require 'volt/page/bindings/base_binding'
2
2
  require 'volt/page/targets/attribute_target'
3
3
 
4
4
  class AttributeBinding < BaseBinding
5
- def initialize(page, target, context, binding_name, attribute_name, getter)
5
+ def initialize(page, target, context, binding_name, attribute_name, getter, setter)
6
6
  super(page, target, context, binding_name)
7
7
 
8
8
  @attribute_name = attribute_name
9
9
  @getter = getter
10
+ @setter = setter
10
11
 
11
12
  setup
12
13
  end
13
14
 
14
15
  def setup
15
16
 
16
- # Find the source for the content binding
17
- @value = value_from_getter(@getter)
18
-
19
- # Run the initial update (render)
20
- update
17
+ # Listen for changes
18
+ @computation = -> do
19
+ begin
20
+ update(@context.instance_eval(&@getter))
21
+ rescue => e
22
+ Volt.logger.error("AttributeBinding Error: #{e.inspect}")
23
+ update('')
24
+ end
25
+ end.watch!
21
26
 
22
- if @value.reactive?
23
- @update_listener = @value.on('changed') { update }
27
+ @is_radio = element.is('[type=radio]')
28
+ if @is_radio
29
+ @selected_value = element.attr('value')
30
+ end
24
31
 
25
- # Bind so when this value updates, we update
26
- case @attribute_name
27
- when 'value'
28
- element.on('input.attrbind') { changed }
29
- when 'checked'
30
- element.on('change.attrbind') {|event| changed(event) }
31
- end
32
+ # Bind so when this value updates, we update
33
+ case @attribute_name
34
+ when 'value'
35
+ element.on('input.attrbind') { changed }
36
+ when 'checked'
37
+ element.on('change.attrbind') {|event| changed(event) }
32
38
  end
33
39
  end
34
40
 
@@ -40,26 +46,45 @@ class AttributeBinding < BaseBinding
40
46
  current_value = element.is(':checked')
41
47
  end
42
48
 
43
- @value.cur = current_value
49
+ if @is_radio
50
+ if current_value
51
+ # if it is a radio button and its checked
52
+ @context.instance_exec(@selected_value, &@setter)
53
+ end
54
+ else
55
+ @context.instance_exec(current_value, &@setter)
56
+ end
44
57
  end
45
58
 
46
59
  def element
47
60
  Element.find('#' + binding_name)
48
61
  end
49
62
 
50
- def update
51
- value = @value.cur
52
-
63
+ def update(new_value)
53
64
  if @attribute_name == 'checked'
54
- update_checked
65
+ update_checked(new_value)
55
66
  return
56
67
  end
57
68
 
58
- if value.is_a?(NilMethodCall) || value.nil?
59
- value = ''
60
- end
69
+ # Stop any previous reactive template computations
70
+ @string_template_renderer_computation.stop if @string_template_renderer_computation
71
+ @string_template_renderer.remove if @string_template_renderer
61
72
 
62
- self.value = value
73
+ if new_value.is_a?(StringTemplateRender)
74
+ # We don't need to refetch the whole reactive template to
75
+ # update, we can just depend on it and update directly.
76
+ @string_template_renderer = new_value
77
+
78
+ @string_template_renderer_computation = -> do
79
+ self.value = @string_template_renderer.html
80
+ end.watch!
81
+ else
82
+ if new_value.is_a?(NilMethodCall) || new_value.nil?
83
+ new_value = ''
84
+ end
85
+
86
+ self.value = new_value
87
+ end
63
88
  end
64
89
 
65
90
  def value=(val)
@@ -75,15 +100,16 @@ class AttributeBinding < BaseBinding
75
100
  end
76
101
  end
77
102
 
78
- def update_checked
79
- value = @value.cur
80
-
103
+ def update_checked(value)
81
104
  if value.is_a?(NilMethodCall) || value.nil?
82
105
  value = false
83
106
  end
84
107
 
85
- element.prop('checked', value)
108
+ if @is_radio
109
+ value = (@selected_value == value)
110
+ end
86
111
 
112
+ element.prop('checked', value)
87
113
  end
88
114
 
89
115
  def remove
@@ -96,22 +122,14 @@ class AttributeBinding < BaseBinding
96
122
  element.off('change.attrbind', nil)
97
123
  end
98
124
 
99
- # Value is a reactive template, remove it
100
- if @value && @value.reactive?
101
- @value.remove
102
- end
103
-
104
-
105
- if @update_listener
106
- @update_listener.remove
107
- @update_listener = nil
108
- end
125
+ @string_template_renderer.remove if @string_template_renderer
126
+ @string_template_renderer_computation.stop if @string_template_renderer_computation
127
+ @computation.stop if @computation
109
128
 
110
129
  # Clear any references
111
130
  @target = nil
112
131
  @context = nil
113
132
  @getter = nil
114
- @value = nil
115
133
  end
116
134
 
117
135
  def remove_anchors
@@ -36,18 +36,4 @@ class BaseBinding
36
36
  def remove_anchors
37
37
  @dom_section.remove_anchors if @dom_section
38
38
  end
39
-
40
- def queue_update
41
- if Volt.server?
42
- # Run right away
43
- update
44
- else
45
- @page.draw_cycle.queue(self)
46
- end
47
- end
48
-
49
- def value_from_getter(getter)
50
- # Evaluate the getter proc in the context
51
- return @context.instance_eval(&getter)
52
- end
53
39
  end