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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +37 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/Rakefile +23 -0
- data/Readme.md +34 -0
- data/VERSION +1 -0
- data/bin/volt +4 -0
- data/docs/GETTING_STARTED.md +7 -0
- data/docs/GUIDE.md +33 -0
- data/lib/volt.rb +15 -0
- data/lib/volt/benchmark/benchmark.rb +25 -0
- data/lib/volt/cli.rb +34 -0
- data/lib/volt/console.rb +19 -0
- data/lib/volt/controllers/model_controller.rb +29 -0
- data/lib/volt/extra_core/array.rb +10 -0
- data/lib/volt/extra_core/blank.rb +88 -0
- data/lib/volt/extra_core/extra_core.rb +7 -0
- data/lib/volt/extra_core/numeric.rb +9 -0
- data/lib/volt/extra_core/object.rb +36 -0
- data/lib/volt/extra_core/string.rb +29 -0
- data/lib/volt/extra_core/stringify_keys.rb +7 -0
- data/lib/volt/extra_core/true_false.rb +44 -0
- data/lib/volt/extra_core/try.rb +31 -0
- data/lib/volt/models.rb +5 -0
- data/lib/volt/models/array_model.rb +37 -0
- data/lib/volt/models/model.rb +210 -0
- data/lib/volt/models/model_wrapper.rb +23 -0
- data/lib/volt/models/params.rb +67 -0
- data/lib/volt/models/url.rb +192 -0
- data/lib/volt/page/url_tracker.rb +36 -0
- data/lib/volt/reactive/array_extensions.rb +13 -0
- data/lib/volt/reactive/event_chain.rb +126 -0
- data/lib/volt/reactive/events.rb +283 -0
- data/lib/volt/reactive/object_tracker.rb +99 -0
- data/lib/volt/reactive/object_tracking.rb +15 -0
- data/lib/volt/reactive/reactive_array.rb +222 -0
- data/lib/volt/reactive/reactive_tags.rb +64 -0
- data/lib/volt/reactive/reactive_value.rb +368 -0
- data/lib/volt/reactive/string_extensions.rb +34 -0
- data/lib/volt/router/routes.rb +83 -0
- data/lib/volt/server.rb +121 -0
- data/lib/volt/server/binding_setup.rb +2 -0
- data/lib/volt/server/channel_handler.rb +31 -0
- data/lib/volt/server/component_handler.rb +88 -0
- data/lib/volt/server/if_binding_setup.rb +29 -0
- data/lib/volt/server/request_handler.rb +16 -0
- data/lib/volt/server/scope.rb +43 -0
- data/lib/volt/server/source_map_server.rb +31 -0
- data/lib/volt/server/template_parser.rb +452 -0
- data/lib/volt/store/mongo.rb +5 -0
- data/lib/volt/templates/attribute_binding.rb +110 -0
- data/lib/volt/templates/base_binding.rb +37 -0
- data/lib/volt/templates/channel.rb +48 -0
- data/lib/volt/templates/content_binding.rb +35 -0
- data/lib/volt/templates/document_events.rb +80 -0
- data/lib/volt/templates/each_binding.rb +115 -0
- data/lib/volt/templates/event_binding.rb +51 -0
- data/lib/volt/templates/if_binding.rb +74 -0
- data/lib/volt/templates/memory_test.rb +26 -0
- data/lib/volt/templates/page.rb +146 -0
- data/lib/volt/templates/reactive_template.rb +38 -0
- data/lib/volt/templates/render_queue.rb +5 -0
- data/lib/volt/templates/sub_context.rb +23 -0
- data/lib/volt/templates/targets/attribute_section.rb +33 -0
- data/lib/volt/templates/targets/attribute_target.rb +18 -0
- data/lib/volt/templates/targets/base_section.rb +14 -0
- data/lib/volt/templates/targets/binding_document/base_node.rb +3 -0
- data/lib/volt/templates/targets/binding_document/component_node.rb +112 -0
- data/lib/volt/templates/targets/binding_document/html_node.rb +11 -0
- data/lib/volt/templates/targets/dom_section.rb +147 -0
- data/lib/volt/templates/targets/dom_target.rb +11 -0
- data/lib/volt/templates/template_binding.rb +159 -0
- data/lib/volt/templates/template_renderer.rb +50 -0
- data/spec/models/event_chain_spec.rb +129 -0
- data/spec/models/model_spec.rb +340 -0
- data/spec/models/old_model_spec.rb +109 -0
- data/spec/models/reactive_array_spec.rb +262 -0
- data/spec/models/reactive_tags_spec.rb +35 -0
- data/spec/models/reactive_value_spec.rb +336 -0
- data/spec/models/string_extensions_spec.rb +57 -0
- data/spec/router/routes_spec.rb +24 -0
- data/spec/server/template_parser_spec.rb +50 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/store/mongo_spec.rb +4 -0
- data/spec/templates/targets/binding_document/component_node_spec.rb +18 -0
- data/spec/templates/template_binding_spec.rb +98 -0
- data/templates/.gitignore +12 -0
- data/templates/Gemfile.tt +8 -0
- data/templates/app/.empty_directory +0 -0
- data/templates/app/home/config/routes.rb +1 -0
- data/templates/app/home/controllers/index_controller.rb +5 -0
- data/templates/app/home/css/.empty_directory +0 -0
- data/templates/app/home/models/.empty_directory +0 -0
- data/templates/app/home/views/index/about.html +9 -0
- data/templates/app/home/views/index/home.html +7 -0
- data/templates/app/home/views/index/index.html +28 -0
- data/templates/config.ru +4 -0
- data/templates/public/css/ansi.css +0 -0
- data/templates/public/css/bootstrap-theme.css +459 -0
- data/templates/public/css/bootstrap.css +7098 -0
- data/templates/public/css/jumbotron.css +79 -0
- data/templates/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/templates/public/fonts/glyphicons-halflings-regular.svg +229 -0
- data/templates/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/templates/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/templates/public/index.html +25 -0
- data/templates/public/js/bootstrap.js +0 -0
- data/templates/public/js/jquery-2.0.3.js +8829 -0
- data/templates/public/js/sockjs-0.2.1.min.js +27 -0
- data/templates/spec/spec_helper.rb +20 -0
- data/volt.gemspec +41 -0
- metadata +412 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# All url related data is stored in params. This includes the main uri
|
|
2
|
+
# in addition to any query parameters. The router is responsible for
|
|
3
|
+
# converting any uri sections into params. Sections in the uri will
|
|
4
|
+
# override any specified parameters.
|
|
5
|
+
#
|
|
6
|
+
# The params value can be updated the same way a model would be, only
|
|
7
|
+
# the updates will trigger an updated url via the browser history api.
|
|
8
|
+
# TODO: Support # for browsers without the history api.
|
|
9
|
+
|
|
10
|
+
class Params < Model
|
|
11
|
+
def initialize(*args)
|
|
12
|
+
super(*args)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def deep_clone
|
|
16
|
+
new_obj = clone
|
|
17
|
+
|
|
18
|
+
new_obj.attributes = new_obj.attributes.dup
|
|
19
|
+
|
|
20
|
+
new_obj
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
tag_method(:delete) do
|
|
24
|
+
destructive!
|
|
25
|
+
end
|
|
26
|
+
def delete(*args)
|
|
27
|
+
super
|
|
28
|
+
|
|
29
|
+
value_updated
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def method_missing(method_name, *args, &block)
|
|
33
|
+
result = super
|
|
34
|
+
|
|
35
|
+
if method_name[0] == '_' && method_name[-1] == '='
|
|
36
|
+
# Trigger value updated after an assignment
|
|
37
|
+
self.value_updated
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def value_updated
|
|
44
|
+
# Once the initial url has been parsed and set into the attributes,
|
|
45
|
+
# start triggering updates on change events.
|
|
46
|
+
# TODO: This is a temp solution, we need to make it so value_updated
|
|
47
|
+
# is called after the reactive_value has been updated.
|
|
48
|
+
if RUBY_PLATFORM == 'opal'
|
|
49
|
+
%x{
|
|
50
|
+
if (window.setTimeout && this.$run_update.bind) {
|
|
51
|
+
if (window.paramsUpdateTimer) {
|
|
52
|
+
clearTimeout(window.paramsUpdateTimer);
|
|
53
|
+
}
|
|
54
|
+
window.paramsUpdateTimer = setTimeout(this.$run_update.bind(this), 0);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def run_update
|
|
61
|
+
$page.params.trigger!('child_changed') if Volt.client?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def new_model(*args)
|
|
65
|
+
Params.new(*args)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# The url class handles parsing and updating the url
|
|
2
|
+
class URL
|
|
3
|
+
include ReactiveTags
|
|
4
|
+
|
|
5
|
+
# TODO: we need to make it so change events only trigger on changes
|
|
6
|
+
attr_reader :scheme, :host, :port, :path, :query, :params
|
|
7
|
+
attr_accessor :router
|
|
8
|
+
|
|
9
|
+
def initialize(router=nil)
|
|
10
|
+
@router = router
|
|
11
|
+
@params = Params.new({}, 'params')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Parse takes in a url and extracts each sections.
|
|
15
|
+
# It also assigns and changes to the params.
|
|
16
|
+
tag_method(:parse) do
|
|
17
|
+
destructive!
|
|
18
|
+
end
|
|
19
|
+
def parse(url)
|
|
20
|
+
matcher = url.match(/^(https?)[:]\/\/([^\/]+)(.*)$/)
|
|
21
|
+
@scheme = matcher[1]
|
|
22
|
+
@host, @port = matcher[2].split(':')
|
|
23
|
+
@port ||= 80
|
|
24
|
+
|
|
25
|
+
@path = matcher[3]
|
|
26
|
+
@path, @fragment = @path.split('#', 2)
|
|
27
|
+
@path, @query = @path.split('?', 2)
|
|
28
|
+
|
|
29
|
+
assign_query_hash_to_params
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Full url rebuilds the url from it's constituent parts
|
|
33
|
+
def full_url
|
|
34
|
+
if @port
|
|
35
|
+
host_with_port = "#{@host}:#{@port}"
|
|
36
|
+
else
|
|
37
|
+
host_with_port = @host
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
path, params = @router.url_for_params(@params)
|
|
41
|
+
|
|
42
|
+
new_url = "#{@scheme}://#{host_with_port}#{path || @path}"
|
|
43
|
+
|
|
44
|
+
unless params.empty?
|
|
45
|
+
new_url += '?'
|
|
46
|
+
query_parts = []
|
|
47
|
+
nested_params_hash(params).each_pair do |key,value|
|
|
48
|
+
value = value.cur
|
|
49
|
+
# remove the _ from the front
|
|
50
|
+
value = `encodeURI(value)`
|
|
51
|
+
query_parts << "#{key}=#{value}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
new_url += query_parts.join('&')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
return new_url
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Called when the state has changed and the url in the
|
|
61
|
+
# browser should be updated
|
|
62
|
+
# Called when an attribute changes to update the url
|
|
63
|
+
tag_method(:update!) do
|
|
64
|
+
destructive!
|
|
65
|
+
# TODO: ! methods should default to destructive
|
|
66
|
+
end
|
|
67
|
+
def update!
|
|
68
|
+
new_url = full_url()
|
|
69
|
+
|
|
70
|
+
if `(document.location.href != new_url)`
|
|
71
|
+
`history.pushState(null, null, new_url)`
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
# Assigning the params is tricky since we don't want to trigger changed on
|
|
77
|
+
# any values that have not changed. So we first loop through all current
|
|
78
|
+
# url params, removing any not present in the params, while also removing
|
|
79
|
+
# them from the list of new params as added. Then we loop through the
|
|
80
|
+
# remaining new parameters and assign them.
|
|
81
|
+
def assign_query_hash_to_params
|
|
82
|
+
# Get a nested hash representing the current url params.
|
|
83
|
+
query_hash = self.query_hash
|
|
84
|
+
|
|
85
|
+
# Get the params that are in the route
|
|
86
|
+
query_hash.merge!(@router.params_for_path(@path))
|
|
87
|
+
|
|
88
|
+
# Loop through the .params we already have assigned.
|
|
89
|
+
assign_from_old(@params, query_hash)
|
|
90
|
+
assign_new(@params, query_hash)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def assign_from_old(params, new_params)
|
|
94
|
+
queued_deletes = []
|
|
95
|
+
|
|
96
|
+
params.cur.attributes.each_pair do |name,old_val|
|
|
97
|
+
# If there is a new value, see if it has [name]
|
|
98
|
+
new_val = new_params ? new_params[name] : nil
|
|
99
|
+
|
|
100
|
+
if !new_val
|
|
101
|
+
# Queues the delete until after we finish the each_pair loop
|
|
102
|
+
queued_deletes << name
|
|
103
|
+
elsif new_val.is_a?(Hash)
|
|
104
|
+
assign_from_old(old_val, new_val)
|
|
105
|
+
else
|
|
106
|
+
# assign value
|
|
107
|
+
if old_val != new_val
|
|
108
|
+
params.send(:"#{name}=", new_val)
|
|
109
|
+
end
|
|
110
|
+
new_params.delete(name)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
queued_deletes.each {|name| params.delete(name) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def assign_new(params, new_params)
|
|
118
|
+
new_params.each_pair do |name, value|
|
|
119
|
+
if value.is_a?(Hash)
|
|
120
|
+
assign_new(params.send(name), value)
|
|
121
|
+
else
|
|
122
|
+
# assign
|
|
123
|
+
params.send(:"#{name}=", value)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def query_hash
|
|
129
|
+
query_hash = {}
|
|
130
|
+
if @query
|
|
131
|
+
@query.split('&').reject {|v| v == '' }.each do |part|
|
|
132
|
+
parts = part.split('=').reject {|v| v == '' }
|
|
133
|
+
|
|
134
|
+
# Decode string
|
|
135
|
+
# parts[0] = `decodeURI(parts[0])`
|
|
136
|
+
parts[1] = `decodeURI(parts[1])`
|
|
137
|
+
|
|
138
|
+
sections = query_key_sections(parts[0])
|
|
139
|
+
|
|
140
|
+
hash_part = query_hash
|
|
141
|
+
sections.each_with_index do |section,index|
|
|
142
|
+
if index == sections.size-1
|
|
143
|
+
# Last part, assign the value
|
|
144
|
+
hash_part[section] = parts[1]
|
|
145
|
+
else
|
|
146
|
+
hash_part = (hash_part[section] ||= {})
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
return query_hash
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Splits a key from a ?key=value&... parameter into its nested
|
|
156
|
+
# parts. It also adds back the _'s used to access them in params.
|
|
157
|
+
# Example:
|
|
158
|
+
# user[name]=Ryan would parse as [:_user, :_name]
|
|
159
|
+
def query_key_sections(key)
|
|
160
|
+
key.split(/\[([^\]]+)\]/).reject(&:empty?).map {|v| :"_#{v}"}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Generate the key for a nested param attribute
|
|
164
|
+
def query_key(path)
|
|
165
|
+
i = 0
|
|
166
|
+
path.map do |v|
|
|
167
|
+
v = v[1..-1]
|
|
168
|
+
i += 1
|
|
169
|
+
if i != 1
|
|
170
|
+
"[#{v}]"
|
|
171
|
+
else
|
|
172
|
+
v
|
|
173
|
+
end
|
|
174
|
+
end.join('')
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def nested_params_hash(params, path=[])
|
|
178
|
+
results = {}
|
|
179
|
+
|
|
180
|
+
params.each_pair do |key,value|
|
|
181
|
+
if value.cur.is_a?(Params) # TODO: Should be a param
|
|
182
|
+
# TODO: Broke here somehow for nested
|
|
183
|
+
results.merge!(nested_params_hash(value, path + [key]))
|
|
184
|
+
else
|
|
185
|
+
results[query_key(path + [key])] = value
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
return results
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# The URLTracker is responsible for updating the url when
|
|
2
|
+
# a param changes, or updating the url model/params when
|
|
3
|
+
# the browser url changes.
|
|
4
|
+
class UrlTracker
|
|
5
|
+
def initialize(page)
|
|
6
|
+
@page = page
|
|
7
|
+
|
|
8
|
+
if Volt.client?
|
|
9
|
+
page.params.on('child_changed') do
|
|
10
|
+
@page.url.update!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
that = self
|
|
14
|
+
|
|
15
|
+
# Setup popstate on the dom ready event. Prevents an extra
|
|
16
|
+
# popstate trigger
|
|
17
|
+
%x{
|
|
18
|
+
var first = true;
|
|
19
|
+
window.addEventListener("popstate", function(e) {
|
|
20
|
+
if (first === false) {
|
|
21
|
+
that.$url_updated();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
first = false;
|
|
25
|
+
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def url_updated(first_call=false)
|
|
33
|
+
@page.url.parse(`document.location.href`)
|
|
34
|
+
@page.url.update! unless first_call
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
CHAIN_DEBUG = false
|
|
2
|
+
|
|
3
|
+
class ChainListener
|
|
4
|
+
attr_reader :object, :callback
|
|
5
|
+
|
|
6
|
+
def initialize(event_chain, object, callback)
|
|
7
|
+
@event_chain = event_chain
|
|
8
|
+
@object = object
|
|
9
|
+
@callback = callback
|
|
10
|
+
|
|
11
|
+
if RUBY_PLATFORM == 'opal' && CHAIN_DEBUG
|
|
12
|
+
`window.chain_listeners = window.chain_listeners || 0;`
|
|
13
|
+
`window.chain_listeners += 1;`
|
|
14
|
+
`console.log('chain listeners: ', window.chain_listeners)`
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def remove
|
|
19
|
+
raise "event chain already removed" if @removed
|
|
20
|
+
@removed = true
|
|
21
|
+
@event_chain.remove_object(self)
|
|
22
|
+
|
|
23
|
+
# We need to clear these to free memory
|
|
24
|
+
@event_chain = nil
|
|
25
|
+
@object = nil
|
|
26
|
+
@callback = nil
|
|
27
|
+
|
|
28
|
+
if RUBY_PLATFORM == 'opal' && CHAIN_DEBUG
|
|
29
|
+
`window.chain_listeners -= 1;`
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class EventChain
|
|
35
|
+
def initialize(main_object)
|
|
36
|
+
@event_chain = {}
|
|
37
|
+
@main_object = main_object
|
|
38
|
+
@event_counts = {}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Register an event listener that chains from object to self
|
|
42
|
+
def setup_listener(event, chain_listener)
|
|
43
|
+
return chain_listener.object.on(event, @main_object) do |*args|
|
|
44
|
+
if callback = chain_listener.callback
|
|
45
|
+
callback.call(event, *args)
|
|
46
|
+
else
|
|
47
|
+
# Trigger on this value, when it happens on the parent
|
|
48
|
+
@main_object.trigger!(event, *args)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def all_listening_events
|
|
54
|
+
all_listeners = []
|
|
55
|
+
all_listeners += @main_object.listeners.keys
|
|
56
|
+
|
|
57
|
+
if @main_object
|
|
58
|
+
@main_object.event_followers.each do |event_follower|
|
|
59
|
+
all_listeners += event_follower.listeners.keys
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
return all_listeners.uniq
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# We can chain our events to any other object that includes
|
|
68
|
+
# Events
|
|
69
|
+
def add_object(object, &block)
|
|
70
|
+
# puts "ADD OBJECT: #{object.inspect} to #{self.inspect}"
|
|
71
|
+
|
|
72
|
+
chain_listener = ChainListener.new(self, object, block)
|
|
73
|
+
|
|
74
|
+
listeners = {}
|
|
75
|
+
|
|
76
|
+
all_listening_events.uniq.each do |event|
|
|
77
|
+
# Create a listener for each event
|
|
78
|
+
listeners[event] = setup_listener(event, chain_listener)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@event_chain[chain_listener] = listeners
|
|
82
|
+
|
|
83
|
+
return chain_listener
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def remove_object(chain_listener)
|
|
88
|
+
@event_chain[chain_listener].each_pair do |event,listener|
|
|
89
|
+
# Unbind each listener
|
|
90
|
+
listener.remove
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
@event_chain.delete(chain_listener)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def add_event(event)
|
|
97
|
+
unless @event_counts[event]
|
|
98
|
+
@event_chain.each_pair do |chain_listener,listeners|
|
|
99
|
+
# Only add if we haven't already chained this event
|
|
100
|
+
unless listeners[event]
|
|
101
|
+
listeners[event] = setup_listener(event, chain_listener)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
@event_counts[event] ||= 0
|
|
107
|
+
@event_counts[event] += 1
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Removes the event from all events in all objects
|
|
111
|
+
def remove_event(event)
|
|
112
|
+
if @event_counts[event]
|
|
113
|
+
count = @event_counts[event] -= 1
|
|
114
|
+
|
|
115
|
+
if count == 0
|
|
116
|
+
@event_chain.each_pair do |chain_listener,listeners|
|
|
117
|
+
listeners[event].remove# if listeners[event]
|
|
118
|
+
listeners.delete(event)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Also remove the event count
|
|
122
|
+
@event_counts.delete(event)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
require 'volt/reactive/event_chain'
|
|
2
|
+
require 'volt/reactive/object_tracker'
|
|
3
|
+
|
|
4
|
+
DEBUG = false
|
|
5
|
+
|
|
6
|
+
# A listener gets returned when adding an 'on' event listener. It can be
|
|
7
|
+
# used to clear the event listener.
|
|
8
|
+
class Listener
|
|
9
|
+
attr_reader :scope_provider
|
|
10
|
+
|
|
11
|
+
def initialize(klass, event, scope_provider, callback)
|
|
12
|
+
@klass = klass
|
|
13
|
+
@event = event
|
|
14
|
+
@scope_provider = scope_provider
|
|
15
|
+
@callback = callback
|
|
16
|
+
|
|
17
|
+
if DEBUG && RUBY_PLATFORM == 'opal'
|
|
18
|
+
# puts "e: #{event} on #{klass.inspect}"
|
|
19
|
+
@@all_events ||= []
|
|
20
|
+
@@all_events << self
|
|
21
|
+
|
|
22
|
+
# counts = {}
|
|
23
|
+
# @@all_events.each do |ev|
|
|
24
|
+
# scope = (ev.scope_provider && ev.scope_provider.scope) || nil
|
|
25
|
+
#
|
|
26
|
+
# # puts `typeof(scope)`
|
|
27
|
+
# if `typeof(scope) !== 'undefined'`
|
|
28
|
+
# counts[scope] ||= 0
|
|
29
|
+
# counts[scope] += 1
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# puts counts.inspect
|
|
34
|
+
|
|
35
|
+
`window.total_listeners = window.total_listeners || 0;`
|
|
36
|
+
`window.total_listeners += 1;`
|
|
37
|
+
`console.log(window.total_listeners);`
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def internal?
|
|
42
|
+
@internal
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def scope
|
|
46
|
+
@scope_provider && @scope_provider.scope
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def call(*args)
|
|
50
|
+
# raise "Triggered on removed: #{@event} on #{@klass2.inspect}" if @removed
|
|
51
|
+
if @removed
|
|
52
|
+
puts "Triggered on removed: #{@event}"
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Queue a live value update
|
|
57
|
+
if @klass.reactive?
|
|
58
|
+
# We are working with a reactive value. Its receiving an event meaning
|
|
59
|
+
# something changed. Queue an update of the value it tracks.
|
|
60
|
+
@klass.object_tracker.queue_update
|
|
61
|
+
# puts "Queued: #{ObjectTracker.queue.inspect}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
@callback.call(*args)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Removes the listener from where ever it was created.
|
|
68
|
+
def remove
|
|
69
|
+
# puts "FAIL:" if @removed
|
|
70
|
+
raise "event #{@event} already removed" if @removed
|
|
71
|
+
|
|
72
|
+
# puts "e rem: #{@event} on #{@klass.inspect}"
|
|
73
|
+
if DEBUG && RUBY_PLATFORM == 'opal'
|
|
74
|
+
@@all_events.delete(self) if @@all_events
|
|
75
|
+
|
|
76
|
+
`window.total_listeners -= 1;`
|
|
77
|
+
`console.log("Rem", window.total_listeners);`
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@removed = true
|
|
82
|
+
@klass.remove_listener(@event, self)
|
|
83
|
+
|
|
84
|
+
# puts "Removed Listener for: #{@event} - #{@scope_provider && @scope_provider.scope.inspect} from #{@klass.inspect}"
|
|
85
|
+
|
|
86
|
+
# We need to clear these references to free the memory
|
|
87
|
+
@scope_provider = nil
|
|
88
|
+
@callback = nil
|
|
89
|
+
# @klass2 = @klass
|
|
90
|
+
@klass = nil
|
|
91
|
+
# @event = nil
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def inspect
|
|
96
|
+
"<Listener:#{object_id} event=#{@event} scope=#{scope.inspect}#{' internal' if internal?}>"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
module Events
|
|
101
|
+
attr_accessor :scope
|
|
102
|
+
# Add a listener for an event
|
|
103
|
+
def on(event, scope_provider=nil, &block)
|
|
104
|
+
# puts "Register: #{event} on #{self.inspect}"
|
|
105
|
+
event = event.to_sym
|
|
106
|
+
|
|
107
|
+
new_listener = Listener.new(self, event, scope_provider, block)
|
|
108
|
+
|
|
109
|
+
@listeners ||= {}
|
|
110
|
+
@listeners[event] ||= []
|
|
111
|
+
@listeners[event] << new_listener
|
|
112
|
+
|
|
113
|
+
first = @listeners[event].size == 1
|
|
114
|
+
add_event_to_chains(event) if first
|
|
115
|
+
|
|
116
|
+
# Let the included class know that an event was registered. (if it cares)
|
|
117
|
+
if self.respond_to?(:event_added)
|
|
118
|
+
# call event added passing the event, the scope, and a boolean if it
|
|
119
|
+
# is the first time this event has been added.
|
|
120
|
+
self.event_added(event, scope_provider, first)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return new_listener
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def event_chain
|
|
127
|
+
@event_chain ||= EventChain.new(self)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def listeners
|
|
131
|
+
@listeners || {}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Typically you would call .remove on the listener returned from the .on
|
|
135
|
+
# method. However, here you can also pass in the original proc to remove
|
|
136
|
+
# a listener
|
|
137
|
+
def remove_listener(event, listener)
|
|
138
|
+
event = event.to_sym
|
|
139
|
+
|
|
140
|
+
raise "Unable to delete #{event} from #{self.inspect}" unless @listeners && @listeners[event]
|
|
141
|
+
|
|
142
|
+
# if @listeners && @listeners[event]
|
|
143
|
+
@listeners[event].delete(listener)
|
|
144
|
+
|
|
145
|
+
no_more_events = @listeners[event].size == 0
|
|
146
|
+
if no_more_events
|
|
147
|
+
remove_event_from_chains(event)
|
|
148
|
+
|
|
149
|
+
# No registered listeners now on this event
|
|
150
|
+
@listeners.delete(event)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Let the class we're included on know that we removed a listener (if it cares)
|
|
154
|
+
if self.respond_to?(:event_removed)
|
|
155
|
+
# Pass in the event and a boolean indicating if it is the last event
|
|
156
|
+
self.event_removed(event, no_more_events)
|
|
157
|
+
end
|
|
158
|
+
# end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# When events get added, we need to notify event chains so they
|
|
162
|
+
# can update and chain any new events.
|
|
163
|
+
def add_event_to_chains(event)
|
|
164
|
+
# First time this event is added, update any chains
|
|
165
|
+
event_chain.add_event(event)
|
|
166
|
+
|
|
167
|
+
# We need to keep the event chain's updated for any objects we're
|
|
168
|
+
# following for events.
|
|
169
|
+
event_followings.each {|ef| ef.event_chain.add_event(event) }
|
|
170
|
+
|
|
171
|
+
if event != :changed && !@other_event_listener
|
|
172
|
+
@other_event_listener = on('changed') { }
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# When events are removed, we need to notify any relevent chains so they
|
|
177
|
+
# can remove any chained events.
|
|
178
|
+
def remove_event_from_chains(event)
|
|
179
|
+
event_chain.remove_event(event)
|
|
180
|
+
|
|
181
|
+
# We need to keep the event chain's updated for any objects we're
|
|
182
|
+
# following for events.
|
|
183
|
+
event_followings.each {|ef| ef.event_chain.remove_event(event) }
|
|
184
|
+
|
|
185
|
+
if event != :changed
|
|
186
|
+
# See if there are any remaining events that aren't changed
|
|
187
|
+
if listeners.keys.reject {|k| k == :changed }.size == 0
|
|
188
|
+
@other_event_listener.remove
|
|
189
|
+
@other_event_listener = nil
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# Track the current object that we're following.
|
|
196
|
+
def event_followings
|
|
197
|
+
@event_followings || []
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def add_following(object)
|
|
201
|
+
@event_followings ||= []
|
|
202
|
+
@event_followings << object
|
|
203
|
+
|
|
204
|
+
# Take all of our listeners and add them to the
|
|
205
|
+
listeners.keys.each do |event|
|
|
206
|
+
object.event_chain.add_event(event)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def remove_following(object)
|
|
211
|
+
@event_followings.delete(object)
|
|
212
|
+
|
|
213
|
+
listeners.keys.each do |event|
|
|
214
|
+
object.event_chain.remove_event(event)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Track who's following us
|
|
219
|
+
def event_followers
|
|
220
|
+
@event_followers || []
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def add_event_follower(follower)
|
|
224
|
+
@event_followers ||= []
|
|
225
|
+
@event_followers << follower
|
|
226
|
+
|
|
227
|
+
follower.add_following(self)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def remove_event_follower(follower)
|
|
231
|
+
if @event_followers
|
|
232
|
+
@event_followers.delete(follower)
|
|
233
|
+
|
|
234
|
+
follower.remove_following(self)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Return all listeners for an event on the current object and any event
|
|
239
|
+
# following objects.
|
|
240
|
+
def all_listeners_for(event)
|
|
241
|
+
# TODO: We dup at the moment because some events unregister events, is there
|
|
242
|
+
# a better solution than this?
|
|
243
|
+
all_listeners = []
|
|
244
|
+
all_listeners += @listeners[event].dup if @listeners && @listeners[event]
|
|
245
|
+
|
|
246
|
+
if @event_followers
|
|
247
|
+
@event_followers.each do |event_follower|
|
|
248
|
+
ef_listeners = event_follower.listeners
|
|
249
|
+
all_listeners += ef_listeners[event].dup if ef_listeners[event]
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
return all_listeners
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def trigger!(event, *args)
|
|
257
|
+
ObjectTracker.process_queue if !reactive? && !respond_to?(:skip_current_queue_flush)
|
|
258
|
+
|
|
259
|
+
event = event.to_sym
|
|
260
|
+
|
|
261
|
+
all_listeners_for(event).each do |listener|
|
|
262
|
+
# Call the event on each listener
|
|
263
|
+
listener.call(*args)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
nil
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Takes a block, which passes in
|
|
270
|
+
def trigger_by_scope!(event, *args, &block)
|
|
271
|
+
ObjectTracker.process_queue if !reactive? && !respond_to?(:skip_current_queue_flush)
|
|
272
|
+
|
|
273
|
+
event = event.to_sym
|
|
274
|
+
|
|
275
|
+
all_listeners_for(event).each do |listener|
|
|
276
|
+
# Call the block, pass in the scope
|
|
277
|
+
if block.call(listener.scope)
|
|
278
|
+
listener.call(*args)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
end
|