volt 0.9.5.pre4 → 0.9.5.pre5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +13 -5
  4. data/app/volt/assets/css/{notices.css.scss → notices.scss} +0 -0
  5. data/app/volt/models/active_volt_instance.rb +1 -1
  6. data/app/volt/tasks/live_query/live_query.rb +11 -3
  7. data/app/volt/tasks/store_tasks.rb +14 -17
  8. data/lib/volt/cli.rb +22 -0
  9. data/lib/volt/cli/asset_compile.rb +63 -63
  10. data/lib/volt/cli/base_index_renderer.rb +26 -0
  11. data/lib/volt/cli/generate.rb +1 -1
  12. data/lib/volt/config.rb +1 -0
  13. data/lib/volt/controllers/model_controller.rb +37 -1
  14. data/lib/volt/extra_core/array.rb +22 -0
  15. data/lib/volt/models/array_model.rb +7 -1
  16. data/lib/volt/models/errors.rb +1 -1
  17. data/lib/volt/models/field_helpers.rb +36 -21
  18. data/lib/volt/models/model.rb +16 -0
  19. data/lib/volt/models/validations/validations.rb +21 -6
  20. data/lib/volt/models/validators/type_validator.rb +35 -3
  21. data/lib/volt/page/bindings/content_binding.rb +1 -1
  22. data/lib/volt/page/bindings/event_binding.rb +40 -16
  23. data/lib/volt/page/document_events.rb +8 -6
  24. data/lib/volt/reactive/reactive_array.rb +18 -1
  25. data/lib/volt/server/forking_server.rb +7 -1
  26. data/lib/volt/server/html_parser/attribute_scope.rb +26 -0
  27. data/lib/volt/server/html_parser/component_view_scope.rb +30 -22
  28. data/lib/volt/server/middleware/default_middleware_stack.rb +6 -1
  29. data/lib/volt/server/rack/asset_files.rb +5 -3
  30. data/lib/volt/server/rack/opal_files.rb +35 -23
  31. data/lib/volt/server/rack/sprockets_helpers_setup.rb +71 -0
  32. data/lib/volt/server/template_handlers/view_processor.rb +1 -2
  33. data/lib/volt/utils/promise_extensions.rb +1 -1
  34. data/lib/volt/version.rb +1 -1
  35. data/lib/volt/volt/app.rb +0 -2
  36. data/lib/volt/volt/client_setup/browser.rb +11 -0
  37. data/spec/apps/kitchen_sink/Gemfile +37 -14
  38. data/spec/apps/kitchen_sink/app/main/config/routes.rb +3 -0
  39. data/spec/apps/kitchen_sink/app/main/controllers/events_controller.rb +26 -0
  40. data/spec/apps/kitchen_sink/app/main/views/events/index.html +30 -0
  41. data/spec/apps/kitchen_sink/app/main/views/main/bindings.html +3 -0
  42. data/spec/apps/kitchen_sink/app/main/views/main/yield.html +1 -6
  43. data/spec/apps/kitchen_sink/app/main/views/{yield-component → yield_component}/index.html +0 -0
  44. data/spec/extra_core/array_spec.rb +26 -0
  45. data/spec/integration/bindings_spec.rb +9 -0
  46. data/spec/integration/event_spec.rb +19 -0
  47. data/spec/models/array_model_spec.rb +13 -0
  48. data/spec/models/field_helpers_spec.rb +2 -2
  49. data/spec/models/validations_spec.rb +31 -0
  50. data/spec/models/validators/type_validator_spec.rb +47 -1
  51. data/spec/reactive/reactive_array_spec.rb +46 -0
  52. data/spec/server/forking_server_spec.rb +27 -0
  53. data/spec/server/html_parser/view_scope_spec.rb +44 -0
  54. data/spec/server/rack/asset_files_spec.rb +2 -2
  55. data/templates/project/Gemfile.tt +8 -0
  56. data/templates/project/config/app.rb.tt +2 -1
  57. data/volt.gemspec +1 -1
  58. metadata +31 -5
@@ -8,4 +8,26 @@ class Array
8
8
  def to_h
9
9
  Hash[self]
10
10
  end
11
+
12
+
13
+ # Converts an array to a sentence
14
+ def to_sentence(options={})
15
+ conjunction = options.fetch(:conjunction, 'and')
16
+ comma = options.fetch(:comma, ',')
17
+ oxford = options.fetch(:oxford, true) # <- true is the right value
18
+
19
+ case size
20
+ when 0
21
+ ''
22
+ when 1
23
+ self[0].to_s
24
+ when 2
25
+ self.join(" #{conjunction} ")
26
+ else
27
+ str = self[0..-2].join(comma + ' ')
28
+ str += comma if oxford
29
+ str += " #{conjunction} " + self[-1].to_s
30
+ str
31
+ end
32
+ end
11
33
  end
@@ -241,6 +241,7 @@ module Volt
241
241
  def new_model(*args)
242
242
  Volt::Model.class_at_path(options[:path]).new(*args)
243
243
  end
244
+ alias_method :new, :new_model
244
245
 
245
246
  def new_array_model(*args)
246
247
  Volt::ArrayModel.class_at_path(options[:path]).new(*args)
@@ -306,6 +307,11 @@ module Volt
306
307
  name.pluralize
307
308
  end
308
309
 
310
+ alias_method :reactive_count, :count
311
+ def count(&block)
312
+ all.reactive_count(&block)
313
+ end
314
+
309
315
  private
310
316
  # called form <<, append, and create. If a hash is passed in, it converts
311
317
  # it to a model. Then it takes the model and inserts it into the ArrayModel
@@ -381,7 +387,7 @@ module Volt
381
387
  end
382
388
 
383
389
  # We need to setup the proxy methods below where they are defined.
384
- proxy_with_load :[], :size, :last, :reverse, :all, :to_a, :empty?, :present?, :blank?
390
+ proxy_with_load :[], :size, :length, :last, :reverse, :all, :to_a, :empty?, :present?, :blank?
385
391
 
386
392
  end
387
393
  end
@@ -26,7 +26,7 @@ module Volt
26
26
  str << "#{field} #{error}"
27
27
  end
28
28
 
29
- str.join
29
+ str.join(', ')
30
30
  end
31
31
  end
32
32
  end
@@ -22,34 +22,45 @@ module FieldHelpers
22
22
  end
23
23
 
24
24
  FIELD_CASTS = {
25
- String => :to_s.to_proc,
26
- Fixnum => lambda {|val| NUMERIC_CAST[:Integer, val] },
27
- Numeric => lambda {|val| NUMERIC_CAST[:Float, val] },
28
- Float => lambda {|val| NUMERIC_CAST[:Float, val] },
29
- Time => nil,
30
- TrueClass => nil,
31
- FalseClass => nil
25
+ String => :to_s.to_proc,
26
+ Fixnum => lambda {|val| NUMERIC_CAST[:Integer, val] },
27
+ Numeric => lambda {|val| NUMERIC_CAST[:Float, val] },
28
+ Float => lambda {|val| NUMERIC_CAST[:Float, val] },
29
+ Time => nil,
30
+ TrueClass => nil,
31
+ FalseClass => nil,
32
+ NilClass => nil,
33
+ Volt::Boolean => nil
32
34
  }
33
- VALID_FIELD_CLASSES = FIELD_CASTS.keys
34
35
 
35
36
 
36
37
  module ClassMethods
37
38
  # field lets you declare your fields instead of using the underscore syntax.
38
39
  # An optional class restriction can be passed in.
39
- def field(name, klass = nil, options = {})
40
- if klass && !VALID_FIELD_CLASSES.include?(klass)
41
- klass_names = VALID_FIELD_CLASSES.map(&:to_s).join(', ')
42
- msg = "valid field types is currently limited to #{klass_names}"
43
- fail FieldHelpers::InvalidFieldClass, msg
40
+ def field(name, klasses = nil, options = {})
41
+ if klasses
42
+ klasses = [klasses].flatten
43
+
44
+ unless klasses.any? {|kl| FIELD_CASTS.key?(kl) }
45
+ klass_names = FIELD_CASTS.keys.map(&:to_s).join(', ')
46
+ msg = "valid field types is currently limited to #{klass_names}, you passed: #{klasses.inspect}"
47
+ fail FieldHelpers::InvalidFieldClass, msg
48
+ end
49
+
50
+ # Add NilClass as an allowed type unless allow_nil: false was passed.
51
+ unless options[:allow_nil] == false
52
+ klasses << NilClass
53
+ end
44
54
  end
45
55
 
46
56
  self.fields_data ||= {}
47
- self.fields_data[name] = [klass, options]
57
+ self.fields_data[name] = [klasses, options]
48
58
 
49
- if klass
50
- # Add type validation, execpt for String, since anything can be a string.
51
- unless klass == String
52
- validate name, type: klass
59
+ if klasses
60
+ # Add type validation, execpt for String, since anything can be cast to
61
+ # a string.
62
+ unless klasses.include?(String)
63
+ validate name, type: klasses
53
64
  end
54
65
  end
55
66
 
@@ -59,10 +70,14 @@ module FieldHelpers
59
70
 
60
71
  define_method(:"#{name}=") do |val|
61
72
  # Check if the value assigned matches the class restriction
62
- if klass
73
+ if klasses
63
74
  # Cast to the right type
64
- if (func = FIELD_CASTS[klass])
65
- val = func[val]
75
+ klasses.each do |kl|
76
+ if (func = FIELD_CASTS[kl])
77
+ # Cast on the first available caster
78
+ val = func[val]
79
+ break
80
+ end
66
81
  end
67
82
  end
68
83
 
@@ -354,6 +354,22 @@ module Volt
354
354
  to_h.to_json
355
355
  end
356
356
 
357
+ # Update tries to update the model and returns
358
+ def update(attrs)
359
+ old_attrs = @attributes.dup
360
+ Model.no_change_tracking do
361
+ assign_all_attributes(attrs, false)
362
+
363
+ validate!.then do |errs|
364
+ if errs && errs.present?
365
+ # Revert wholesale
366
+ @attributes = old_attrs
367
+ Promise.new.resolve(errs)
368
+ end
369
+ end
370
+ end
371
+ end
372
+
357
373
  private
358
374
  def run_initial_setup(initial_setup)
359
375
  # Save the changes
@@ -42,12 +42,15 @@ module Volt
42
42
 
43
43
  if run_in_actions.size == 0 || run_in_actions.include?(action)
44
44
  @instance_validations = {}
45
+ @instance_custom_validations = []
45
46
 
46
47
  instance_exec(action, &block)
47
48
 
48
49
  result = run_validations(@instance_validations)
50
+ result.merge!(run_custom_validations(@instance_custom_validations))
49
51
 
50
52
  @instance_validations = nil
53
+ @instance_custom_validations = nil
51
54
 
52
55
  result
53
56
  end
@@ -55,9 +58,19 @@ module Volt
55
58
  end
56
59
  end
57
60
 
58
- def validate(field_name = nil, options = nil)
59
- @instance_validations[field_name] ||= {}
60
- @instance_validations[field_name].merge!(options)
61
+ # Called on the model inside of a validations block. Allows the user to
62
+ # control if validations should be run.
63
+ def validate(field_name = nil, options = nil, &block)
64
+ if block
65
+ # Setup a custom validation inside of the current validations block.
66
+ if field_name || options
67
+ fail 'validate should be passed a field name and options or a block, not both.'
68
+ end
69
+ @instance_custom_validations << block
70
+ else
71
+ @instance_validations[field_name] ||= {}
72
+ @instance_validations[field_name].merge!(options)
73
+ end
61
74
  end
62
75
 
63
76
  def self.included(base)
@@ -200,10 +213,12 @@ module Volt
200
213
  promise
201
214
  end
202
215
 
203
- def run_custom_validations
216
+ def run_custom_validations(custom_validations = nil)
217
+ # Default to running the class level custom validations
218
+ custom_validations ||= self.class.custom_validations
219
+
204
220
  promise = Promise.new.resolve(nil)
205
- # Call all of the custom validations
206
- custom_validations = self.class.custom_validations
221
+
207
222
  if custom_validations
208
223
  custom_validations.each do |custom_validation|
209
224
  # Add to the promise chain
@@ -1,17 +1,49 @@
1
1
  # Enforces a type on a field. Typically setup from ```field :name, Type```
2
2
  module Volt
3
+ # Volt::Boolean can be used if you want a boolean type
4
+ class Boolean
5
+ end
6
+
3
7
  class TypeValidator
4
8
  def self.validate(model, field_name, args)
5
9
  errors = {}
6
10
  value = model.get(field_name)
7
11
 
8
- type_restriction = args.is_a?(Hash) ? args[:type] : args
12
+ type_restriction = args.is_a?(Hash) ? (args[:type] || args[:types]) : args
9
13
 
10
- unless value.is_a?(type_restriction)
14
+ # Make into an array of 1 if its not already an array.
15
+ type_restrictions = [type_restriction].flatten
16
+
17
+ valid_type = false
18
+ type_restrictions.each do |type_rest|
19
+ if value.is_a?(type_rest)
20
+ valid_type = true
21
+ break
22
+ end
23
+ end
24
+
25
+ unless valid_type
11
26
  if args.is_a?(Hash) && args[:message]
12
27
  message = args[:message]
13
28
  else
14
- message = "must be of type #{type_restriction.to_s}"
29
+ type_msgs = type_restrictions.map do |type_rest|
30
+ if [Fixnum, Float, Numeric].include?(type_rest)
31
+ "a number"
32
+ elsif type_rest == NilClass
33
+ # we don't mention the nil restriction
34
+ nil
35
+ elsif type_rest == Volt::Boolean
36
+ ['true', 'false']
37
+ elsif type_rest == TrueClass
38
+ 'true'
39
+ elsif type_rest == FalseClass
40
+ 'false'
41
+ else
42
+ "a #{type_rest.to_s}"
43
+ end
44
+ end.flatten
45
+
46
+ message = "must be #{type_msgs.compact.to_sentence(conjunction: 'or')}"
15
47
  end
16
48
 
17
49
  errors[field_name] = [message]
@@ -50,7 +50,7 @@ module Volt
50
50
  # https://github.com/opal/opal/issues/798
51
51
  str.gsub(HTML_ESCAPE_REGEXP) do |char|
52
52
  HTML_ESCAPE[char]
53
- end
53
+ end.gsub(' ', " \u00A0").gsub("\n ", "\n\u00A0")
54
54
  end
55
55
 
56
56
  def remove
@@ -5,6 +5,9 @@ module Volt
5
5
  class JSEvent
6
6
  attr_reader :js_event
7
7
 
8
+ # The Volt controller that dispatched the event.
9
+ attr_accessor :controller
10
+
8
11
  def initialize(js_event)
9
12
  @js_event = js_event
10
13
  end
@@ -43,31 +46,52 @@ module Volt
43
46
  end
44
47
 
45
48
 
46
- handler = proc do |js_event|
49
+ handler = proc do |js_event, *args|
47
50
  event = JSEvent.new(js_event)
48
51
  event.prevent_default! if event_name == 'submit'
49
52
 
50
- # Call the proc the user setup for the event in context,
51
- # pass in the wrapper for the JS event
52
- result = @context.instance_exec(event, &call_proc)
53
-
54
- # The following doesn't work due to the promise already chained issue.
55
- # # Ignore native objects.
56
- # result = nil unless BasicObject === result
53
+ # When the event is triggered via ```trigger(..)``` in a controller,
54
+ # it will pass its self as the first argument. We set that to
55
+ # ```controller``` on the event, so it can be easily accessed.
56
+ if args[0].is_a?(Volt::ModelController)
57
+ args = args.dup
58
+ event.controller = args.shift
59
+ end
57
60
 
58
- # # if the result is a promise, log an exception if it failed and wasn't
59
- # # handled
60
- # if result.is_a?(Promise) && !result.next
61
- # result.fail do |err|
62
- # Volt.logger.error("EventBinding Error: promise returned from event binding #{@event_name} was rejected")
63
- # Volt.logger.error(err)
64
- # end
65
- # end
61
+ args << event
66
62
 
63
+ self.class.call_handler_proc(@context, call_proc, event, args)
67
64
  end
65
+
68
66
  @listener = browser.events.add(@event_name, self, handler)
69
67
  end
70
68
 
69
+ def self.call_handler_proc(context, call_proc, event, args)
70
+ # When the EventBinding is compiled, it converts a passed in string to
71
+ # get a Method:
72
+ #
73
+ # Example:
74
+ # <a e-awesome="some_method">...</a>
75
+ #
76
+ # The call_proc will be passed in as: Proc.new { method(:some_method) }
77
+ #
78
+ # So first we call the call_proc, then that returns a method (or proc),
79
+ # which we call passing in the arguments based on the arity.
80
+ #
81
+ # If the e- binding has arguments passed to it, we just use those.
82
+ result = context.instance_exec(event, &call_proc)
83
+ # Trim args to match arity
84
+
85
+ # The proc returned a
86
+ if result && result.is_a?(Method)
87
+ args = args[0...result.arity]
88
+
89
+ result.call(*args)
90
+ end
91
+
92
+ result
93
+ end
94
+
71
95
  # Remove the event binding
72
96
  def remove
73
97
  browser.events.remove(@event_name, self)
@@ -14,10 +14,12 @@ module Volt
14
14
 
15
15
  that = self
16
16
 
17
+ document_handler = proc do |*args|
18
+ handle(event, *args)
19
+ end
20
+
17
21
  `
18
- $('body').on(event, function(e) {
19
- that.$handle(event, e, e.target || e.originalEvent.target);
20
- });
22
+ $('body').on(event, #{document_handler});
21
23
  `
22
24
 
23
25
  end
@@ -26,8 +28,8 @@ module Volt
26
28
  @events[event][binding.binding_name][binding.object_id] = handler
27
29
  end
28
30
 
29
- def handle(event_name, event, target)
30
- element = `$(#{target})`
31
+ def handle(event_name, event, *args)
32
+ element = `$(event.target || event.originalEvent.target)`
31
33
 
32
34
  loop do
33
35
  # Lookup the handler, make sure to not assume the group
@@ -43,7 +45,7 @@ module Volt
43
45
  if handlers
44
46
  handlers.values.each do |handler|
45
47
  # Call each handler for this object
46
- handler.call(event)
48
+ handler.call(event, *args)
47
49
  end
48
50
  end
49
51
 
@@ -93,10 +93,27 @@ module Volt
93
93
  end
94
94
  end
95
95
 
96
+ def last
97
+ self[-1]
98
+ end
99
+
96
100
  # TODO: Handle a range
97
101
  def [](index)
98
102
  # Handle a negative index, depend on size
99
- index = @array.size + index if index < 0
103
+ if index < 0
104
+ # Depend on size by calling .size, since we are looking up reverse
105
+ # indexes
106
+
107
+ # cache size lookup
108
+ size = self.size
109
+
110
+ index = size + index
111
+
112
+ # In this case, we're looking back past 0 (going backwards), so we get
113
+ # nil. Since we're depending on @size_dep (because we called .size),
114
+ # it will invalidate when the size changes.
115
+ return nil if index < 0
116
+ end
100
117
 
101
118
  # Get or create the dependency
102
119
  dep = (@array_deps[index] ||= Dependency.new)
@@ -257,8 +257,14 @@ module Volt
257
257
 
258
258
  def start_change_listener
259
259
  sync_mod_time
260
+
261
+ options = {}
262
+ if ENV['POLL_FS']
263
+ options[:force_polling] = true
264
+ end
265
+
260
266
  # Setup the listeners for file changes
261
- @listener = Listen.to("#{@server.app_path}/") do |modified, added, removed|
267
+ @listener = Listen.to("#{@server.app_path}/", options) do |modified, added, removed|
262
268
  Thread.new do
263
269
  # Run the reload in a new thread
264
270
  reload(modified + added + removed)