ovto 0.2.0

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 (105) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.gitmodules +3 -0
  4. data/CHANGELOG.md +22 -0
  5. data/Gemfile +7 -0
  6. data/Gemfile.lock +57 -0
  7. data/LICENSE.txt +44 -0
  8. data/README.md +84 -0
  9. data/Rakefile +41 -0
  10. data/book/README.md +1 -0
  11. data/book/SUMMARY.md +13 -0
  12. data/book/api/actions.md +77 -0
  13. data/book/api/app.md +86 -0
  14. data/book/api/component.md +175 -0
  15. data/book/api/fetch.md +42 -0
  16. data/book/api/state.md +97 -0
  17. data/book/guides/debugging.md +23 -0
  18. data/book/guides/development.md +11 -0
  19. data/book/guides/tutorial.md +288 -0
  20. data/book/screenshot.png +0 -0
  21. data/docs/api/Ovto/Actions.html +135 -0
  22. data/docs/api/Ovto/App.html +531 -0
  23. data/docs/api/Ovto/Component/MoreThanOneNode.html +135 -0
  24. data/docs/api/Ovto/Component.html +350 -0
  25. data/docs/api/Ovto/Runtime.html +315 -0
  26. data/docs/api/Ovto/State/MissingValue.html +135 -0
  27. data/docs/api/Ovto/State/UnknownKey.html +135 -0
  28. data/docs/api/Ovto/State.html +699 -0
  29. data/docs/api/Ovto/WiredActions.html +343 -0
  30. data/docs/api/Ovto.html +319 -0
  31. data/docs/api/_index.html +229 -0
  32. data/docs/api/actions.html +398 -0
  33. data/docs/api/app.html +411 -0
  34. data/docs/api/class_list.html +51 -0
  35. data/docs/api/component.html +469 -0
  36. data/docs/api/css/common.css +1 -0
  37. data/docs/api/css/full_list.css +58 -0
  38. data/docs/api/css/style.css +499 -0
  39. data/docs/api/file.README.html +162 -0
  40. data/docs/api/file_list.html +56 -0
  41. data/docs/api/frames.html +17 -0
  42. data/docs/api/index.html +162 -0
  43. data/docs/api/js/app.js +248 -0
  44. data/docs/api/js/full_list.js +216 -0
  45. data/docs/api/js/jquery.js +4 -0
  46. data/docs/api/method_list.html +243 -0
  47. data/docs/api/state.html +430 -0
  48. data/docs/api/top-level-namespace.html +110 -0
  49. data/docs/gitbook/fonts/fontawesome/FontAwesome.otf +0 -0
  50. data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.eot +0 -0
  51. data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.svg +685 -0
  52. data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.ttf +0 -0
  53. data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff +0 -0
  54. data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff2 +0 -0
  55. data/docs/gitbook/gitbook-plugin-fontsettings/fontsettings.js +240 -0
  56. data/docs/gitbook/gitbook-plugin-fontsettings/website.css +291 -0
  57. data/docs/gitbook/gitbook-plugin-highlight/ebook.css +135 -0
  58. data/docs/gitbook/gitbook-plugin-highlight/website.css +434 -0
  59. data/docs/gitbook/gitbook-plugin-lunr/lunr.min.js +7 -0
  60. data/docs/gitbook/gitbook-plugin-lunr/search-lunr.js +59 -0
  61. data/docs/gitbook/gitbook-plugin-search/lunr.min.js +7 -0
  62. data/docs/gitbook/gitbook-plugin-search/search-engine.js +50 -0
  63. data/docs/gitbook/gitbook-plugin-search/search.css +35 -0
  64. data/docs/gitbook/gitbook-plugin-search/search.js +213 -0
  65. data/docs/gitbook/gitbook-plugin-sharing/buttons.js +90 -0
  66. data/docs/gitbook/gitbook.js +4 -0
  67. data/docs/gitbook/images/apple-touch-icon-precomposed-152.png +0 -0
  68. data/docs/gitbook/images/favicon.ico +0 -0
  69. data/docs/gitbook/style.css +9 -0
  70. data/docs/gitbook/theme.js +4 -0
  71. data/docs/guides/debugging.html +355 -0
  72. data/docs/guides/development.html +361 -0
  73. data/docs/guides/tutorial.html +571 -0
  74. data/docs/index.html +422 -0
  75. data/docs/screenshot.png +0 -0
  76. data/docs/search_index.json +1 -0
  77. data/example/sinatra/Gemfile +6 -0
  78. data/example/sinatra/Gemfile.lock +59 -0
  79. data/example/sinatra/README.md +21 -0
  80. data/example/sinatra/app.rb +18 -0
  81. data/example/sinatra/config.ru +30 -0
  82. data/example/sinatra/ovto/app.rb +171 -0
  83. data/example/sinatra/public/style.css +4 -0
  84. data/example/sinatra/public/todomvc-app-css_index.css +376 -0
  85. data/example/sinatra/public/todomvc-common_base.css +141 -0
  86. data/example/sinatra/views/index.erb +21 -0
  87. data/example/static/Gemfile +3 -0
  88. data/example/static/Gemfile.lock +30 -0
  89. data/example/static/README.md +10 -0
  90. data/example/static/Rakefile +4 -0
  91. data/example/static/app.js +24808 -0
  92. data/example/static/app.rb +43 -0
  93. data/example/static/index.html +11 -0
  94. data/lib/ovto/actions.rb +10 -0
  95. data/lib/ovto/app.rb +58 -0
  96. data/lib/ovto/component.rb +191 -0
  97. data/lib/ovto/fetch.rb +53 -0
  98. data/lib/ovto/runtime.rb +388 -0
  99. data/lib/ovto/state.rb +69 -0
  100. data/lib/ovto/version.rb +3 -0
  101. data/lib/ovto/wired_actions.rb +33 -0
  102. data/lib/ovto.rb +50 -0
  103. data/ovto.gemspec +22 -0
  104. data/screenshot.png +0 -0
  105. metadata +161 -0
@@ -0,0 +1,43 @@
1
+ require 'ovto'
2
+
3
+ class MyApp < Ovto::App
4
+ class State < Ovto::State
5
+ item :celsius, default: 0
6
+
7
+ def fahrenheit
8
+ (celsius * 9 / 5.0) + 32
9
+ end
10
+ end
11
+
12
+ class Actions < Ovto::Actions
13
+ def set_celsius(state:, value:)
14
+ return {celsius: value}
15
+ end
16
+
17
+ def set_fahrenheit(state:, value:)
18
+ new_celsius = (value - 32) * 5 / 9.0
19
+ return {celsius: new_celsius}
20
+ end
21
+ end
22
+
23
+ class View < Ovto::Component
24
+ def render(state:)
25
+ o 'div' do
26
+ o 'span', 'Celcius:'
27
+ o 'input', {
28
+ type: 'text',
29
+ onchange: ->(e){ actions.set_celsius(value: e.target.value.to_i) },
30
+ value: state.celsius
31
+ }
32
+ o 'span', 'Fahrenheit:'
33
+ o 'input', {
34
+ type: 'text',
35
+ onchange: ->(e){ actions.set_fahrenheit(value: e.target.value.to_i) },
36
+ value: state.fahrenheit
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ MyApp.run(id: 'ovto-view')
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <script type='text/javascript' src='app.js'></script>
6
+ </head>
7
+ <body>
8
+ <div id='ovto-view'></div>
9
+ <div id='ovto-debug'></div>
10
+ </body>
11
+ </html>
@@ -0,0 +1,10 @@
1
+ module Ovto
2
+ # Base class for ovto actions.
3
+ class Actions
4
+ attr_writer :wired_actions
5
+
6
+ def actions
7
+ @wired_actions
8
+ end
9
+ end
10
+ end
data/lib/ovto/app.rb ADDED
@@ -0,0 +1,58 @@
1
+ module Ovto
2
+ class App
3
+ # Create an App and start it
4
+ def self.run(*args)
5
+ new.run(*args)
6
+ end
7
+
8
+ def initialize
9
+ @state = self.class.const_get('State').new
10
+ @wired_actions = nil
11
+ end
12
+ attr_reader :state
13
+
14
+ def actions
15
+ @wired_actions
16
+ end
17
+
18
+ # Internal use only
19
+ def _set_state(new_state)
20
+ @state = new_state
21
+ end
22
+
23
+ # Start this app
24
+ def run(*args)
25
+ Ovto.log_error{ _run(*args) }
26
+ end
27
+
28
+ # Called when this app is started
29
+ def setup
30
+ # override this if needed
31
+ end
32
+
33
+ private
34
+
35
+ # Setup runtime and wired_actions
36
+ def _run(id: nil)
37
+ runtime = Ovto::Runtime.new(self)
38
+ actions = self.class.const_get('Actions').new
39
+ @wired_actions = WiredActions.new(actions, self, runtime)
40
+ actions.wired_actions = @wired_actions
41
+ view = self.class.const_get('View').new(@wired_actions)
42
+ if id
43
+ %x{
44
+ document.addEventListener('DOMContentLoaded', function(){
45
+ var container = document.getElementById(id);
46
+ if (!container) {
47
+ throw "Ovto::App#run: tag with id='" + id + "' was not found";
48
+ }
49
+ #{runtime.run(view, `container`)}
50
+ });
51
+ }
52
+ else
53
+ runtime.run(view, nil)
54
+ end
55
+ setup
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,191 @@
1
+ module Ovto
2
+ class Component
3
+ # `render` tried to yield multiple nodes
4
+ class MoreThanOneNode < StandardError; end
5
+
6
+ def self.hash_to_js_obj(hash)
7
+ ret = `{}`
8
+ hash.each do |k, v|
9
+ `ret[k] = v`
10
+ end
11
+ ret
12
+ end
13
+
14
+ def initialize(wired_actions)
15
+ @wired_actions = wired_actions
16
+ # Initialize here for the unit tests
17
+ @vdom_tree = []
18
+ end
19
+
20
+ def render
21
+ ''
22
+ end
23
+
24
+ private
25
+
26
+ # Render entire MyApp::View
27
+ # Called from runtime.rb
28
+ def render_view(state)
29
+ do_render(state: state)
30
+ end
31
+
32
+ def do_render(**args)
33
+ @vdom_tree = []
34
+ @done_render = false
35
+ @current_state = args[:state]
36
+ parameters = method(:render).parameters
37
+ if `!parameters` || parameters.nil? || accepts_state?(parameters)
38
+ # We can pass `state:` safely
39
+ return render(**args)
40
+ else
41
+ # Remove `state:` keyword
42
+ args_wo_state = args.reject{|k, v| k == :state}
43
+ # Check it is empty (see https://github.com/opal/opal/issues/1872)
44
+ return args_wo_state.empty? ? render() : render(**args_wo_state)
45
+ end
46
+ end
47
+
48
+ # Return true if the method accepts `state:` keyword
49
+ def accepts_state?(parameters)
50
+ parameters.each do |item|
51
+ return true if item == [:key, :state] ||
52
+ item == [:keyreq, :state] ||
53
+ item[0] == :keyrest
54
+ end
55
+ return false
56
+ end
57
+
58
+ def actions
59
+ @wired_actions
60
+ end
61
+
62
+ # o 'div', 'Hello.'
63
+ # o 'div', class: 'main', 'Hello.'
64
+ # o 'div', style: {color: 'red'}, 'Hello.'
65
+ # o 'div.main'
66
+ # o 'div#main'
67
+ # o 'div' do 'Hello.' end
68
+ # o 'div' do
69
+ # o 'h1', 'Hello.'
70
+ # end
71
+ def o(_tag_name, arg1=nil, arg2=nil, &block)
72
+ if arg1.is_a?(Hash)
73
+ attributes = arg1
74
+ content = arg2
75
+ elsif arg2 == nil
76
+ attributes = {}
77
+ content = arg1
78
+ else
79
+ raise ArgumentError
80
+ end
81
+
82
+ children = render_children(content, block)
83
+ case _tag_name
84
+ when Class
85
+ result = render_component(_tag_name, attributes, children)
86
+ when 'text'
87
+ unless attributes.empty?
88
+ raise ArgumentError, "text cannot take attributes"
89
+ end
90
+ result = content
91
+ when String
92
+ tag_name, base_attributes = *extract_attrs(_tag_name)
93
+ # Ignore nil/false
94
+ more_attributes = attributes.reject{|k, v| !v}
95
+ result = render_tag(tag_name, merge_attrs(base_attributes, more_attributes), children)
96
+ else
97
+ raise TypeError, "tag_name must be a String or Component but got "+
98
+ Ovto.inspect(tag_name)
99
+ end
100
+ if @vdom_tree.empty?
101
+ if @done_render
102
+ raise MoreThanOneNode, "#{self.class}#render must generate a single DOM node. Please wrap the tags with a 'div' or something."
103
+ end
104
+ @done_render = true
105
+ return result
106
+ else
107
+ @vdom_tree.last.push(result)
108
+ return @vdom_tree.last
109
+ end
110
+ end
111
+
112
+ def extract_attrs(tag_name)
113
+ case tag_name
114
+ when /^([^.#]*)\.([-_\w]+)(\#([-_\w]+))?/ # a.b#c
115
+ tag_name, class_name, id = ($1.empty? ? 'div' : $1), $2, $4
116
+ when /^([^.#]*)\#([-_\w]+)(\.([-_\w]+))?/ # a#b.c
117
+ tag_name, class_name, id = ($1.empty? ? 'div' : $1), $4, $2
118
+ else
119
+ class_name = id = nil
120
+ end
121
+ attributes = {}
122
+ attributes[:class] = class_name if class_name
123
+ attributes[:id] = id if id
124
+ return tag_name, attributes
125
+ end
126
+
127
+ # Merge attributes into base_attributes, with special care for `class:`
128
+ def merge_attrs(base_attributes, attributes)
129
+ base_class = base_attributes[:class]
130
+ more_class = attributes[:class]
131
+ merged_class = if base_class && more_class
132
+ base_class + " " + more_class
133
+ else
134
+ base_class || more_class
135
+ end
136
+ if merged_class
137
+ base_attributes.merge(attributes).merge(:class => merged_class)
138
+ else
139
+ base_attributes.merge(attributes)
140
+ end
141
+ end
142
+
143
+ def render_children(content=nil, block=nil)
144
+ case
145
+ when content && block
146
+ raise ArgumentError, "o cannot take both content and block"
147
+ when content
148
+ [content.to_s]
149
+ when block
150
+ @vdom_tree.push []
151
+ block_value = block.call
152
+ results = @vdom_tree.pop
153
+ if results.length > 0
154
+ results
155
+ else
156
+ # When 'o' is never called in the child block, use the last value
157
+ # eg.
158
+ # o 'span' do
159
+ # 'Hello' #=> This will be the content of the span tag
160
+ # end
161
+ [block_value]
162
+ end
163
+ else
164
+ []
165
+ end
166
+ end
167
+
168
+ def render_component(comp_class, args, children)
169
+ comp = comp_class.new(@wired_actions)
170
+ render_args = {state: @current_state}.merge(args)
171
+ return comp.do_render(**render_args){ children }
172
+ end
173
+
174
+ def render_tag(tag_name, attributes, children)
175
+ js_attributes = Component.hash_to_js_obj(attributes || {})
176
+ if (style = attributes['style'])
177
+ `js_attributes.style = #{Component.hash_to_js_obj(style)}`
178
+ end
179
+ children ||= `null`
180
+ ret = %x{
181
+ {
182
+ nodeName: tag_name,
183
+ attributes: js_attributes,
184
+ children: children,
185
+ key: js_attributes.key
186
+ }
187
+ }
188
+ ret
189
+ end
190
+ end
191
+ end
data/lib/ovto/fetch.rb ADDED
@@ -0,0 +1,53 @@
1
+ require 'promise'
2
+ require 'native'
3
+ require 'json'
4
+
5
+ module Ovto
6
+ # Wrapper for the fetch API
7
+ # The server must respond a json text.
8
+ #
9
+ # Example:
10
+ # Ovto.fetch('/api/new_task', 'POST', {title: "do something"}){|json_data|
11
+ # p json_data
12
+ # }.fail{|e| # Network error, 404 Not Found, JSON parse error, etc.
13
+ # p e
14
+ # }
15
+ def self.fetch(url, method='GET', data=nil)
16
+ init = `{
17
+ method: #{method},
18
+ credentials: 'same-origin' // Send cookies to the server (eg. for CookieStore of Rails)
19
+ }`
20
+ if method != 'GET'
21
+ %x{
22
+ var headers = {'Content-Type': 'application/json'};
23
+ var metaTag = document.querySelector('meta[name=csrf-token]');
24
+ if (metaTag) headers['X-CSRF-Token'] = metaTag.content;
25
+
26
+ init['headers'] = headers;
27
+ init['body'] = #{data.to_json};
28
+ }
29
+ end
30
+ return _do_fetch(url, init)
31
+ end
32
+
33
+ # Create an Opal Promise to call fetch API
34
+ def self._do_fetch(url, init)
35
+ promise = Promise.new
36
+ text = error = nil
37
+ %x{
38
+ fetch(url, init).then(response => {
39
+ if (response.ok) {
40
+ return response.text();
41
+ }
42
+ else {
43
+ throw response;
44
+ }
45
+ }).then(text =>
46
+ #{promise.resolve(JSON.parse(text))}
47
+ ).catch(error =>
48
+ #{promise.reject(error)}
49
+ );
50
+ }
51
+ return promise
52
+ end
53
+ end