volt 0.9.0.pre4 → 0.9.0.pre5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e4d4f11c399ddcd63de7bc157707e0b1e5a91561
4
- data.tar.gz: 773ade706e860021c11bf7d22c78e868fa51d139
3
+ metadata.gz: c6b166ab0b3c9e33c493d7c65d65d3c5db8f9cdb
4
+ data.tar.gz: 406513b0bc558b964c2207cf6b9ee4f66796d65e
5
5
  SHA512:
6
- metadata.gz: 0542352171aa44247b3471c8e3ea3838e6d2127f896c56312b127936d523840c6d6e76c7ad16126e46844322004ef119bc1949696e83563b9754db8bc014cbf0
7
- data.tar.gz: fcb365747674befac85bf8a8b08bf64addb800fb193500e822a8f4cb7cad4ea20ee51f67688d4aedcb47bc89b189867b9c18d39e65ac79c1f2a680ffb457845a
6
+ metadata.gz: 0350b5ca4ba46fc30c183be534f5e0ba4b0a4ba1af4cfedf4599a354d91a2f72865aaba9d639fe821001740874c9cc6b73d2dff095a673d3b128d22ca3bcd75c
7
+ data.tar.gz: 6ba8bd6674ea7057d27ab6259b203c8397c023b43c39eaf0d66573e36c44d56c1dc606e357a1d921016b3271101b78d7696d417896c697f3a2ed354c74e38446
data/CHANGELOG.md CHANGED
@@ -22,9 +22,12 @@
22
22
  - ```store``` is now available inside of specs. If it is accessed in a spec, the database will be cleaned after the spec.
23
23
  - ```the_page``` is a shortcut to the page collection inside of specs. (Unfortunately, ```page``` is used by capybara, so for now we're using ```the_page```, we'll find a better solution in the future.)
24
24
  - Add filtering to logging on password, and option to configure filtered args. Also, improve the way errors are displayed.
25
+ - You can now call raw in a content binding to render the raw html on the page. Use carefully, this can open you up to xss attacks. We reccomend never showing html from the user directly.
25
26
 
26
27
  ### Changed
27
28
  - template bindings have been renamed to view. ```{{ view "path/for/view" }}``` instead of ```{{ template "path/for/view" }}```
29
+ - view bindings (formerly template) wait until the template's #loaded? method returns true (by .watch! ing it for changes)
30
+ - #loaded? on controllers now returns false if the model is set to a Promise, until the promise is resolved.
28
31
  - the {action}_remove method had been changed to before_{action}_remove and after_{action}_remove to provide more hooks and a clearer understanding of when it is happening.
29
32
  - the following were renamed to follow gem naming conventions:
30
33
  - volt-user-templates (now volt-user_templates)
@@ -46,6 +49,7 @@
46
49
  - Volt::TaskHandler is now Volt::Task
47
50
  - Move testing gems to the generated Gemfile for projects
48
51
  - ```if ENV['BROWSER']``` is no longer required around integration tests. We now use rspec filtering on ```type: :feature``` if you aren't running with ENV['BROWSER']
52
+ - ```go``` has been renamed to ```redirect_to``` to keep things consistent between ruby frameworks. (And to allow for go to be used elsewhere)
49
53
 
50
54
  ### Removed
51
55
  - .false?, .true?, .or, and .and were removed since NilModels were removed. This means you get back a real nil value when accessing an undefined model attribute.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.0.pre4
1
+ 0.9.0.pre5
@@ -19,9 +19,7 @@ class LiveQueryPool < Volt::GenericPool
19
19
 
20
20
  def updated_collection(collection, skip_channel)
21
21
  # collection = collection.to_sym
22
- # puts "RUN UPDATE FOR #{collection.inspect} - #{@pool.inspect}"
23
22
  lookup_all(collection).each do |live_query|
24
- # puts "UPDATE COLLECTION: #{collection} - #{live_query.inspect}"
25
23
  live_query.run(skip_channel)
26
24
  end
27
25
  end
@@ -0,0 +1,101 @@
1
+ # The Actions module adds helpers for setting up and using
2
+ # actions on a class. You can setup helpers for an action with
3
+ #
4
+ # setup_action_helpers_in_class(:before, :after)
5
+ #
6
+ # The above will setup before_action and after_action methods on
7
+ # the class. Typically setup_action_helpers_in_class will be run
8
+ # in a base class.
9
+ #
10
+ # before_action :require_login
11
+ module Volt
12
+ module Actions
13
+ # StopChainException inherits from Exception directly so it will not be handled by a
14
+ # default rescue.
15
+ class StopChainException < Exception ; end
16
+
17
+ module ClassMethods
18
+ # Takes a list of action groups (as symbols). An action group is typically used for before/after, but
19
+ # can be used anytime you have multiple sets of actions. The method will create an {x}_action method for each
20
+ # group passed in.
21
+ def setup_action_helpers_in_class(*groups)
22
+ groups.each do |group|
23
+ # Setup a class attribute to track the
24
+ callbacks_var_name = :"#{group}_action_callbacks"
25
+ class_attribute(callbacks_var_name)
26
+
27
+ # Create the method on the class
28
+ define_singleton_method(:"#{group}_action") do |*args, &block|
29
+ # Add the block in place of the symbol
30
+ args.unshift(block) if block
31
+
32
+ raise "No callback symbol or block provided" unless args[0]
33
+
34
+ callbacks = send(callbacks_var_name)
35
+
36
+ unless callbacks
37
+ callbacks = []
38
+ send(:"#{callbacks_var_name}=", callbacks)
39
+ end
40
+
41
+ if args.last.is_a?(Hash)
42
+ options = args.pop
43
+ else
44
+ options = nil
45
+ end
46
+
47
+ args.each do |callback|
48
+ callbacks << [callback, options]
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ # To run the actions on a class, call #run_actions passing in the group
56
+ # and the action being called on. If the callback chain was stopped with
57
+ # #stop_chain, it will return true, otherwise false.
58
+ def run_actions(group, action)
59
+ callbacks = self.class.send(:"#{group}_action_callbacks")
60
+
61
+ filtered_callbacks = filter_actions_by_only_exclude(callbacks || [], action)
62
+
63
+ begin
64
+ filtered_callbacks.each do |callback|
65
+ instance_eval(&callback)
66
+ end
67
+
68
+ return false
69
+ rescue StopChainException => e
70
+ return true
71
+ end
72
+ end
73
+
74
+ # The stop chain method can be called inside of a callback and it will
75
+ # raise an exception under the hood which will stop the chain and evaluation
76
+ # from where stop_chain is called.
77
+ def stop_chain
78
+ raise StopChainException
79
+ end
80
+
81
+ def self.included(base)
82
+ base.send :extend, ClassMethods
83
+ end
84
+
85
+ private
86
+ # TODO: currently we filter during the call, we could maybe improve performance
87
+ # here by storing by action and having an all category as well.
88
+ def filter_actions_by_only_exclude(callbacks, action)
89
+ callbacks.select do |callback, options|
90
+ if options
91
+ # If there is an only, make sure the action is in the list.
92
+ options[:only].include?(action.to_sym)
93
+ else
94
+ # If no only, include it
95
+ true
96
+ end
97
+ end.map {|v| v[0] }
98
+ end
99
+
100
+ end
101
+ end
@@ -1,15 +1,21 @@
1
1
  require 'volt/reactive/reactive_accessors'
2
+ require 'volt/controllers/actions'
2
3
 
3
4
  module Volt
4
5
  class ModelController
5
6
  include ReactiveAccessors
7
+ include Actions
6
8
 
7
9
  reactive_accessor :current_model
10
+ reactive_accessor :last_promise
8
11
 
9
12
  # The section is assigned a reference to a "DomSection" which has
10
13
  # the dom for the controllers view.
11
14
  attr_accessor :section
12
15
 
16
+ # Setup before_action and after_action
17
+ setup_action_helpers_in_class(:before, :after)
18
+
13
19
  # Container returns the node that is parent to all nodes in the section.
14
20
  def container
15
21
  section.container_node
@@ -40,11 +46,11 @@ module Volt
40
46
  def model=(val)
41
47
  if val.is_a?(Promise)
42
48
  # Resolve the promise before setting
43
- @last_promise = val
49
+ self.last_promise = val
44
50
 
45
51
  val.then do |result|
46
52
  # Only assign if nothing else has been assigned since we started the resolve
47
- self.model = result if @last_promise == val
53
+ self.model = result if self.last_promise == val
48
54
  end.fail do |err|
49
55
  Volt.logger.error("Unable to resolve promise assigned to model on #{inspect}")
50
56
  end
@@ -53,7 +59,7 @@ module Volt
53
59
  end
54
60
 
55
61
  # Clear
56
- @last_promise = nil
62
+ self.last_promise = nil
57
63
 
58
64
  # Start with a nil reactive value.
59
65
  self.current_model ||= Model.new
@@ -106,8 +112,14 @@ module Volt
106
112
  end
107
113
  end
108
114
 
109
- # Change the url params, similar to redirecting to a new url
110
115
  def go(url)
116
+ Volt.logger.warn('Deprecation warning: `go` has been renamed to `redirect_to` for consistency with other frameworks.')
117
+
118
+ redirect_to(url)
119
+ end
120
+
121
+ # Change the url
122
+ def redirect_to(url)
111
123
  # We might be in the rendering loop, so wait until the next tick before
112
124
  # we change the url
113
125
  Timers.next_tick do
@@ -163,8 +175,36 @@ module Volt
163
175
  $page.url.url_with(params)
164
176
  end
165
177
 
178
+ # loaded? is a quick way to see if the model for the controller is loaded
179
+ # yet. If the model is there, it asks the model if its loaded. If the model
180
+ # was set to a promise, it waits for the promise to resolve.
166
181
  def loaded?
167
- self.model.respond_to?(:loaded?) && self.model.loaded?
182
+ if model.respond_to?(:loaded?)
183
+ # There is a model and it is loaded
184
+ return model.loaded?
185
+ elsif last_promise || model.is_a?(Promise)
186
+ # The model is a promise or is resolving
187
+ return false
188
+ else
189
+ # Otherwise, its loaded
190
+ return true
191
+ end
192
+ end
193
+
194
+ def require_login(message="You must login to access this area.")
195
+ unless Volt.current_user_id
196
+ flash._notices << message
197
+ go '/login'
198
+
199
+ stop_chain
200
+ end
201
+ end
202
+
203
+ # Raw marks a string as html safe, so bindings can be rendered as html.
204
+ # With great power comes great responsibility.
205
+ def raw(str)
206
+ str = str.to_s unless str.is_a?(String)
207
+ str.html_safe
168
208
  end
169
209
 
170
210
  # Check if this controller responds_to method, or the model
@@ -30,6 +30,9 @@ module Volt
30
30
  @new = false
31
31
 
32
32
  if new_model
33
+ # Mark the model as loaded
34
+ new_model.change_state_to(:loaded_state, :loaded)
35
+
33
36
  # Set the buffer's id to track the main model's id
34
37
  attributes[:_id] = new_model._id
35
38
  options[:save_to] = new_model
@@ -80,7 +80,7 @@ module Volt
80
80
  if @persistor
81
81
  @persistor.loaded(initial_state)
82
82
  else
83
- change_state_to(:loaded_state, :loaded, false)
83
+ change_state_to(:loaded_state, initial_state || :loaded, false)
84
84
  end
85
85
 
86
86
  # Trigger the new event, pass in :new
@@ -293,8 +293,8 @@ module Volt
293
293
  end
294
294
  end
295
295
 
296
- def new_model(attributes, options)
297
- Volt::Model.class_at_path(options[:path]).new(attributes, options)
296
+ def new_model(*args)
297
+ Volt::Model.class_at_path(options[:path]).new(*args)
298
298
  end
299
299
 
300
300
  def new_array_model(attributes, options)
@@ -3,6 +3,7 @@ module Volt
3
3
  # Implements the base persistor functionality.
4
4
  class Base
5
5
  def loaded(initial_state = nil)
6
+ @model.change_state_to(:loaded_state, initial_state || :loaded)
6
7
  end
7
8
 
8
9
  def changed(attribute_name)
@@ -22,6 +22,14 @@ module Volt
22
22
  @in_identity_map = false
23
23
  end
24
24
 
25
+ def loaded(initial_state = nil)
26
+ if model.path == []
27
+ initial_state = :loaded
28
+ end
29
+
30
+ model.change_state_to(:loaded_state, initial_state)
31
+ end
32
+
25
33
  def add_to_collection
26
34
  @in_collection = true
27
35
  ensure_setup
@@ -1,7 +1,11 @@
1
1
  require 'volt/page/bindings/base_binding'
2
+ require 'volt/page/bindings/html_safe/string_extension'
2
3
 
3
4
  module Volt
4
5
  class ContentBinding < BaseBinding
6
+ HTML_ESCAPE_REGEXP = /[&"'><\n]/
7
+ HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;', "'" => '&#39;', "\n" => "<br />\n" }
8
+
5
9
  def initialize(page, target, context, binding_name, getter)
6
10
  # puts "New Content Binding: #{self.inspect}"
7
11
  super(page, target, context, binding_name)
@@ -20,14 +24,32 @@ module Volt
20
24
  end
21
25
 
22
26
  def update(value)
23
- value = value || ''
27
+ value = (value || '').to_s unless value.is_a?(String)
28
+ html_safe = value.html_safe?
24
29
 
25
30
  # Exception values display the exception as a string
26
- value = value.to_s
31
+ value = value.to_s
27
32
 
28
33
  # Update the html in this section
29
34
  # TODO: Move the formatter into another class.
30
- dom_section.text = value.gsub("\n", "<br />\n")
35
+
36
+ # The html safe check lets us know that if string can be rendered
37
+ # directly as html
38
+ unless html_safe
39
+ # Escape any < and >, but convert newlines to br's, and fix quotes and
40
+ value = html_escape(value)
41
+ end
42
+
43
+ # Assign the content
44
+ dom_section.html = value
45
+ # dom_section.text = value
46
+ end
47
+
48
+ def html_escape(str)
49
+ # https://github.com/opal/opal/issues/798
50
+ str.gsub(HTML_ESCAPE_REGEXP) do |char|
51
+ HTML_ESCAPE[char]
52
+ end
31
53
  end
32
54
 
33
55
  def remove
@@ -0,0 +1,13 @@
1
+ class String
2
+ def html_safe
3
+ # Convert to a real string (opal uses native strings normally, so wrap so we can
4
+ # use instance variables)
5
+ str = String.new(self)
6
+ str.instance_variable_set('@html_safe', true)
7
+ str
8
+ end
9
+
10
+ def html_safe?
11
+ @html_safe
12
+ end
13
+ end
@@ -0,0 +1,44 @@
1
+ module Volt
2
+ class ControllerHandler
3
+ attr_reader :controller, :action
4
+
5
+ def initialize(controller, action)
6
+ @controller = controller
7
+ @action = action.to_sym
8
+ end
9
+
10
+ def call_action(stage_prefix=nil, stage_suffix=nil)
11
+ return unless @action
12
+
13
+ has_stage = stage_prefix || stage_suffix
14
+
15
+ if has_stage
16
+ method_name = @action
17
+ method_name = "#{stage_prefix}_#{method_name}" if stage_prefix
18
+ method_name = "#{method_name}_#{stage_suffix}" if stage_suffix
19
+
20
+ method_name = method_name.to_sym
21
+ else
22
+ method_name = @action
23
+ end
24
+
25
+ # If no stage, then we are calling the main action method,
26
+ # so we should call the before/after actions
27
+ unless has_stage
28
+ if @controller.run_actions(:before, @action)
29
+ # stop_chain was called
30
+ return true
31
+ end
32
+ end
33
+
34
+ if @controller.respond_to?(method_name)
35
+ @controller.send(method_name)
36
+ end
37
+
38
+ @controller.run_actions(:after, @action) unless has_stage
39
+
40
+ # before_action chain was not stopped
41
+ return false
42
+ end
43
+ end
44
+ end
@@ -1,39 +1,27 @@
1
1
  # Some template bindings share the controller with other template bindings based
2
- # on a name. This class keeps track of the number of templates using this controller
3
- # and clears it once no one else is using it. Use #get or #inc to add to the count.
4
- # #clear removes 1 from the count. When the count is 0, delete the controller.
2
+ # on a name. This class creates a cache based on the group_controller name and the
3
+ # controller class.
5
4
  module Volt
6
5
  class GroupedControllers
7
- @@controllers = {}
8
-
9
6
  def initialize(name)
10
7
  @name = name
8
+ @@pool ||= GenericCountingPool.new
11
9
  end
12
10
 
13
- def get
14
- (controller = self.controller) && controller[0]
15
- end
16
-
17
- def set(controller)
18
- @@controllers[@name] = [controller, 1]
11
+ def lookup_or_create(controller, &block)
12
+ @@pool.find(@name, controller, &block)
19
13
  end
20
14
 
21
- def inc
22
- controller[1] += 1
15
+ def remove(controller)
16
+ @@pool.remove(@name, controller)
23
17
  end
24
18
 
25
19
  def clear
26
- controller = self.controller
27
- controller[1] -= 1
28
- if controller[1] == 0
29
- @@controllers.delete(@name)
30
- end
20
+ @@pool.clear
31
21
  end
32
22
 
33
- private
34
-
35
- def controller
36
- @@controllers[@name]
23
+ def inspect
24
+ "<GroupedController @name:#{@name.inspect} #{@@pool.inspect}>"
37
25
  end
38
26
  end
39
27
  end
@@ -2,6 +2,7 @@ require 'volt/page/bindings/base_binding'
2
2
  require 'volt/page/template_renderer'
3
3
  require 'volt/page/bindings/view_binding/grouped_controllers'
4
4
  require 'volt/page/bindings/view_binding/view_lookup_for_path'
5
+ require 'volt/page/bindings/view_binding/controller_handler'
5
6
 
6
7
 
7
8
  module Volt
@@ -31,15 +32,9 @@ module Volt
31
32
  end.watch!
32
33
  end
33
34
 
35
+ # update is called when the path string changes.
34
36
  def update(path, section_or_arguments = nil, options = {})
35
37
  Computation.run_without_tracking do
36
- # Remove existing template and call _removed
37
- controller_send(:"#{@action}_removed") if @action && @controller
38
- if @current_template
39
- @current_template.remove
40
- @current_template = nil
41
- end
42
-
43
38
  @options = options
44
39
 
45
40
  # A blank path needs to load a missing template, otherwise it tries to load
@@ -67,15 +62,57 @@ module Volt
67
62
  # Sometimes we want multiple template bindings to share the same controller (usually
68
63
  # when displaying a :Title and a :Body), this instance tracks those.
69
64
  if @options && (controller_group = @options[:controller_group])
65
+ # Setup the grouped controller for the first time.
70
66
  @grouped_controller = GroupedControllers.new(controller_group)
71
- else
72
- clear_grouped_controller
73
67
  end
74
68
 
69
+ # If a controller is already starting, but not yet started, then remove it.
70
+ remove_starting_controller
71
+
75
72
  full_path, controller_path = @view_lookup.path_for_template(path, section)
76
- render_template(full_path, controller_path)
77
73
 
78
- queue_clear_grouped_controller
74
+ unless full_path
75
+ # if we don't have a full path, then we have a missing template
76
+ render_next_template(full_path, path)
77
+ return
78
+ end
79
+
80
+ @starting_controller_handler, generated_new, chain_stopped = create_controller_handler(full_path, controller_path)
81
+
82
+ # Check if chain was stopped when the action ran
83
+ if chain_stopped
84
+ # An action stopped the chain. When this happens, we stop running here.
85
+ remove_starting_controller
86
+ else
87
+ # None of the actions stopped the chain
88
+
89
+ # Wait until the controller is loaded before we actually render.
90
+ @waiting_for_load = -> { @starting_controller_handler.controller.loaded? }.watch_until!(true) do
91
+ render_next_template(full_path, path)
92
+ end
93
+
94
+ queue_clear_grouped_controller
95
+ end
96
+ end
97
+ end
98
+
99
+ # Called when the next template is ready to render
100
+ def render_next_template(full_path, path)
101
+ begin
102
+ remove_current_controller_and_template
103
+
104
+ # Switch the current template
105
+ @current_controller_handler = @starting_controller_handler
106
+ @starting_controller_handler = nil
107
+
108
+ # Also track the current controller directly
109
+ @controller = @current_controller_handler.controller if full_path
110
+
111
+ @waiting_for_load = nil
112
+ render_template(full_path || path)
113
+ rescue => e
114
+ Volt.logger.error("Error during render of template at #{path}: #{e.inspect}")
115
+ Volt.logger.error(e.backtrace)
79
116
  end
80
117
  end
81
118
 
@@ -101,41 +138,90 @@ module Volt
101
138
  end
102
139
  end
103
140
 
104
- # The context for templates can be either a controller, or the original context.
105
- def render_template(full_path, controller_path)
106
- # If arguments is nil, then an blank SubContext will be created
107
- args = [SubContext.new(@arguments, nil, true)]
141
+ def remove_current_controller_and_template
142
+ # Remove existing controller and template and call _removed
143
+ if @current_controller_handler
144
+ @current_controller_handler.call_action('before', 'remove')
145
+ end
146
+
147
+ if @current_template
148
+ @current_template.remove
149
+ @current_template = nil
150
+ end
151
+
152
+ if @current_controller_handler
153
+ @current_controller_handler.call_action('after', 'remove')
154
+ end
155
+
156
+ if @grouped_controller && @current_controller_handler
157
+ # Remove a reference for the controller in the group.
158
+ @grouped_controller.remove(@current_controller_handler.controller.class)
159
+ end
108
160
 
109
161
  @controller = nil
162
+ @current_controller_handler = nil
163
+ end
110
164
 
111
- # Fetch grouped controllers if we're grouping
112
- @controller = @grouped_controller.get if @grouped_controller
165
+ def remove_starting_controller
166
+ # Clear any previously running wait for loads. This is for when the path changes
167
+ # before the view actually renders.
168
+ if @waiting_for_load
169
+ @waiting_for_load.stop
170
+ end
113
171
 
114
- # The action to be called and rendered
115
- @action = nil
172
+ if @starting_controller_handler
173
+ # Only call the after_..._removed because the dom never loaded.
174
+ @starting_controller_handler.call_action('after', 'removed')
175
+ @starting_controller_handler = nil
176
+ end
177
+ end
116
178
 
117
- if @controller
118
- # Track that we're using the group controller
119
- @grouped_controller.inc if @grouped_controller
179
+ # Create controller handler loads up a controller inside of the controller handler for the paths
180
+ def create_controller_handler(full_path, controller_path)
181
+ # If arguments is nil, then an blank SubContext will be created
182
+ args = [SubContext.new(@arguments, nil, true)]
183
+
184
+ # get the controller class and action
185
+ controller_class, action = get_controller(controller_path)
186
+ controller_class ||= ModelController
187
+
188
+ generated_new = false
189
+ new_controller = Proc.new do
190
+ # Mark that we needed to generate a new controller instance (not reused
191
+ # from the group)
192
+ generated_new = true
193
+ # Setup the controller
194
+ controller_class.new(*args)
195
+ end
196
+
197
+ # Fetch grouped controllers if we're grouping
198
+ if @grouped_controller
199
+ # Find the controller in the group, or create it
200
+ controller = @grouped_controller.lookup_or_create(controller_class, &new_controller)
120
201
  else
121
- # Otherwise, make a new controller
122
- controller_class, @action = get_controller(controller_path)
202
+ # Just create the controller
203
+ controller = new_controller.call
204
+ end
123
205
 
124
- if controller_class
125
- # Setup the controller
126
- @controller = controller_class.new(*args)
127
- else
128
- @controller = ModelController.new(*args)
129
- end
206
+ handler = ControllerHandler.new(controller, action)
130
207
 
131
- # Trigger the action
132
- controller_send(@action) if @action
208
+ if generated_new
209
+ # Call the action
210
+ stopped = handler.call_action
133
211
 
134
- # Track the grouped controller
135
- @grouped_controller.set(@controller) if @grouped_controller
212
+ if stopped
213
+ controller.instance_variable_set('@chain_stopped', true)
214
+ end
215
+ else
216
+ stopped = controller.instance_variable_get('@chain_stopped')
136
217
  end
137
218
 
138
- @current_template = TemplateRenderer.new(@page, @target, @controller, @binding_name, full_path)
219
+ return handler, generated_new, stopped
220
+ end
221
+
222
+ # The context for templates can be either a controller, or the original context.
223
+ def render_template(full_path, path)
224
+ @current_template = TemplateRenderer.new(@page, @target, @controller, @binding_name, full_path, path)
139
225
 
140
226
  call_ready
141
227
  end
@@ -146,49 +232,32 @@ module Volt
146
232
  # the dom if needed
147
233
  # Only assign sections for action's, so we don't get AttributeSections bound
148
234
  # also.
149
- if @action && @controller.respond_to?(:section=)
235
+ if @controller.respond_to?(:section=)
150
236
  @controller.section = @current_template.dom_section
151
237
  end
152
238
 
153
- controller_send(:"#{@action}_ready") if @action
239
+ # Call the ready callback on the controller
240
+ @current_controller_handler.call_action(nil, 'ready')
154
241
  end
155
242
  end
156
243
 
244
+ # Called when the binding is removed from the page
157
245
  def remove
246
+ # Cleanup any starting controller
247
+ remove_starting_controller
248
+
158
249
  @computation.stop
159
250
  @computation = nil
160
251
 
161
- controller_send(:"before_#{@action}_remove") if @controller && @action
162
-
163
- clear_grouped_controller
164
-
165
- if @current_template
166
- # Remove the template if one has been rendered, when the template binding is
167
- # removed.
168
- @current_template.remove
169
- end
252
+ remove_current_controller_and_template
170
253
 
171
254
  super
172
-
173
- if @controller
174
- controller_send(:"after_#{@action}_remove") if @action
175
-
176
- @controller = nil
177
- end
178
255
  end
179
256
 
180
257
  private
181
-
182
- # Sends the action to the controller if it exists
183
- def controller_send(action_name)
184
- if @controller.respond_to?(action_name)
185
- @controller.send(action_name)
186
- end
187
- end
188
-
189
258
  # Fetch the controller class
190
259
  def get_controller(controller_path)
191
- return nil, nil unless controller_path && controller_path.size > 0
260
+ raise "Invalid controller path: #{controller_path.inspect}" unless controller_path && controller_path.size > 0
192
261
 
193
262
  action = controller_path[-1]
194
263
 
@@ -17,8 +17,13 @@ module Volt
17
17
  fail 'not implemented'
18
18
  end
19
19
 
20
+ def set_template
21
+ fail 'not implemented'
22
+ end
23
+
20
24
  def set_content_to_template(page, template_name)
21
25
  if self.is_a?(DomSection)
26
+ # DomTemplates are an optimization when working with the DOM (as opposed to other targets)
22
27
  dom_template = (@@template_cache[template_name] ||= DomTemplate.new(page, template_name))
23
28
 
24
29
  set_template(dom_template)
@@ -29,7 +34,7 @@ module Volt
29
34
  html = template['html']
30
35
  bindings = template['bindings']
31
36
  else
32
- html = "<div>-- &lt; missing template #{template_name.inspect.html_inspect}, make sure it's component is included in dependencies.rb &gt; --</div>"
37
+ html = "<div>-- &lt; missing view or tag at #{template_name.inspect}, make sure it's component is included in dependencies.rb &gt; --</div>"
33
38
  bindings = {}
34
39
  end
35
40
 
@@ -16,7 +16,7 @@ module Volt
16
16
  html = template['html']
17
17
  @bindings = template['bindings']
18
18
  else
19
- html = "<div>-- &lt; missing template #{template_name.inspect.html_inspect}, make sure it's component is included in dependencies.rb &gt; --</div>"
19
+ html = "<div>-- &lt; missing view or tag at #{template_name.inspect}, make sure it's component is included in dependencies.rb &gt; --</div>"
20
20
  @bindings = {}
21
21
  end
22
22
 
@@ -148,7 +148,7 @@ class Proc
148
148
  # @param the value to match
149
149
  # @return [Volt::Computation] the initial computation is returned.
150
150
  def watch_until!(value, &block)
151
- computation = -> do
151
+ computation = Proc.new do |comp|
152
152
  # First fetch the value
153
153
  result = self.call
154
154
 
@@ -159,7 +159,7 @@ class Proc
159
159
  block.call
160
160
 
161
161
  # stop the computation
162
- computation.stop
162
+ comp.stop
163
163
  end
164
164
  end.watch!
165
165
 
@@ -24,7 +24,8 @@ module Volt
24
24
 
25
25
  # Lookups an item
26
26
  def lookup(*args, &block)
27
- item = super(*args, &block)
27
+ # Note: must call without args because of https://github.com/opal/opal/issues/500
28
+ item = super
28
29
 
29
30
  item[1]
30
31
  end
@@ -34,12 +35,14 @@ module Volt
34
35
  end
35
36
 
36
37
  def remove(*args)
37
- item = __lookup(*args)
38
- item[0] -= 1
39
-
40
- if item[0] == 0
41
- # Last one using this item has removed it.
42
- super(*args)
38
+ item = lookup_without_generate(*args)
39
+ if item
40
+ item[0] -= 1
41
+
42
+ if item[0] == 0
43
+ # Last one using this item has removed it.
44
+ super(*args)
45
+ end
43
46
  end
44
47
  end
45
48
  end
@@ -42,6 +42,18 @@ module Volt
42
42
  end
43
43
  end
44
44
 
45
+ # Looks up the path without generating a new one
46
+ def lookup_without_generate(*args)
47
+ section = @pool
48
+
49
+ args.each_with_index do |arg, index|
50
+ section = section[arg]
51
+ return nil unless section
52
+ end
53
+
54
+ section
55
+ end
56
+
45
57
  # Does the actual creating, if a block is not passed in, it calls
46
58
  # #create on the class.
47
59
  def create_new_item(*args)
@@ -97,6 +109,10 @@ module Volt
97
109
  end
98
110
  end
99
111
 
112
+ def inspect
113
+ "<#{self.class.to_s} #{@pool.inspect}>"
114
+ end
115
+
100
116
  def print
101
117
  puts '--- Running Queries ---'
102
118
 
@@ -8,6 +8,8 @@ client '/flash', action: 'flash'
8
8
  client '/yield', action: 'yield'
9
9
  client '/first_last', action: 'first_last'
10
10
  client '/todos', controller: 'todos'
11
+ client '/html_safe', action: 'html_safe'
12
+ client '/missing', action: 'missing'
11
13
 
12
14
  # Signup/login routes
13
15
  client '/signup', component: 'user_templates', controller: 'signup'
@@ -37,6 +37,10 @@ module Main
37
37
  'content'
38
38
  end
39
39
 
40
+ def example_html
41
+ '<button id="examplebutton">Example Button</button>'
42
+ end
43
+
40
44
  private
41
45
 
42
46
  # the main template contains a #template binding that shows another
@@ -0,0 +1,7 @@
1
+ <:Title>
2
+ HTML Safe
3
+
4
+ <:Body>
5
+ <h1>Html Safe</h1>
6
+
7
+ {{ raw example_html }}
@@ -0,0 +1,13 @@
1
+ <:Title>
2
+ Missing Tag and View
3
+
4
+ <:Body>
5
+ <h1>Missing Tag and View</h1>
6
+
7
+ {{ view 'some/wrong/path' }}
8
+
9
+ <hr />
10
+
11
+ <:not:a:component />
12
+
13
+
@@ -0,0 +1,148 @@
1
+ require 'spec_helper'
2
+ require 'volt/controllers/model_controller'
3
+
4
+ class BaseTestActions
5
+ include Volt::Actions
6
+
7
+ setup_action_helpers_in_class(:before, :after)
8
+ end
9
+
10
+ class TestActionsBlocks < BaseTestActions
11
+ before_action do
12
+ @ran_before1 = true
13
+ end
14
+
15
+ before_action do
16
+ @ran_before2 = true
17
+ end
18
+
19
+ attr_reader :ran_before1, :ran_before2
20
+ end
21
+
22
+ class TestActionsSymbolsBase < BaseTestActions
23
+ attr_accessor :ran_one, :ran_two
24
+
25
+ def run_one
26
+ @ran_one = true
27
+ end
28
+
29
+ def run_two
30
+ @ran_two = true
31
+ end
32
+ end
33
+
34
+ class TestActionsSymbols < TestActionsSymbolsBase
35
+ before_action :run_one
36
+ before_action :run_two
37
+ end
38
+
39
+ class TestActions2 < BaseTestActions
40
+ end
41
+
42
+ class TestActionsMultipleSymbols < TestActionsSymbolsBase
43
+ before_action :run_one, :run_two
44
+ end
45
+
46
+ # Runs three actions, stopping the chanin after one
47
+ class TestStopCallbacks < BaseTestActions
48
+ before_action :run_one, :run_two, :run_three
49
+ attr_accessor :ran_one, :ran_two, :ran_end_of_two, :ran_three
50
+
51
+ def run_one
52
+ @ran_one = true
53
+ end
54
+
55
+ def run_two
56
+ @ran_two = true
57
+ stop_chain
58
+ @ran_end_of_two = true
59
+ end
60
+
61
+ def run_three
62
+ @ran_three = true
63
+ end
64
+ end
65
+
66
+ class TestNoCallbacks < BaseTestActions
67
+ end
68
+
69
+ class TestOnlyCallbacks < TestActionsSymbolsBase
70
+ before_action :run_one, :run_two, only: [:new]
71
+ end
72
+
73
+ describe Volt::Actions do
74
+ it 'should trigger before actions via blocks' do
75
+ test_class = TestActionsBlocks.new
76
+
77
+ expect(test_class.ran_before1).to eq(nil)
78
+ expect(test_class.ran_before2).to eq(nil)
79
+
80
+ test_class.run_actions(:before, :index)
81
+
82
+ expect(test_class.ran_before1).to eq(true)
83
+ expect(test_class.ran_before2).to eq(true)
84
+ end
85
+
86
+ it 'should trigger before actions via symbols' do
87
+ test_class = TestActionsSymbols.new
88
+
89
+ expect(test_class.ran_one).to eq(nil)
90
+ expect(test_class.ran_two).to eq(nil)
91
+
92
+ test_class.run_actions(:before, :index)
93
+
94
+ expect(test_class.ran_one).to eq(true)
95
+ expect(test_class.ran_two).to eq(true)
96
+
97
+ end
98
+
99
+ it 'should raise an exception if no symbol or block is provided' do
100
+ expect do
101
+ TestActions2.before_action
102
+ end.to raise_error(RuntimeError, "No callback symbol or block provided")
103
+ end
104
+
105
+ it 'should support multiple symbols passed an action helper' do
106
+ test_class = TestActionsMultipleSymbols.new
107
+
108
+ expect(test_class.ran_one).to eq(nil)
109
+ expect(test_class.ran_two).to eq(nil)
110
+
111
+ result = test_class.run_actions(:before, :index)
112
+ expect(result).to eq(false)
113
+
114
+ expect(test_class.ran_one).to eq(true)
115
+ expect(test_class.ran_two).to eq(true)
116
+ end
117
+
118
+ it 'should stop the chain when #stop_chain is called and return false from #run_actions' do
119
+ test_class = TestStopCallbacks.new
120
+
121
+ result = test_class.run_actions(:before, :index)
122
+ expect(result).to eq(true)
123
+
124
+ expect(test_class.ran_one).to eq(true)
125
+ expect(test_class.ran_two).to eq(true)
126
+ expect(test_class.ran_end_of_two).to eq(nil)
127
+ expect(test_class.ran_three).to eq(nil)
128
+ end
129
+
130
+ it 'should call without any callbacks' do
131
+ test_class = TestNoCallbacks.new
132
+
133
+ result = test_class.run_actions(:before, :index)
134
+ expect(result).to eq(false)
135
+ end
136
+
137
+ it 'should follow only limitations' do
138
+ test_only = TestOnlyCallbacks.new
139
+
140
+ test_only.run_actions(:before, :index)
141
+ expect(test_only.ran_one).to eq(nil)
142
+ expect(test_only.ran_two).to eq(nil)
143
+
144
+ test_only.run_actions(:before, :new)
145
+ expect(test_only.ran_one).to eq(true)
146
+ expect(test_only.ran_two).to eq(true)
147
+ end
148
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ if RUBY_PLATFORM != 'opal'
4
+ describe Volt::ModelController do
5
+ it 'should accept a promise as a model and resolve it' do
6
+ controller = Volt::ModelController.new
7
+
8
+ promise = Promise.new
9
+
10
+ controller.model = promise
11
+
12
+ expect(controller.model).to eq(nil)
13
+
14
+ promise.resolve(20)
15
+
16
+ expect(controller.model).to eq(20)
17
+ end
18
+
19
+ it 'should not return true from loaded until the promise is resolved' do
20
+ controller = Volt::ModelController.new
21
+
22
+ promise = Promise.new
23
+ controller.model = promise
24
+
25
+ expect(controller.loaded?).to eq(false)
26
+
27
+ promise.resolve(Volt::Model.new)
28
+ expect(controller.loaded?).to eq(true)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe "missing tags and view's", type: :feature do
4
+ it 'should show a message about the missing tag/view' do
5
+ visit '/missing'
6
+
7
+ expect(page).to have_content('view or tag at "some/wrong/path"')
8
+ expect(page).to have_content('view or tag at "not/a/component"')
9
+
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe "HTML safe/raw", type: :feature do
4
+ it 'should render html with the raw helper' do
5
+ visit '/html_safe'
6
+
7
+ expect(page).to have_selector('button[id="examplebutton"]')
8
+ end
9
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: volt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0.pre4
4
+ version: 0.9.0.pre5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Stout
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-10 00:00:00.000000000 Z
11
+ date: 2015-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -480,6 +480,7 @@ files:
480
480
  - lib/volt/cli/new_gem.rb
481
481
  - lib/volt/cli/runner.rb
482
482
  - lib/volt/config.rb
483
+ - lib/volt/controllers/actions.rb
483
484
  - lib/volt/controllers/http_controller.rb
484
485
  - lib/volt/controllers/model_controller.rb
485
486
  - lib/volt/data_stores/data_store.rb
@@ -544,8 +545,10 @@ files:
544
545
  - lib/volt/page/bindings/content_binding.rb
545
546
  - lib/volt/page/bindings/each_binding.rb
546
547
  - lib/volt/page/bindings/event_binding.rb
548
+ - lib/volt/page/bindings/html_safe/string_extension.rb
547
549
  - lib/volt/page/bindings/if_binding.rb
548
550
  - lib/volt/page/bindings/view_binding.rb
551
+ - lib/volt/page/bindings/view_binding/controller_handler.rb
549
552
  - lib/volt/page/bindings/view_binding/grouped_controllers.rb
550
553
  - lib/volt/page/bindings/view_binding/view_lookup_for_path.rb
551
554
  - lib/volt/page/bindings/yield_binding.rb
@@ -646,8 +649,10 @@ files:
646
649
  - spec/apps/kitchen_sink/app/main/views/main/cookie_test.html
647
650
  - spec/apps/kitchen_sink/app/main/views/main/first_last.html
648
651
  - spec/apps/kitchen_sink/app/main/views/main/flash.html
652
+ - spec/apps/kitchen_sink/app/main/views/main/html_safe.html
649
653
  - spec/apps/kitchen_sink/app/main/views/main/index.html
650
654
  - spec/apps/kitchen_sink/app/main/views/main/main.html
655
+ - spec/apps/kitchen_sink/app/main/views/main/missing.html
651
656
  - spec/apps/kitchen_sink/app/main/views/main/store.html
652
657
  - spec/apps/kitchen_sink/app/main/views/main/yield.html
653
658
  - spec/apps/kitchen_sink/app/main/views/todos/index.html
@@ -656,7 +661,9 @@ files:
656
661
  - spec/apps/kitchen_sink/config.ru
657
662
  - spec/apps/kitchen_sink/config/app.rb
658
663
  - spec/apps/kitchen_sink/config/base/index.html
664
+ - spec/controllers/actions_spec.rb
659
665
  - spec/controllers/http_controller_spec.rb
666
+ - spec/controllers/model_controller_spec.rb
660
667
  - spec/controllers/reactive_accessors_spec.rb
661
668
  - spec/extra_core/array_spec.rb
662
669
  - spec/extra_core/blank_spec.rb
@@ -673,6 +680,8 @@ files:
673
680
  - spec/integration/flash_spec.rb
674
681
  - spec/integration/http_endpoints_spec.rb
675
682
  - spec/integration/list_spec.rb
683
+ - spec/integration/missing_spec.rb
684
+ - spec/integration/raw_html_binding.rb
676
685
  - spec/integration/templates_spec.rb
677
686
  - spec/integration/url_spec.rb
678
687
  - spec/integration/user_spec.rb
@@ -849,8 +858,10 @@ test_files:
849
858
  - spec/apps/kitchen_sink/app/main/views/main/cookie_test.html
850
859
  - spec/apps/kitchen_sink/app/main/views/main/first_last.html
851
860
  - spec/apps/kitchen_sink/app/main/views/main/flash.html
861
+ - spec/apps/kitchen_sink/app/main/views/main/html_safe.html
852
862
  - spec/apps/kitchen_sink/app/main/views/main/index.html
853
863
  - spec/apps/kitchen_sink/app/main/views/main/main.html
864
+ - spec/apps/kitchen_sink/app/main/views/main/missing.html
854
865
  - spec/apps/kitchen_sink/app/main/views/main/store.html
855
866
  - spec/apps/kitchen_sink/app/main/views/main/yield.html
856
867
  - spec/apps/kitchen_sink/app/main/views/todos/index.html
@@ -859,7 +870,9 @@ test_files:
859
870
  - spec/apps/kitchen_sink/config.ru
860
871
  - spec/apps/kitchen_sink/config/app.rb
861
872
  - spec/apps/kitchen_sink/config/base/index.html
873
+ - spec/controllers/actions_spec.rb
862
874
  - spec/controllers/http_controller_spec.rb
875
+ - spec/controllers/model_controller_spec.rb
863
876
  - spec/controllers/reactive_accessors_spec.rb
864
877
  - spec/extra_core/array_spec.rb
865
878
  - spec/extra_core/blank_spec.rb
@@ -876,6 +889,8 @@ test_files:
876
889
  - spec/integration/flash_spec.rb
877
890
  - spec/integration/http_endpoints_spec.rb
878
891
  - spec/integration/list_spec.rb
892
+ - spec/integration/missing_spec.rb
893
+ - spec/integration/raw_html_binding.rb
879
894
  - spec/integration/templates_spec.rb
880
895
  - spec/integration/url_spec.rb
881
896
  - spec/integration/user_spec.rb