volt 0.7.23 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +8 -1
- data/CHANGELOG.md +22 -0
- data/Gemfile +8 -0
- data/Guardfile +2 -2
- data/Readme.md +139 -136
- data/VERSION +1 -1
- data/app/volt/assets/js/setImmediate.js +175 -0
- data/app/volt/tasks/live_query/data_store.rb +0 -2
- data/app/volt/tasks/live_query/live_query.rb +4 -4
- data/docs/GETTING_STARTED.md +24 -3
- data/docs/WHY.md +1 -22
- data/lib/volt.rb +20 -1
- data/lib/volt/console.rb +20 -0
- data/lib/volt/controllers/model_controller.rb +25 -11
- data/lib/volt/extra_core/object.rb +2 -14
- data/lib/volt/extra_core/string.rb +4 -0
- data/lib/volt/models.rb +0 -1
- data/lib/volt/models/array_model.rb +8 -16
- data/lib/volt/models/cursor.rb +1 -1
- data/lib/volt/models/model.rb +40 -60
- data/lib/volt/models/model_hash_behaviour.rb +10 -24
- data/lib/volt/models/model_helpers.rb +2 -2
- data/lib/volt/models/model_state.rb +1 -1
- data/lib/volt/models/model_wrapper.rb +4 -4
- data/lib/volt/models/persistors/array_store.rb +44 -28
- data/lib/volt/models/persistors/base.rb +1 -1
- data/lib/volt/models/persistors/model_store.rb +1 -1
- data/lib/volt/models/persistors/params.rb +5 -1
- data/lib/volt/models/persistors/query/query_listener.rb +2 -0
- data/lib/volt/models/persistors/store.rb +3 -2
- data/lib/volt/models/persistors/store_state.rb +7 -2
- data/lib/volt/models/url.rb +35 -29
- data/lib/volt/models/validations.rb +7 -17
- data/lib/volt/page/bindings/attribute_binding.rb +57 -39
- data/lib/volt/page/bindings/base_binding.rb +0 -14
- data/lib/volt/page/bindings/content_binding.rb +15 -18
- data/lib/volt/page/bindings/each_binding.rb +67 -34
- data/lib/volt/page/bindings/if_binding.rb +15 -12
- data/lib/volt/page/bindings/template_binding.rb +77 -59
- data/lib/volt/page/bindings/template_binding/grouped_controllers.rb +19 -4
- data/lib/volt/page/channel.rb +22 -38
- data/lib/volt/page/channel_stub.rb +3 -6
- data/lib/volt/page/page.rb +24 -26
- data/lib/volt/page/string_template_renderer.rb +46 -0
- data/lib/volt/page/sub_context.rb +7 -1
- data/lib/volt/page/targets/binding_document/component_node.rb +11 -9
- data/lib/volt/page/tasks.rb +3 -2
- data/lib/volt/page/url_tracker.rb +4 -3
- data/lib/volt/reactive/computation.rb +131 -0
- data/lib/volt/reactive/dependency.rb +71 -0
- data/lib/volt/reactive/eventable.rb +82 -0
- data/lib/volt/reactive/hash_dependency.rb +36 -0
- data/lib/volt/{controllers → reactive}/reactive_accessors.rb +8 -11
- data/lib/volt/reactive/reactive_array.rb +100 -193
- data/lib/volt/reactive/reactive_hash.rb +49 -0
- data/lib/volt/server/html_parser/attribute_scope.rb +24 -4
- data/lib/volt/server/html_parser/if_view_scope.rb +15 -15
- data/lib/volt/server/html_parser/view_scope.rb +31 -1
- data/spec/apps/kitchen_sink/Gemfile +4 -8
- data/spec/apps/kitchen_sink/app/main/config/dependencies.rb +8 -0
- data/spec/apps/kitchen_sink/app/main/config/routes.rb +8 -1
- data/spec/apps/kitchen_sink/app/main/controllers/main_controller.rb +8 -0
- data/spec/apps/kitchen_sink/app/main/views/main/bindings.html +73 -0
- data/spec/apps/kitchen_sink/app/main/views/main/index.html +6 -1
- data/spec/apps/kitchen_sink/app/main/views/main/main.html +26 -6
- data/spec/apps/kitchen_sink/app/main/views/main/store.html +6 -0
- data/spec/controllers/reactive_accessors_spec.rb +13 -15
- data/spec/integration/bindings_spec.rb +159 -0
- data/spec/integration/templates_spec.rb +15 -0
- data/spec/models/model_spec.rb +130 -228
- data/spec/reactive/computation_spec.rb +63 -0
- data/spec/reactive/dependency_spec.rb +5 -0
- data/spec/reactive/eventable_spec.rb +48 -0
- data/spec/reactive/reactive_array_spec.rb +97 -0
- data/spec/router/routes_spec.rb +26 -27
- data/spec/server/html_parser/view_parser_spec.rb +3 -21
- data/spec/server/rack/asset_files_spec.rb +1 -1
- data/templates/project/app/main/views/main/main.html +2 -2
- metadata +29 -41
- data/lib/volt/extra_core/time.rb +0 -16
- data/lib/volt/page/draw_cycle.rb +0 -31
- data/lib/volt/page/memory_test.rb +0 -26
- data/lib/volt/page/reactive_template.rb +0 -32
- data/lib/volt/reactive/array_extensions.rb +0 -12
- data/lib/volt/reactive/destructive_methods.rb +0 -19
- data/lib/volt/reactive/event_chain.rb +0 -125
- data/lib/volt/reactive/events.rb +0 -216
- data/lib/volt/reactive/object_tracking.rb +0 -14
- data/lib/volt/reactive/reactive_block.rb +0 -88
- data/lib/volt/reactive/reactive_generator.rb +0 -44
- data/lib/volt/reactive/reactive_tags.rb +0 -71
- data/lib/volt/reactive/reactive_value.rb +0 -427
- data/lib/volt/reactive/string_extensions.rb +0 -31
- data/spec/integration/test_integration_spec.rb +0 -14
- data/spec/models/event_chain_spec.rb +0 -150
- data/spec/models/model_buffers_spec.rb +0 -9
- data/spec/models/old_model_spec.rb +0 -67
- data/spec/models/reactive_array_spec.rb +0 -364
- data/spec/models/reactive_block_spec.rb +0 -13
- data/spec/models/reactive_call_times_spec.rb +0 -28
- data/spec/models/reactive_generator_spec.rb +0 -58
- data/spec/models/reactive_tags_spec.rb +0 -35
- data/spec/models/reactive_value_spec.rb +0 -370
- data/spec/models/store_spec.rb +0 -16
- data/spec/models/string_extensions_spec.rb +0 -57
@@ -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
|
-
@
|
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
|
-
@
|
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
|
|
data/lib/volt/models/url.rb
CHANGED
@@ -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
|
5
|
+
include ReactiveAccessors
|
4
6
|
|
5
7
|
# TODO: we need to make it so change events only trigger on changes
|
6
|
-
|
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
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
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
|
61
|
-
host_with_port = "#{
|
64
|
+
if port
|
65
|
+
host_with_port = "#{host}:#{port}"
|
62
66
|
else
|
63
|
-
host_with_port =
|
67
|
+
host_with_port = host
|
64
68
|
end
|
65
69
|
|
66
|
-
path, params = @router.params_to_url(@params.
|
70
|
+
path, params = @router.params_to_url(@params.to_h)
|
67
71
|
|
68
|
-
new_url = "#{
|
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
|
-
|
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
|
-
|
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 = $('#' +
|
115
|
+
var anchor = $('#' + frag);
|
111
116
|
if (anchor.length == 0) {
|
112
|
-
anchor = $('*[name="' +
|
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(
|
142
|
+
new_params = @router.url_to_params(path)
|
138
143
|
|
139
144
|
if new_params == false
|
140
|
-
raise "no routes match 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.
|
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
|
-
|
192
|
-
|
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
|
-
|
37
|
-
|
25
|
+
marked_fields[field_name] = true
|
26
|
+
end
|
38
27
|
|
39
|
-
|
40
|
-
|
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
|
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
|
-
#
|
17
|
-
@
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
23
|
-
|
27
|
+
@is_radio = element.is('[type=radio]')
|
28
|
+
if @is_radio
|
29
|
+
@selected_value = element.attr('value')
|
30
|
+
end
|
24
31
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
@
|
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
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
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
|