volt 0.9.5.pre4 → 0.9.5.pre5

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