ovto 0.5.0 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.gitmodules +3 -0
  3. data/CHANGELOG.md +14 -2
  4. data/Gemfile +2 -3
  5. data/Gemfile.lock +23 -27
  6. data/book/SUMMARY.md +2 -0
  7. data/book/api/actions.md +1 -1
  8. data/book/api/middleware.md +99 -0
  9. data/book/api/pure_component.md +3 -0
  10. data/docs/api/Array.html +190 -0
  11. data/docs/api/Hash.html +196 -0
  12. data/docs/api/Ovto/Actions.html +128 -40
  13. data/docs/api/Ovto/App.html +254 -40
  14. data/docs/api/Ovto/Component/MoreThanOneNode.html +6 -6
  15. data/docs/api/Ovto/Component.html +96 -29
  16. data/docs/api/Ovto/Middleware/Actions.html +428 -0
  17. data/docs/api/Ovto/Middleware/Base.html +605 -0
  18. data/docs/api/Ovto/Middleware/Component.html +354 -0
  19. data/docs/api/Ovto/Middleware.html +286 -0
  20. data/docs/api/Ovto/PureComponent/StateIsNotAvailable.html +6 -6
  21. data/docs/api/Ovto/PureComponent.html +11 -11
  22. data/docs/api/Ovto/Runtime.html +7 -7
  23. data/docs/api/Ovto/State/MissingValue.html +6 -6
  24. data/docs/api/Ovto/State/{UnknownKey.html → UnknownStateKey.html} +11 -11
  25. data/docs/api/Ovto/State.html +59 -43
  26. data/docs/api/Ovto/WiredActionSet.html +636 -0
  27. data/docs/api/Ovto/WiredActions.html +78 -35
  28. data/docs/api/Ovto.html +223 -30
  29. data/docs/api/_index.html +77 -11
  30. data/docs/api/actions.html +29 -3
  31. data/docs/api/app.html +28 -2
  32. data/docs/api/class_list.html +3 -3
  33. data/docs/api/component.html +30 -4
  34. data/docs/api/css/style.css +3 -2
  35. data/docs/api/fetch.html +30 -4
  36. data/docs/api/file.README.html +76 -78
  37. data/docs/api/file_list.html +2 -2
  38. data/docs/api/frames.html +2 -2
  39. data/docs/api/index.html +76 -78
  40. data/docs/api/js/app.js +14 -3
  41. data/docs/api/method_list.html +303 -31
  42. data/docs/api/middleware.html +498 -0
  43. data/docs/api/pure_component.html +428 -0
  44. data/docs/api/state.html +28 -2
  45. data/docs/api/top-level-namespace.html +11 -9
  46. data/docs/guides/debugging.html +28 -2
  47. data/docs/guides/development.html +28 -2
  48. data/docs/guides/install.html +28 -2
  49. data/docs/guides/tutorial.html +28 -2
  50. data/docs/index.html +28 -2
  51. data/docs/search_index.json +1 -1
  52. data/examples/sinatra/Gemfile +2 -0
  53. data/examples/sinatra/Gemfile.lock +32 -30
  54. data/examples/sinatra/app.rb +1 -4
  55. data/examples/sinatra/config.ru +6 -6
  56. data/examples/static/Gemfile.lock +9 -9
  57. data/lib/ovto/actions.rb +11 -0
  58. data/lib/ovto/app.rb +37 -13
  59. data/lib/ovto/component.rb +130 -67
  60. data/lib/ovto/middleware.rb +121 -0
  61. data/lib/ovto/pure_component.rb +1 -1
  62. data/lib/ovto/state.rb +13 -5
  63. data/lib/ovto/version.rb +1 -1
  64. data/lib/ovto/wired_action_set.rb +40 -0
  65. data/lib/ovto/wired_actions.rb +49 -11
  66. data/lib/ovto.rb +26 -0
  67. data/ovto.gemspec +1 -1
  68. metadata +21 -11
  69. data/docs/api/pure_component.md +0 -27
@@ -4,3 +4,5 @@ gem 'opal-sprockets'
4
4
  gem 'sinatra'
5
5
  gem 'sinatra-contrib'
6
6
  gem 'rack'
7
+ gem 'rake'
8
+ gem 'puma'
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- ovto (0.4.1)
4
+ ovto (0.6.0)
5
5
  opal (>= 0.11, < 2)
6
6
  rack (~> 2.0)
7
7
  thor (~> 0.20)
@@ -9,44 +9,44 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- ast (2.4.0)
13
- backports (3.13.0)
14
- concurrent-ruby (1.1.5)
15
- hike (1.2.3)
16
- multi_json (1.13.1)
17
- mustermann (1.0.3)
18
- opal (0.11.4)
12
+ ast (2.4.2)
13
+ concurrent-ruby (1.1.9)
14
+ multi_json (1.15.0)
15
+ mustermann (1.1.1)
16
+ ruby2_keywords (~> 0.0.1)
17
+ nio4r (2.5.8)
18
+ opal (1.2.0)
19
19
  ast (>= 2.3.0)
20
- hike (~> 1.2)
21
- parser (= 2.3.3.1)
22
- sourcemap (~> 0.1.0)
23
- opal-sprockets (0.4.3.0.11.0.3.7)
24
- opal (~> 0.11.0)
25
- sprockets (~> 3.7)
20
+ parser (~> 3.0)
21
+ opal-sprockets (1.0.2)
22
+ opal (>= 1.0, < 2.0)
23
+ sprockets (~> 4.0)
26
24
  tilt (>= 1.4)
27
- parser (2.3.3.1)
28
- ast (~> 2.2)
29
- rack (2.0.8)
30
- rack-protection (2.0.5)
25
+ parser (3.0.2.0)
26
+ ast (~> 2.4.1)
27
+ puma (5.5.2)
28
+ nio4r (~> 2.0)
29
+ rack (2.2.3)
30
+ rack-protection (2.1.0)
31
31
  rack
32
- sinatra (2.0.5)
32
+ rake (13.0.6)
33
+ ruby2_keywords (0.0.5)
34
+ sinatra (2.1.0)
33
35
  mustermann (~> 1.0)
34
- rack (~> 2.0)
35
- rack-protection (= 2.0.5)
36
+ rack (~> 2.2)
37
+ rack-protection (= 2.1.0)
36
38
  tilt (~> 2.0)
37
- sinatra-contrib (2.0.5)
38
- backports (>= 2.8.2)
39
+ sinatra-contrib (2.1.0)
39
40
  multi_json
40
41
  mustermann (~> 1.0)
41
- rack-protection (= 2.0.5)
42
- sinatra (= 2.0.5)
43
- tilt (>= 1.3, < 3)
44
- sourcemap (0.1.1)
45
- sprockets (3.7.2)
42
+ rack-protection (= 2.1.0)
43
+ sinatra (= 2.1.0)
44
+ tilt (~> 2.0)
45
+ sprockets (4.0.2)
46
46
  concurrent-ruby (~> 1.0)
47
47
  rack (> 1, < 3)
48
48
  thor (0.20.3)
49
- tilt (2.0.9)
49
+ tilt (2.0.10)
50
50
 
51
51
  PLATFORMS
52
52
  ruby
@@ -54,9 +54,11 @@ PLATFORMS
54
54
  DEPENDENCIES
55
55
  opal-sprockets
56
56
  ovto!
57
+ puma
57
58
  rack
59
+ rake
58
60
  sinatra
59
61
  sinatra-contrib
60
62
 
61
63
  BUNDLED WITH
62
- 2.0.1
64
+ 2.2.22
@@ -3,16 +3,13 @@ require 'sinatra/reloader'
3
3
  require 'opal'
4
4
 
5
5
  class SinatraApp < Sinatra::Base
6
- # Proc to generate javascript include tag (see config.ru)
7
- set :generate_javascript_include_tag, nil
8
-
9
6
  configure(:development) do
10
7
  register Sinatra::Reloader
11
8
  also_reload "#{__dir__}/**/*.rb"
12
9
  end
13
10
 
14
11
  get '/' do
15
- @js_tag = options.generate_javascript_include_tag
12
+ @js_tag = $GENERATE_JAVASCRIPT_INCLUDE_TAG.call # Defined in config.ru
16
13
  erb :index # Render views/index.erb
17
14
  end
18
15
  end
@@ -2,20 +2,20 @@ require 'opal/sprockets'
2
2
  require_relative './app.rb'
3
3
 
4
4
  Opal.use_gem 'ovto'
5
- opal = Opal::Sprockets::Server.new {|s|
5
+ opal_server = Opal::Sprockets::Server.new {|s|
6
6
  s.append_path './ovto/'
7
7
  s.main = 'app'
8
8
  }
9
9
 
10
- sprockets = opal.sprockets
10
+ sprockets = opal_server.sprockets
11
11
  prefix = '/assets'
12
12
 
13
13
  map prefix do
14
14
  run sprockets
15
15
  end
16
16
 
17
- run Sinatra.new(SinatraApp){
18
- set :generate_javascript_include_tag, proc{
19
- ::Opal::Sprockets.javascript_include_tag('app', sprockets: sprockets, prefix: prefix, debug: true)
20
- }
17
+ $GENERATE_JAVASCRIPT_INCLUDE_TAG = ->{
18
+ ::Opal::Sprockets.javascript_include_tag('app', sprockets: sprockets, prefix: prefix, debug: true)
21
19
  }
20
+
21
+ run SinatraApp
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- ovto (0.4.1)
4
+ ovto (0.6.0)
5
5
  opal (>= 0.11, < 2)
6
6
  rack (~> 2.0)
7
7
  thor (~> 0.20)
@@ -9,15 +9,15 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- ast (2.4.0)
12
+ ast (2.4.2)
13
13
  ifchanged (1.0.1)
14
- opal (1.0.2)
14
+ opal (1.2.0)
15
15
  ast (>= 2.3.0)
16
- parser (~> 2.6)
17
- parser (2.6.5.0)
18
- ast (~> 2.4.0)
19
- rack (2.0.8)
20
- rake (12.3.2)
16
+ parser (~> 3.0)
17
+ parser (3.0.2.0)
18
+ ast (~> 2.4.1)
19
+ rack (2.2.3)
20
+ rake (13.0.6)
21
21
  thor (0.20.3)
22
22
 
23
23
  PLATFORMS
@@ -29,4 +29,4 @@ DEPENDENCIES
29
29
  rake
30
30
 
31
31
  BUNDLED WITH
32
- 2.0.1
32
+ 2.2.22
data/lib/ovto/actions.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  module Ovto
2
2
  # Base class for ovto actions.
3
3
  class Actions
4
+ # WiredActions must be set after initialization
5
+ # (this cannot be an argument of #initialize because Actions and
6
+ # WiredActions have references to each other)
4
7
  attr_writer :wired_actions
5
8
 
6
9
  def actions
@@ -10,5 +13,13 @@ module Ovto
10
13
  def state
11
14
  @wired_actions._app.state
12
15
  end
16
+
17
+ def middleware_name
18
+ WiredActionSet::I_AM_APP_NOT_A_MIDDLEWARE
19
+ end
20
+
21
+ def middleware_path
22
+ []
23
+ end
13
24
  end
14
25
  end
data/lib/ovto/app.rb CHANGED
@@ -1,18 +1,37 @@
1
1
  module Ovto
2
2
  class App
3
+ # List of installed middleware classes
4
+ def self.middlewares
5
+ @middlewares ||= []
6
+ end
7
+
3
8
  # Create an App and start it
4
9
  def self.run(*args)
5
10
  new.run(*args)
6
11
  end
7
12
 
13
+ # Install a middleware
14
+ def self.use(middleware_class)
15
+ self.middlewares.push(middleware_class)
16
+ end
17
+
8
18
  def initialize
9
- @state = self.class.const_get('State').new
10
- @wired_actions = nil
19
+ app_state_class = self.class.const_get('State')
20
+ # Inject middleware states
21
+ app_state_class.item :_middlewares, default_proc: ->{
22
+ Ovto::Middleware.create_middleware_states_class(self.class.middlewares).new
23
+ }
24
+ @state = app_state_class.new
25
+ @wired_action_set = nil
26
+ @main_component = nil
11
27
  end
12
28
  attr_reader :state
13
29
 
30
+ # An instance of YourApp::MainComponent (mainly for testing)
31
+ attr_reader :main_component
32
+
14
33
  def actions
15
- @wired_actions
34
+ @wired_action_set.app_wired_actions
16
35
  end
17
36
 
18
37
  # Internal use only
@@ -36,9 +55,9 @@ module Ovto
36
55
  def _run(id: nil)
37
56
  runtime = Ovto::Runtime.new(self)
38
57
  actions = self.class.const_get('Actions').new
39
- @wired_actions = WiredActions.new(actions, self, runtime)
40
- actions.wired_actions = @wired_actions
41
- view = create_view
58
+ @wired_action_set = WiredActionSet.new(self, actions, [], self.class.middlewares, runtime)
59
+ actions.wired_actions = @wired_action_set.app_wired_actions
60
+ @main_component = create_view(@wired_action_set)
42
61
  if id
43
62
  %x{
44
63
  document.addEventListener('DOMContentLoaded', function(){
@@ -46,16 +65,16 @@ module Ovto
46
65
  if (!container) {
47
66
  throw "Ovto::App#run: tag with id='" + id + "' was not found";
48
67
  }
49
- #{start_application(runtime, view, `container`)}
68
+ #{start_application(runtime, `container`)}
50
69
  });
51
70
  }
52
71
  else
53
- start_application(runtime, view, nil)
72
+ start_application(runtime, nil)
54
73
  end
55
74
  end
56
75
 
57
76
  # Instantiate MyApp::MainComponent
58
- def create_view
77
+ def create_view(wired_action_set)
59
78
  begin
60
79
  main_component_class = self.class.const_get('MainComponent')
61
80
  rescue NameError => orig_ex
@@ -68,12 +87,17 @@ module Ovto
68
87
  "#{self.class}::View to #{self.class}::MainComponent"
69
88
  end
70
89
  end
71
- return main_component_class.new(@wired_actions)
90
+ return main_component_class.new(wired_action_set)
72
91
  end
73
92
 
74
- def start_application(runtime, view, container)
75
- runtime.run(view, container)
76
- setup
93
+ def start_application(runtime, container)
94
+ Ovto.log_error {
95
+ runtime.run(@main_component, container)
96
+ setup
97
+ self.class.middlewares.each do |m|
98
+ m._run_setup(@wired_action_set[m.name])
99
+ end
100
+ }
77
101
  end
78
102
  end
79
103
  end
@@ -13,10 +13,16 @@ module Ovto
13
13
  ret
14
14
  end
15
15
 
16
- def initialize(wired_actions)
17
- @wired_actions = wired_actions
16
+ # (internal) Defined for convenience
17
+ def self.middleware_name
18
+ WiredActionSet::I_AM_APP_NOT_A_MIDDLEWARE
19
+ end
20
+
21
+ def initialize(wired_action_set, middleware_path=[])
22
+ @wired_action_set = wired_action_set || WiredActionSet.dummy
23
+ @middleware_path = middleware_path
18
24
  # Initialize here for the unit tests
19
- @vdom_tree = []
25
+ @vdom_stack = [[]]
20
26
  @components = []
21
27
  @components_index = 0
22
28
  end
@@ -26,7 +32,7 @@ module Ovto
26
32
  end
27
33
 
28
34
  def state
29
- @wired_actions._app.state
35
+ @wired_action_set.app.state
30
36
  end
31
37
 
32
38
  private
@@ -37,35 +43,35 @@ module Ovto
37
43
  do_render({}, state)
38
44
  end
39
45
 
40
- def do_render(args, state)
46
+ # Call #render to generate VDom
47
+ def do_render(args, state, &block)
41
48
  Ovto.debug_trace_log("rendering #{self}")
42
- @vdom_tree = []
49
+ @vdom_stack = [[]]
43
50
  @components_index = 0
44
51
  @done_render = false
45
52
  @current_state = state
46
- parameters = method(:render).parameters
47
- if `!parameters` || parameters.nil? || accepts_state?(parameters)
48
- # We can pass `state:` safely
49
- args_with_state = {state: @current_state}.merge(args)
50
- return render(args_with_state)
53
+ rendered = Ovto.log_error {
54
+ Ovto.send_args_with_state(self, :render, args, state, &block)
55
+ }
56
+ case rendered
57
+ when String
58
+ return rendered
59
+ when Array
60
+ if rendered.length > 1
61
+ raise MoreThanOneNode
62
+ end
63
+ raise "rendered is empty" if rendered.length == 0
64
+ return rendered[0]
51
65
  else
52
- # Check it is empty (see https://github.com/opal/opal/issues/1872)
53
- return args.empty? ? render() : render(**args)
54
- end
55
- end
56
-
57
- # Return true if the method accepts `state:` keyword
58
- def accepts_state?(parameters)
59
- parameters.each do |item|
60
- return true if item == [:key, :state] ||
61
- item == [:keyreq, :state] ||
62
- item[0] == :keyrest
66
+ console.error("#render returned unknown value", rendered)
67
+ raise "#render returned unknown value"
63
68
  end
64
- return false
65
69
  end
66
70
 
67
71
  def actions
68
- @wired_actions
72
+ return @middleware_path.inject(@wired_action_set){|wa_set, middleware_name|
73
+ wa_set[middleware_name]
74
+ }[WiredActionSet::THE_MIDDLEWARE_ITSELF]
69
75
  end
70
76
 
71
77
  # o 'div', 'Hello.'
@@ -78,31 +84,39 @@ module Ovto
78
84
  # o 'h1', 'Hello.'
79
85
  # end
80
86
  # o 'div', `{nodeName: ....}` # Inject VDom spec directly
87
+ # o SubComponentClass
88
+ # o SubComponentClass do ... end # Ovto passes the block to SubComponent#render
81
89
  def o(_tag_name, arg1=nil, arg2=nil, &block)
82
- if native?(arg1)
90
+ if native?(arg1) # Embed VDom directly
83
91
  attributes = {}
84
92
  content = arg1
85
- elsif arg1.is_a?(Hash)
93
+ elsif arg1.is_a?(Hash) # Has attributes
86
94
  attributes = arg1
87
95
  content = arg2
88
- elsif arg2 == nil
96
+ elsif arg2 == nil # Has content instead of attributes, or both are nil
89
97
  attributes = {}
90
98
  content = arg1
91
99
  else
92
100
  raise ArgumentError
93
101
  end
94
102
 
95
- children = render_children(content, block)
96
103
  case _tag_name
97
104
  when Class
98
- result = render_component(_tag_name, attributes, children)
105
+ if content
106
+ raise ArgumentError, "use a block to pass content to sub component"
107
+ end
108
+ result = render_component(_tag_name, attributes, &block)
99
109
  when 'text'
100
110
  unless attributes.empty?
101
111
  raise ArgumentError, "text cannot take attributes"
102
112
  end
103
113
  result = content
104
114
  when String
115
+ children = render_children(content, block)
105
116
  tag_name, base_attributes = *extract_attrs(_tag_name)
117
+ if tag_name == "textarea" && (content || block)
118
+ raise ArgumentError, "Use `value:` to specify content of a textarea"
119
+ end
106
120
  # Ignore nil/false
107
121
  more_attributes = attributes.reject{|k, v| !v}
108
122
  result = render_tag(tag_name, merge_attrs(base_attributes, more_attributes), children)
@@ -110,16 +124,8 @@ module Ovto
110
124
  raise TypeError, "tag_name must be a String or Component but got "+
111
125
  Ovto.inspect(tag_name)
112
126
  end
113
- if @vdom_tree.empty?
114
- if @done_render
115
- raise MoreThanOneNode, "#{self.class}#render must generate a single DOM node. Please wrap the tags with a 'div' or something."
116
- end
117
- @done_render = true
118
- return result
119
- else
120
- @vdom_tree.last.push(result)
121
- return @vdom_tree.last
122
- end
127
+ @vdom_stack.last.push(result)
128
+ return @vdom_stack.last
123
129
  end
124
130
 
125
131
  def extract_attrs(tag_name)
@@ -164,37 +170,52 @@ module Ovto
164
170
  [content.to_s]
165
171
  end
166
172
  when block
167
- @vdom_tree.push []
168
- block_value = block.call
169
- results = @vdom_tree.pop
170
- if results.length > 0 # 'o' was called at least once
171
- results
172
- elsif native?(block_value)
173
- # Inject VDom tree written in JS object
174
- # eg. Embed markdown
175
- [block_value]
176
- elsif block_value.is_a?(String)
177
- # When 'o' is never called in the child block, use the last value
178
- # eg.
179
- # o 'span' do
180
- # 'Hello' #=> This will be the content of the span tag
181
- # end
182
- [block_value]
183
- else
184
- # o 'div' do
185
- # # When items is `[]`, 'o' is never called and `block_value` will be `[]`
186
- # items.each{ o 'div', '...' }
187
- # end
188
- []
189
- end
173
+ render_block(block)
190
174
  else
191
175
  []
192
176
  end
193
177
  end
194
178
 
195
- def render_component(comp_class, args, children)
179
+ def render_block(block)
180
+ @vdom_stack.push []
181
+ orig_depth = @vdom_stack.length
182
+ block_value = block.call
183
+ @vdom_stack = @vdom_stack.first(orig_depth)
184
+ results = @vdom_stack.pop
185
+
186
+ if results.length > 0 # 'o' was called at least once
187
+ results
188
+ elsif native?(block_value)
189
+ # Inject VDom tree written in JS object
190
+ # eg. Embed markdown
191
+ [block_value]
192
+ elsif block_value.is_a?(String)
193
+ # When 'o' is never called in the child block, use the last value
194
+ # eg.
195
+ # o 'span' do
196
+ # 'Hello' #=> This will be the content of the span tag
197
+ # end
198
+ [block_value]
199
+ elsif block_value.is_a?(Array)
200
+ # Case 1
201
+ # o "div", &block
202
+ # Case 2
203
+ # items = []
204
+ # o 'div' do items.each{ o ... } end # == o 'div' do [] end
205
+ block_value
206
+ else
207
+ console.error("Invalid block_value:", Ovto.inspect(block_value))
208
+ raise "Invalid block value"
209
+ end
210
+ end
211
+
212
+ # Instantiate component and call its #render to get VDom
213
+ def render_component(comp_class, args, &block)
196
214
  comp = new_component(comp_class)
197
- return comp.do_render(args, @current_state){ children }
215
+ orig_stack, @vdom_stack = @vdom_stack, [[]]
216
+ ret = comp.do_render(args, @current_state, &block)
217
+ @vdom_stack = orig_stack
218
+ ret
198
219
  end
199
220
 
200
221
  def new_component(comp_class)
@@ -204,14 +225,56 @@ module Ovto
204
225
  return comp
205
226
  end
206
227
 
207
- comp = @components[@components_index] = comp_class.new(@wired_actions)
228
+ middleware_path = new_middleware_path(comp_class)
229
+ comp = @components[@components_index] = comp_class.new(@wired_action_set, middleware_path)
208
230
  @components_index += 1
209
231
  comp
210
232
  end
211
233
 
234
+ # Make new middleware_path by adding comp_class
235
+ def new_middleware_path(comp_class)
236
+ mw_name = comp_class.middleware_name
237
+ if (idx = @middleware_path.index(mw_name))
238
+ # eg. suppose OvtoIde uses OvtoWindow
239
+ # class CompI < OvtoIde::Component
240
+ # def render
241
+ # o Window do
242
+ # o AnotherComponentOfOvtoIde
243
+ # end
244
+ # end
245
+ # end
246
+ # class Window < OvtoWindow::Component
247
+ # def render(&block)
248
+ # o ".window", &block
249
+ # end
250
+ # end
251
+ # Rendering order:
252
+ # 1. CompI (ovto_ide)
253
+ # 2. Window (ovto_window)
254
+ # 3. AnotherComponentOfOvtoIde (ovto_ide again)
255
+ @middleware_path[0..idx]
256
+ else
257
+ if comp_class.middleware_name == WiredActionSet::I_AM_APP_NOT_A_MIDDLEWARE
258
+ @middleware_path
259
+ else
260
+ @middleware_path + [comp_class.middleware_name]
261
+ end
262
+ end
263
+ # TODO: it would be nice if we could raise an error when comp_class
264
+ # is invalid middleware (i.e. not use'd)
265
+ end
266
+
212
267
  def render_tag(tag_name, attributes, children)
213
- js_attributes = Component.hash_to_js_obj(attributes || {})
214
- if (style = attributes['style'])
268
+ attributes_ = attributes.map{|k, v|
269
+ if k.start_with?("on")
270
+ # Inject log_error to event handlers
271
+ [k, ->(e){ Ovto.log_error{ v.call(e) }}]
272
+ else
273
+ [k, v]
274
+ end
275
+ }.to_h
276
+ js_attributes = Component.hash_to_js_obj(attributes_ || {})
277
+ if (style = attributes_['style'])
215
278
  `js_attributes.style = #{Component.hash_to_js_obj(style)}`
216
279
  end
217
280
  children ||= `null`