volt 0.7.1 → 0.7.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -2
- data/Readme.md +97 -56
- data/VERSION +1 -1
- data/app/volt/assets/js/sockjs-0.3.4.min.js +27 -0
- data/app/volt/assets/js/vertxbus.js +216 -0
- data/app/volt/tasks/live_query/live_query.rb +5 -5
- data/app/volt/tasks/live_query/live_query_pool.rb +1 -1
- data/app/volt/tasks/query_tasks.rb +5 -0
- data/app/volt/tasks/store_tasks.rb +44 -18
- data/docs/WHY.md +10 -0
- data/lib/volt/cli.rb +18 -7
- data/lib/volt/controllers/model_controller.rb +30 -8
- data/lib/volt/extra_core/inflections.rb +63 -0
- data/lib/volt/extra_core/inflector/inflections.rb +203 -0
- data/lib/volt/extra_core/inflector/methods.rb +63 -0
- data/lib/volt/extra_core/inflector.rb +4 -0
- data/lib/volt/extra_core/object.rb +9 -0
- data/lib/volt/extra_core/string.rb +10 -14
- data/lib/volt/models/array_model.rb +45 -27
- data/lib/volt/models/cursor.rb +6 -0
- data/lib/volt/models/model.rb +127 -12
- data/lib/volt/models/model_hash_behaviour.rb +8 -5
- data/lib/volt/models/model_helpers.rb +4 -4
- data/lib/volt/models/model_state.rb +22 -0
- data/lib/volt/models/persistors/array_store.rb +49 -35
- data/lib/volt/models/persistors/base.rb +3 -3
- data/lib/volt/models/persistors/model_store.rb +17 -6
- data/lib/volt/models/persistors/query/query_listener.rb +0 -2
- data/lib/volt/models/persistors/store.rb +0 -4
- data/lib/volt/models/persistors/store_state.rb +27 -0
- data/lib/volt/models/url.rb +2 -2
- data/lib/volt/models/validations/errors.rb +0 -0
- data/lib/volt/models/validations/length.rb +13 -0
- data/lib/volt/models/validations/validations.rb +82 -0
- data/lib/volt/models.rb +1 -1
- data/lib/volt/page/bindings/attribute_binding.rb +29 -14
- data/lib/volt/page/bindings/base_binding.rb +2 -2
- data/lib/volt/page/bindings/component_binding.rb +29 -25
- data/lib/volt/page/bindings/content_binding.rb +1 -0
- data/lib/volt/page/bindings/each_binding.rb +25 -33
- data/lib/volt/page/bindings/event_binding.rb +0 -1
- data/lib/volt/page/bindings/if_binding.rb +3 -1
- data/lib/volt/page/bindings/template_binding.rb +61 -28
- data/lib/volt/page/document_events.rb +3 -1
- data/lib/volt/page/draw_cycle.rb +22 -0
- data/lib/volt/page/page.rb +10 -1
- data/lib/volt/page/reactive_template.rb +23 -16
- data/lib/volt/page/sub_context.rb +1 -1
- data/lib/volt/page/targets/attribute_section.rb +3 -2
- data/lib/volt/page/targets/attribute_target.rb +0 -4
- data/lib/volt/page/targets/base_section.rb +25 -0
- data/lib/volt/page/targets/binding_document/component_node.rb +13 -14
- data/lib/volt/page/targets/binding_document/html_node.rb +4 -0
- data/lib/volt/page/targets/dom_section.rb +16 -67
- data/lib/volt/page/targets/dom_template.rb +99 -0
- data/lib/volt/page/targets/helpers/comment_searchers.rb +29 -0
- data/lib/volt/page/template_renderer.rb +2 -14
- data/lib/volt/reactive/array_extensions.rb +0 -1
- data/lib/volt/reactive/event_chain.rb +9 -2
- data/lib/volt/reactive/events.rb +44 -37
- data/lib/volt/reactive/object_tracking.rb +1 -1
- data/lib/volt/reactive/reactive_array.rb +18 -0
- data/lib/volt/reactive/reactive_count.rb +108 -0
- data/lib/volt/reactive/reactive_generator.rb +44 -0
- data/lib/volt/reactive/reactive_value.rb +73 -73
- data/lib/volt/reactive/string_extensions.rb +1 -1
- data/lib/volt/router/routes.rb +205 -88
- data/lib/volt/server/component_handler.rb +3 -1
- data/lib/volt/server/html_parser/view_parser.rb +20 -4
- data/lib/volt/server/rack/component_paths.rb +13 -10
- data/lib/volt/server/rack/index_files.rb +4 -4
- data/lib/volt/server/socket_connection_handler.rb +5 -1
- data/lib/volt/server.rb +10 -3
- data/spec/apps/kitchen_sink/.gitignore +8 -0
- data/spec/apps/kitchen_sink/Gemfile +32 -0
- data/spec/apps/kitchen_sink/app/home/views/index/index.html +3 -5
- data/spec/apps/kitchen_sink/config.ru +4 -0
- data/spec/apps/kitchen_sink/public/index.html +2 -2
- data/spec/extra_core/inflector_spec.rb +8 -0
- data/spec/models/event_chain_spec.rb +18 -0
- data/spec/models/model_buffers_spec.rb +9 -0
- data/spec/models/model_spec.rb +22 -9
- data/spec/models/reactive_array_spec.rb +26 -1
- data/spec/models/reactive_call_times_spec.rb +28 -0
- data/spec/models/reactive_value_spec.rb +19 -0
- data/spec/models/validations_spec.rb +39 -0
- data/spec/page/bindings/content_binding_spec.rb +1 -0
- data/spec/{templates → page/bindings}/template_binding_spec.rb +54 -0
- data/spec/router/routes_spec.rb +156 -8
- data/spec/server/html_parser/sandlebars_parser_spec.rb +55 -47
- data/spec/server/html_parser/view_parser_spec.rb +3 -0
- data/spec/server/rack/asset_files_spec.rb +1 -1
- data/spec/spec_helper.rb +25 -11
- data/spec/templates/targets/binding_document/component_node_spec.rb +12 -0
- data/templates/project/Gemfile.tt +11 -0
- data/templates/project/app/home/config/routes.rb +1 -1
- data/templates/project/app/home/controllers/index_controller.rb +5 -5
- data/templates/project/app/home/views/index/index.html +6 -6
- data/volt.gemspec +5 -6
- metadata +34 -76
- data/app/volt/assets/js/sockjs-0.2.1.min.js +0 -27
- data/lib/volt/reactive/object_tracker.rb +0 -107
@@ -0,0 +1,203 @@
|
|
1
|
+
module Inflector
|
2
|
+
# A singleton instance of this class is yielded by Inflector.inflections,
|
3
|
+
# which can then be used to specify additional inflection rules. If passed
|
4
|
+
# an optional locale, rules for other languages can be specified. The
|
5
|
+
# default locale is <tt>:en</tt>. Only rules for English are provided.
|
6
|
+
#
|
7
|
+
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
8
|
+
# inflect.plural /^(ox)$/i, '\1\2en'
|
9
|
+
# inflect.singular /^(ox)en/i, '\1'
|
10
|
+
#
|
11
|
+
# inflect.irregular 'octopus', 'octopi'
|
12
|
+
#
|
13
|
+
# inflect.uncountable 'equipment'
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# New rules are added at the top. So in the example above, the irregular
|
17
|
+
# rule for octopus will now be the first of the pluralization and
|
18
|
+
# singularization rules that is runs. This guarantees that your rules run
|
19
|
+
# before any of the rules that may already have been loaded.
|
20
|
+
class Inflections
|
21
|
+
@__instance__ = {}
|
22
|
+
|
23
|
+
def self.instance(locale = :en)
|
24
|
+
@__instance__[locale] ||= new
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], [], [], {}, /(?=a)b/
|
31
|
+
end
|
32
|
+
|
33
|
+
# Private, for the test suite.
|
34
|
+
def initialize_dup(orig) # :nodoc:
|
35
|
+
%w(plurals singulars uncountables humans acronyms acronym_regex).each do |scope|
|
36
|
+
instance_variable_set("@#{scope}", orig.send(scope).dup)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Specifies a new acronym. An acronym must be specified as it will appear
|
41
|
+
# in a camelized string. An underscore string that contains the acronym
|
42
|
+
# will retain the acronym when passed to +camelize+, +humanize+, or
|
43
|
+
# +titleize+. A camelized string that contains the acronym will maintain
|
44
|
+
# the acronym when titleized or humanized, and will convert the acronym
|
45
|
+
# into a non-delimited single lowercase word when passed to +underscore+.
|
46
|
+
#
|
47
|
+
# acronym 'HTML'
|
48
|
+
# titleize 'html' #=> 'HTML'
|
49
|
+
# camelize 'html' #=> 'HTML'
|
50
|
+
# underscore 'MyHTML' #=> 'my_html'
|
51
|
+
#
|
52
|
+
# The acronym, however, must occur as a delimited unit and not be part of
|
53
|
+
# another word for conversions to recognize it:
|
54
|
+
#
|
55
|
+
# acronym 'HTTP'
|
56
|
+
# camelize 'my_http_delimited' #=> 'MyHTTPDelimited'
|
57
|
+
# camelize 'https' #=> 'Https', not 'HTTPs'
|
58
|
+
# underscore 'HTTPS' #=> 'http_s', not 'https'
|
59
|
+
#
|
60
|
+
# acronym 'HTTPS'
|
61
|
+
# camelize 'https' #=> 'HTTPS'
|
62
|
+
# underscore 'HTTPS' #=> 'https'
|
63
|
+
#
|
64
|
+
# Note: Acronyms that are passed to +pluralize+ will no longer be
|
65
|
+
# recognized, since the acronym will not occur as a delimited unit in the
|
66
|
+
# pluralized result. To work around this, you must specify the pluralized
|
67
|
+
# form as an acronym as well:
|
68
|
+
#
|
69
|
+
# acronym 'API'
|
70
|
+
# camelize(pluralize('api')) #=> 'Apis'
|
71
|
+
#
|
72
|
+
# acronym 'APIs'
|
73
|
+
# camelize(pluralize('api')) #=> 'APIs'
|
74
|
+
#
|
75
|
+
# +acronym+ may be used to specify any word that contains an acronym or
|
76
|
+
# otherwise needs to maintain a non-standard capitalization. The only
|
77
|
+
# restriction is that the word must begin with a capital letter.
|
78
|
+
#
|
79
|
+
# acronym 'RESTful'
|
80
|
+
# underscore 'RESTful' #=> 'restful'
|
81
|
+
# underscore 'RESTfulController' #=> 'restful_controller'
|
82
|
+
# titleize 'RESTfulController' #=> 'RESTful Controller'
|
83
|
+
# camelize 'restful' #=> 'RESTful'
|
84
|
+
# camelize 'restful_controller' #=> 'RESTfulController'
|
85
|
+
#
|
86
|
+
# acronym 'McDonald'
|
87
|
+
# underscore 'McDonald' #=> 'mcdonald'
|
88
|
+
# camelize 'mcdonald' #=> 'McDonald'
|
89
|
+
def acronym(word)
|
90
|
+
@acronyms[word.downcase] = word
|
91
|
+
@acronym_regex = /#{@acronyms.values.join("|")}/
|
92
|
+
end
|
93
|
+
|
94
|
+
# Specifies a new pluralization rule and its replacement. The rule can
|
95
|
+
# either be a string or a regular expression. The replacement should
|
96
|
+
# always be a string that may include references to the matched data from
|
97
|
+
# the rule.
|
98
|
+
def plural(rule, replacement)
|
99
|
+
@uncountables.delete(rule) if rule.is_a?(String)
|
100
|
+
@uncountables.delete(replacement)
|
101
|
+
@plurals.insert(0, [rule, replacement])
|
102
|
+
end
|
103
|
+
|
104
|
+
# Specifies a new singularization rule and its replacement. The rule can
|
105
|
+
# either be a string or a regular expression. The replacement should
|
106
|
+
# always be a string that may include references to the matched data from
|
107
|
+
# the rule.
|
108
|
+
def singular(rule, replacement)
|
109
|
+
@uncountables.delete(rule) if rule.is_a?(String)
|
110
|
+
@uncountables.delete(replacement)
|
111
|
+
@singulars.insert(0, [rule, replacement])
|
112
|
+
end
|
113
|
+
|
114
|
+
# Specifies a new irregular that applies to both pluralization and
|
115
|
+
# singularization at the same time. This can only be used for strings, not
|
116
|
+
# regular expressions. You simply pass the irregular in singular and
|
117
|
+
# plural form.
|
118
|
+
#
|
119
|
+
# irregular 'octopus', 'octopi'
|
120
|
+
# irregular 'person', 'people'
|
121
|
+
def irregular(singular, plural)
|
122
|
+
@uncountables.delete(singular)
|
123
|
+
@uncountables.delete(plural)
|
124
|
+
|
125
|
+
s0 = singular[0]
|
126
|
+
srest = singular[1..-1]
|
127
|
+
|
128
|
+
p0 = plural[0]
|
129
|
+
prest = plural[1..-1]
|
130
|
+
|
131
|
+
if s0.upcase == p0.upcase
|
132
|
+
plural(/(#{s0})#{srest}$/i, '\1' + prest)
|
133
|
+
plural(/(#{p0})#{prest}$/i, '\1' + prest)
|
134
|
+
|
135
|
+
singular(/(#{s0})#{srest}$/i, '\1' + srest)
|
136
|
+
singular(/(#{p0})#{prest}$/i, '\1' + srest)
|
137
|
+
else
|
138
|
+
plural(/#{s0.upcase}(?i)#{srest}$/, p0.upcase + prest)
|
139
|
+
plural(/#{s0.downcase}(?i)#{srest}$/, p0.downcase + prest)
|
140
|
+
plural(/#{p0.upcase}(?i)#{prest}$/, p0.upcase + prest)
|
141
|
+
plural(/#{p0.downcase}(?i)#{prest}$/, p0.downcase + prest)
|
142
|
+
|
143
|
+
singular(/#{s0.upcase}(?i)#{srest}$/, s0.upcase + srest)
|
144
|
+
singular(/#{s0.downcase}(?i)#{srest}$/, s0.downcase + srest)
|
145
|
+
singular(/#{p0.upcase}(?i)#{prest}$/, s0.upcase + srest)
|
146
|
+
singular(/#{p0.downcase}(?i)#{prest}$/, s0.downcase + srest)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Add uncountable words that shouldn't be attempted inflected.
|
151
|
+
#
|
152
|
+
# uncountable 'money'
|
153
|
+
# uncountable 'money', 'information'
|
154
|
+
# uncountable %w( money information rice )
|
155
|
+
def uncountable(*words)
|
156
|
+
(@uncountables << words).flatten!
|
157
|
+
end
|
158
|
+
|
159
|
+
# Specifies a humanized form of a string by a regular expression rule or
|
160
|
+
# by a string mapping. When using a regular expression based replacement,
|
161
|
+
# the normal humanize formatting is called after the replacement. When a
|
162
|
+
# string is used, the human form should be specified as desired (example:
|
163
|
+
# 'The name', not 'the_name').
|
164
|
+
#
|
165
|
+
# human /_cnt$/i, '\1_count'
|
166
|
+
# human 'legacy_col_person_name', 'Name'
|
167
|
+
def human(rule, replacement)
|
168
|
+
@humans.insert(0, [rule, replacement])
|
169
|
+
end
|
170
|
+
|
171
|
+
# Clears the loaded inflections within a given scope (default is
|
172
|
+
# <tt>:all</tt>). Give the scope as a symbol of the inflection type, the
|
173
|
+
# options are: <tt>:plurals</tt>, <tt>:singulars</tt>, <tt>:uncountables</tt>,
|
174
|
+
# <tt>:humans</tt>.
|
175
|
+
#
|
176
|
+
# clear :all
|
177
|
+
# clear :plurals
|
178
|
+
def clear(scope = :all)
|
179
|
+
case scope
|
180
|
+
when :all
|
181
|
+
@plurals, @singulars, @uncountables, @humans = [], [], [], []
|
182
|
+
else
|
183
|
+
instance_variable_set "@#{scope}", []
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Yields a singleton instance of Inflector::Inflections so you can specify
|
189
|
+
# additional inflector rules. If passed an optional locale, rules for other
|
190
|
+
# languages can be specified. If not specified, defaults to <tt>:en</tt>.
|
191
|
+
# Only rules for English are provided.
|
192
|
+
#
|
193
|
+
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
194
|
+
# inflect.uncountable 'rails'
|
195
|
+
# end
|
196
|
+
def self.inflections(locale = :en)
|
197
|
+
if block_given?
|
198
|
+
yield Inflections.instance(locale)
|
199
|
+
else
|
200
|
+
Inflections.instance(locale)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# The Inflector transforms words from singular to plural, class names to table
|
4
|
+
# names, modularized class names to ones without, and class names to foreign
|
5
|
+
# keys. The default inflections for pluralization, singularization, and
|
6
|
+
# uncountable words are kept in inflections.rb.
|
7
|
+
module Inflector
|
8
|
+
# Returns the plural form of the word in the string.
|
9
|
+
#
|
10
|
+
# If passed an optional +locale+ parameter, the word will be
|
11
|
+
# pluralized using rules defined for that language. By default,
|
12
|
+
# this parameter is set to <tt>:en</tt>.
|
13
|
+
#
|
14
|
+
# 'post'.pluralize # => "posts"
|
15
|
+
# 'octopus'.pluralize # => "octopi"
|
16
|
+
# 'sheep'.pluralize # => "sheep"
|
17
|
+
# 'words'.pluralize # => "words"
|
18
|
+
# 'CamelOctopus'.pluralize # => "CamelOctopi"
|
19
|
+
# 'ley'.pluralize(:es) # => "leyes"
|
20
|
+
def self.pluralize(word, locale = :en)
|
21
|
+
apply_inflections(word, inflections(locale).plurals)
|
22
|
+
end
|
23
|
+
|
24
|
+
# The reverse of +pluralize+, returns the singular form of a word in a
|
25
|
+
# string.
|
26
|
+
#
|
27
|
+
# If passed an optional +locale+ parameter, the word will be
|
28
|
+
# pluralized using rules defined for that language. By default,
|
29
|
+
# this parameter is set to <tt>:en</tt>.
|
30
|
+
#
|
31
|
+
# 'posts'.singularize # => "post"
|
32
|
+
# 'octopi'.singularize # => "octopus"
|
33
|
+
# 'sheep'.singularize # => "sheep"
|
34
|
+
# 'word'.singularize # => "word"
|
35
|
+
# 'CamelOctopi'.singularize # => "CamelOctopus"
|
36
|
+
# 'leyes'.singularize(:es) # => "ley"
|
37
|
+
def self.singularize(word, locale = :en)
|
38
|
+
apply_inflections(word, inflections(locale).singulars)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Applies inflection rules for +singularize+ and +pluralize+.
|
45
|
+
#
|
46
|
+
# apply_inflections('post', inflections.plurals) # => "posts"
|
47
|
+
# apply_inflections('posts', inflections.singulars) # => "post"
|
48
|
+
def self.apply_inflections(word, rules)
|
49
|
+
result = word.to_s.dup
|
50
|
+
|
51
|
+
if word.empty? || inflections.uncountables.include?(result.downcase[/\b\w+\Z/])
|
52
|
+
result
|
53
|
+
else
|
54
|
+
rules.each do |(rule, replacement)|
|
55
|
+
if result.match(rule)
|
56
|
+
result = result.sub(rule, replacement)
|
57
|
+
break
|
58
|
+
end
|
59
|
+
end
|
60
|
+
result
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,4 @@
|
|
1
|
+
# Copied from rails ActiveSupport: https://github.com/rails/rails/blob/feaa6e2048fe86bcf07e967d6e47b865e42e055b/activesupport/lib/active_support/inflector/inflections.rb
|
2
|
+
require 'volt/extra_core/inflector/inflections'
|
3
|
+
require 'volt/extra_core/inflector/methods'
|
4
|
+
require 'volt/extra_core/inflections'
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'volt/extra_core/inflector'
|
2
|
+
|
1
3
|
class String
|
2
4
|
# TODO: replace with better implementations
|
3
5
|
# NOTE: strings are currently immutable in Opal, so no ! methods
|
@@ -9,31 +11,25 @@ class String
|
|
9
11
|
self.scan(/[A-Z][a-z]*/).join("_").downcase
|
10
12
|
end
|
11
13
|
|
14
|
+
def dasherize
|
15
|
+
self.gsub('_', '-')
|
16
|
+
end
|
17
|
+
|
12
18
|
def pluralize
|
13
|
-
|
14
|
-
if self[-1] != 's'
|
15
|
-
return self + 's'
|
16
|
-
else
|
17
|
-
return self
|
18
|
-
end
|
19
|
+
Inflector.pluralize(self)
|
19
20
|
end
|
20
21
|
|
21
22
|
def singularize
|
22
|
-
|
23
|
-
if self[-1] == 's'
|
24
|
-
return self[0..-2]
|
25
|
-
else
|
26
|
-
return self
|
27
|
-
end
|
23
|
+
Inflector.singularize(self)
|
28
24
|
end
|
29
25
|
|
30
26
|
def plural?
|
31
27
|
# TODO: Temp implementation
|
32
|
-
self
|
28
|
+
self.pluralize == self
|
33
29
|
end
|
34
30
|
|
35
31
|
def singular?
|
36
32
|
# TODO: Temp implementation
|
37
|
-
self
|
33
|
+
self.singularize == self
|
38
34
|
end
|
39
35
|
end
|
@@ -1,11 +1,28 @@
|
|
1
1
|
require 'volt/models/model_wrapper'
|
2
2
|
require 'volt/models/model_helpers'
|
3
|
+
require 'volt/models/model_state'
|
3
4
|
|
4
5
|
class ArrayModel < ReactiveArray
|
5
6
|
include ModelWrapper
|
6
7
|
include ModelHelpers
|
8
|
+
include ModelState
|
7
9
|
|
8
|
-
attr_reader :parent, :path, :persistor, :options
|
10
|
+
attr_reader :parent, :path, :persistor, :options, :array
|
11
|
+
|
12
|
+
|
13
|
+
# For many methods, we want to call load data as soon as the model is interacted
|
14
|
+
# with, so we proxy the method, then call super.
|
15
|
+
def self.proxy_with_load_data(*method_names)
|
16
|
+
method_names.each do |method_name|
|
17
|
+
define_method(method_name) do |*args|
|
18
|
+
# puts "CALL #{method_name} - Load Data on #{self.inspect}"
|
19
|
+
load_data
|
20
|
+
super(*args)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
proxy_with_load_data :[], :size, :first, :last
|
9
26
|
|
10
27
|
def initialize(array=[], options={})
|
11
28
|
@options = options
|
@@ -20,39 +37,26 @@ class ArrayModel < ReactiveArray
|
|
20
37
|
@persistor.loaded if @persistor
|
21
38
|
end
|
22
39
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
load_data
|
27
|
-
super
|
28
|
-
end
|
29
|
-
|
30
|
-
def size
|
31
|
-
load_data
|
32
|
-
super
|
40
|
+
tag_method(:find) do
|
41
|
+
destructive!
|
42
|
+
pass_reactive!
|
33
43
|
end
|
34
|
-
|
35
|
-
def state
|
44
|
+
def find(*args)
|
36
45
|
if @persistor
|
37
|
-
@persistor.
|
46
|
+
return @persistor.find(*args)
|
38
47
|
else
|
39
|
-
|
48
|
+
raise "this model's persistance layer does not support find, try using store"
|
40
49
|
end
|
41
50
|
end
|
42
51
|
|
43
|
-
|
44
|
-
state == :loaded
|
45
|
-
end
|
46
|
-
|
47
|
-
tag_method(:find) do
|
52
|
+
tag_method(:fetch) do
|
48
53
|
destructive!
|
49
|
-
pass_reactive!
|
50
54
|
end
|
51
|
-
def
|
55
|
+
def fetch(*args, &block)
|
52
56
|
if @persistor
|
53
|
-
return @persistor.
|
57
|
+
return @persistor.fetch(*args, &block)
|
54
58
|
else
|
55
|
-
raise "this model's persistance layer does not support
|
59
|
+
raise "this model's persistance layer does not support fetch, try using store"
|
56
60
|
end
|
57
61
|
end
|
58
62
|
|
@@ -72,7 +76,10 @@ class ArrayModel < ReactiveArray
|
|
72
76
|
super(model)
|
73
77
|
|
74
78
|
@persistor.added(model, @array.size-1) if @persistor
|
79
|
+
|
80
|
+
return model
|
75
81
|
end
|
82
|
+
alias_method :append, :<<
|
76
83
|
|
77
84
|
# Make sure it gets wrapped
|
78
85
|
def inject(*args)
|
@@ -86,8 +93,8 @@ class ArrayModel < ReactiveArray
|
|
86
93
|
super(*args)
|
87
94
|
end
|
88
95
|
|
89
|
-
def new_model(
|
90
|
-
class_at_path(options[:path]).new(
|
96
|
+
def new_model(*args)
|
97
|
+
class_at_path(options[:path]).new(*args)
|
91
98
|
end
|
92
99
|
|
93
100
|
def new_array_model(*args)
|
@@ -114,6 +121,18 @@ class ArrayModel < ReactiveArray
|
|
114
121
|
super
|
115
122
|
end
|
116
123
|
|
124
|
+
tag_method(:buffer) do
|
125
|
+
destructive!
|
126
|
+
end
|
127
|
+
def buffer
|
128
|
+
model_path = options[:path] + [:[]]
|
129
|
+
model_klass = class_at_path(model_path)
|
130
|
+
|
131
|
+
new_options = options.merge(path: model_path, save_to: self).reject {|k,_| k.to_sym == :persistor }
|
132
|
+
model = model_klass.new({}, new_options)
|
133
|
+
|
134
|
+
return ReactiveValue.new(model)
|
135
|
+
end
|
117
136
|
|
118
137
|
private
|
119
138
|
# Takes the persistor if there is one and
|
@@ -130,5 +149,4 @@ class ArrayModel < ReactiveArray
|
|
130
149
|
end
|
131
150
|
end
|
132
151
|
|
133
|
-
|
134
152
|
end
|
data/lib/volt/models/model.rb
CHANGED
@@ -3,6 +3,9 @@ require 'volt/models/array_model'
|
|
3
3
|
require 'volt/models/model_helpers'
|
4
4
|
require 'volt/reactive/object_tracking'
|
5
5
|
require 'volt/models/model_hash_behaviour'
|
6
|
+
require 'volt/models/validations/validations'
|
7
|
+
require 'volt/models/model_state'
|
8
|
+
|
6
9
|
|
7
10
|
class NilMethodCall < NoMethodError
|
8
11
|
def true?
|
@@ -20,16 +23,24 @@ class Model
|
|
20
23
|
include ObjectTracking
|
21
24
|
include ModelHelpers
|
22
25
|
include ModelHashBehaviour
|
26
|
+
include Validations
|
27
|
+
include ModelState
|
23
28
|
|
24
29
|
attr_accessor :attributes
|
25
30
|
attr_reader :parent, :path, :persistor, :options
|
26
31
|
|
27
|
-
def initialize(attributes={}, options={})
|
32
|
+
def initialize(attributes={}, options={}, initial_state=nil)
|
28
33
|
self.options = options
|
29
34
|
|
30
|
-
self.attributes
|
35
|
+
self.send(:attributes=, attributes, true)
|
36
|
+
|
37
|
+
@cache = {}
|
38
|
+
|
39
|
+
# Models stat in a loaded state since they are normally setup from an
|
40
|
+
# ArrayModel, which will have the data when they get added.
|
41
|
+
@state = :loaded
|
31
42
|
|
32
|
-
@persistor.loaded if @persistor
|
43
|
+
@persistor.loaded(initial_state) if @persistor
|
33
44
|
end
|
34
45
|
|
35
46
|
# Update the options
|
@@ -41,6 +52,18 @@ class Model
|
|
41
52
|
@persistor = setup_persistor(options[:persistor])
|
42
53
|
end
|
43
54
|
|
55
|
+
# Assign multiple attributes as a hash, directly.
|
56
|
+
def attributes=(attrs, initial_setup=false)
|
57
|
+
@attributes = wrap_values(attrs)
|
58
|
+
|
59
|
+
unless initial_setup
|
60
|
+
trigger!('changed')
|
61
|
+
|
62
|
+
# Let the persistor know something changed
|
63
|
+
@persistor.changed if @persistor
|
64
|
+
end
|
65
|
+
end
|
66
|
+
alias_method :assign_attributes, :attributes=
|
44
67
|
|
45
68
|
# Pass the comparison through
|
46
69
|
def ==(val)
|
@@ -81,6 +104,9 @@ class Model
|
|
81
104
|
|
82
105
|
# Do the assignment to a model and trigger a changed event
|
83
106
|
def assign_attribute(method_name, *args, &block)
|
107
|
+
# Free any cached value
|
108
|
+
free(method_name)
|
109
|
+
|
84
110
|
self.expand!
|
85
111
|
# Assign, without the =
|
86
112
|
attribute_name = method_name[0..-2].to_sym
|
@@ -107,11 +133,25 @@ class Model
|
|
107
133
|
# The method we are calling is on a nil model, return a wrapped
|
108
134
|
# exception.
|
109
135
|
return return_undefined_method(method_name)
|
110
|
-
elsif attributes && attributes.has_key?(method_name)
|
111
|
-
# Method has the key, look it up directly
|
112
|
-
return attributes[method_name]
|
113
136
|
else
|
114
|
-
|
137
|
+
# See if the value is in attributes
|
138
|
+
value = (attributes && attributes[method_name])
|
139
|
+
|
140
|
+
# Also check @cache
|
141
|
+
value ||= (@cache && @cache[method_name])
|
142
|
+
|
143
|
+
if value
|
144
|
+
# key was in attributes or cache
|
145
|
+
return value
|
146
|
+
else
|
147
|
+
# Cache the value, will be removed when expanded or something
|
148
|
+
# is assigned over it.
|
149
|
+
# TODO: implement a timed out cache flusing
|
150
|
+
new_model = read_new_model(method_name)
|
151
|
+
@cache[method_name] = new_model
|
152
|
+
|
153
|
+
return new_model
|
154
|
+
end
|
115
155
|
end
|
116
156
|
end
|
117
157
|
|
@@ -142,8 +182,12 @@ class Model
|
|
142
182
|
class_at_path(options[:path]).new(attributes, options)
|
143
183
|
end
|
144
184
|
|
145
|
-
def new_array_model(
|
146
|
-
|
185
|
+
def new_array_model(attributes, options)
|
186
|
+
# Start with an empty query
|
187
|
+
options = options.dup
|
188
|
+
options[:query] = {}
|
189
|
+
|
190
|
+
ArrayModel.new(attributes, options)
|
147
191
|
end
|
148
192
|
|
149
193
|
def trigger_by_attribute!(event_name, attribute, *passed_args)
|
@@ -159,15 +203,20 @@ class Model
|
|
159
203
|
end
|
160
204
|
end
|
161
205
|
|
206
|
+
# Removes an item from the cache
|
207
|
+
def free(name)
|
208
|
+
@cache.delete(name)
|
209
|
+
end
|
210
|
+
|
162
211
|
# If this model is nil, it makes it into a hash model, then
|
163
212
|
# sets it up to track from the parent.
|
164
213
|
def expand!
|
165
214
|
if attributes.nil?
|
166
|
-
|
215
|
+
@attributes = {}
|
167
216
|
if @parent
|
168
217
|
@parent.expand!
|
169
218
|
|
170
|
-
@parent.
|
219
|
+
@parent.send(:"#{@path.last}=", self)
|
171
220
|
end
|
172
221
|
end
|
173
222
|
end
|
@@ -196,18 +245,84 @@ class Model
|
|
196
245
|
# Add the new item
|
197
246
|
result << value
|
198
247
|
|
248
|
+
trigger!('added', nil, 0)
|
249
|
+
trigger!('changed')
|
250
|
+
|
199
251
|
return nil
|
200
252
|
end
|
201
253
|
|
202
254
|
def inspect
|
203
|
-
"<#{self.class.to_s} #{attributes.inspect}>"
|
255
|
+
"<#{self.class.to_s}:#{object_id} #{attributes.inspect}>"
|
204
256
|
end
|
205
257
|
|
206
258
|
def deep_cur
|
207
259
|
attributes
|
208
260
|
end
|
209
261
|
|
262
|
+
|
263
|
+
def save!
|
264
|
+
if errors.size == 0
|
265
|
+
puts "SAVING: #{self.errors.inspect} - #{self.inspect} - #{options[:save_to].inspect} on #{self.inspect}"
|
266
|
+
save_to = options[:save_to]
|
267
|
+
if save_to
|
268
|
+
if save_to.is_a?(ArrayModel)
|
269
|
+
# Add to the collection
|
270
|
+
new_model = save_to.append(self.attributes)
|
271
|
+
|
272
|
+
options[:save_to] = new_model
|
273
|
+
else
|
274
|
+
puts "SAVE BUFFERED"
|
275
|
+
# We have a saved model
|
276
|
+
return save_to.assign_attributes(self.attributes)
|
277
|
+
end
|
278
|
+
else
|
279
|
+
raise "Model is not a buffer, can not be saved, modifications should be persisted as they are made."
|
280
|
+
end
|
281
|
+
|
282
|
+
return true
|
283
|
+
else
|
284
|
+
# Some errors, mark all fields
|
285
|
+
self.class.validations.keys.each do |key|
|
286
|
+
mark_field!(key.to_sym)
|
287
|
+
end
|
288
|
+
trigger_for_methods!('changed', :errors, :marked_errors)
|
289
|
+
|
290
|
+
return false
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
|
295
|
+
# Returns a buffered version of the model
|
296
|
+
tag_method(:buffer) do
|
297
|
+
destructive!
|
298
|
+
end
|
299
|
+
def buffer
|
300
|
+
model_path = options[:path]
|
301
|
+
model_klass = class_at_path(model_path)
|
302
|
+
|
303
|
+
new_options = options.merge(path: model_path, save_to: self).reject {|k,_| k.to_sym == :persistor }
|
304
|
+
model = model_klass.new({}, new_options, :loading)
|
305
|
+
|
306
|
+
if state == :loaded
|
307
|
+
setup_buffer(model)
|
308
|
+
else
|
309
|
+
self.parent.fetch do
|
310
|
+
setup_buffer(model)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# puts "SAVE TO:: #{model.options[:save_to].inspect} for #{model.inspect}"
|
315
|
+
|
316
|
+
return ReactiveValue.new(model)
|
317
|
+
end
|
318
|
+
|
319
|
+
|
210
320
|
private
|
321
|
+
def setup_buffer(model)
|
322
|
+
model.attributes = self.attributes
|
323
|
+
model.change_state_to(:loaded)
|
324
|
+
end
|
325
|
+
|
211
326
|
# Clear the previous value and assign a new one
|
212
327
|
def __assign_element(key, value)
|
213
328
|
__clear_element(key)
|