volt 0.7.23 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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