ovto 0.2.0

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