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,388 @@
1
+ # vim: set ft=javascript:
2
+ require 'native'
3
+ module Ovto
4
+ class Runtime
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def run(view, container)
10
+ getState = ->{ @app.state }
11
+ @scheduleRender = `Ovto.run(getState, view, container)`
12
+ end
13
+
14
+ def scheduleRender
15
+ @scheduleRender.call
16
+ end
17
+ end
18
+ end
19
+
20
+ # Core part
21
+ # Copied from https://github.com/hyperapp/hyperapp/blob/6c4f4fb927b0ebba69cb6397ee8c1b69a9e81e18/src/index.js (see LICENSE.txt) and added some modification
22
+ # TODO: should we use https://github.com/jorgebucaran/ultradom instead?
23
+ %x{
24
+ var Ovto = {};
25
+ Ovto.run = function(getState, view, container) {
26
+ var map = [].map
27
+ var rootElement = (container && container.children[0]) || null
28
+ var oldNode = rootElement && recycleElement(rootElement)
29
+ var lifecycle = []
30
+ var skipRender
31
+ var isRecycling = true
32
+
33
+ scheduleRender()
34
+
35
+ return scheduleRender
36
+
37
+ function recycleElement(element) {
38
+ return {
39
+ nodeName: element.nodeName.toLowerCase(),
40
+ attributes: {},
41
+ children: map.call(element.childNodes, function(element) {
42
+ return element.nodeType === 3 // Node.TEXT_NODE
43
+ ? element.nodeValue
44
+ : recycleElement(element)
45
+ })
46
+ }
47
+ }
48
+
49
+ function resolveNode(node) {
50
+ if (node === Opal.nil || node == null) {
51
+ return "";
52
+ }
53
+ else if (node.$$id) { // is a Opal obj
54
+ return node.$render_view(getState());
55
+ }
56
+ else {
57
+ return node;
58
+ }
59
+ }
60
+
61
+ function render() {
62
+ skipRender = !skipRender
63
+
64
+ var node = resolveNode(view)
65
+
66
+ if (container && !skipRender) {
67
+ rootElement = patch(container, rootElement, oldNode, (oldNode = node))
68
+ }
69
+
70
+ isRecycling = false
71
+
72
+ while (lifecycle.length) lifecycle.pop()()
73
+ }
74
+
75
+ function scheduleRender() {
76
+ if (!skipRender) {
77
+ skipRender = true
78
+ setTimeout(render)
79
+ }
80
+ }
81
+
82
+ function clone(target, source) {
83
+ var out = {}
84
+
85
+ for (var i in target) out[i] = target[i]
86
+ for (var i in source) out[i] = source[i]
87
+
88
+ return out
89
+ }
90
+
91
+ // function setPartialState(path, value, source) {
92
+ // var target = {}
93
+ // if (path.length) {
94
+ // target[path[0]] =
95
+ // path.length > 1
96
+ // ? setPartialState(path.slice(1), value, source[path[0]])
97
+ // : value
98
+ // return clone(source, target)
99
+ // }
100
+ // return value
101
+ // }
102
+ //
103
+ // function getPartialState(path, source) {
104
+ // var i = 0
105
+ // while (i < path.length) {
106
+ // source = source[path[i++]]
107
+ // }
108
+ // return source
109
+ // }
110
+ //
111
+ // function wireStateToActions(path, state, actions) {
112
+ // for (var key in actions) {
113
+ // typeof actions[key] === "function"
114
+ // ? (function(key, action) {
115
+ // actions[key] = function(data) {
116
+ // var result = action(data)
117
+ //
118
+ // if (typeof result === "function") {
119
+ // result = result(getPartialState(path, getState()), actions)
120
+ // }
121
+ //
122
+ // if (
123
+ // result &&
124
+ // result !== (state = getPartialState(path, getState())) &&
125
+ // !result.then // !isPromise
126
+ // ) {
127
+ // globalState = setPartialState(
128
+ // path,
129
+ // clone(state, result),
130
+ // getState()
131
+ // )
132
+ // scheduleRender(globalState)
133
+ // }
134
+ //
135
+ // return result
136
+ // }
137
+ // })(key, actions[key])
138
+ // : wireStateToActions(
139
+ // path.concat(key),
140
+ // (state[key] = clone(state[key])),
141
+ // (actions[key] = clone(actions[key]))
142
+ // )
143
+ // }
144
+ //
145
+ // return actions
146
+ // }
147
+
148
+ function getKey(node) {
149
+ return node ? node.key : null
150
+ }
151
+
152
+ function eventListener(event) {
153
+ var ovto_ev = #{Native(`event`)}
154
+ return event.currentTarget.events[event.type](ovto_ev)
155
+ }
156
+
157
+ function updateAttribute(element, name, value, oldValue, isSvg) {
158
+ if (name === "key") {
159
+ } else if (name === "style") {
160
+ for (var i in clone(oldValue, value)) {
161
+ var style = value == null || value[i] == null ? "" : value[i]
162
+ if (i[0] === "-") {
163
+ element[name].setProperty(i, style)
164
+ } else {
165
+ element[name][i] = style
166
+ }
167
+ }
168
+ } else {
169
+ if (name[0] === "o" && name[1] === "n") {
170
+ name = name.slice(2)
171
+
172
+ if (element.events) {
173
+ if (!oldValue) oldValue = element.events[name]
174
+ } else {
175
+ element.events = {}
176
+ }
177
+
178
+ element.events[name] = value
179
+
180
+ if (value) {
181
+ if (!oldValue) {
182
+ element.addEventListener(name, eventListener)
183
+ }
184
+ } else {
185
+ element.removeEventListener(name, eventListener)
186
+ }
187
+ } else if (name in element && name !== "list" && !isSvg) {
188
+ element[name] = value == null ? "" : value
189
+ } else if (value != null && value !== false) {
190
+ element.setAttribute(name, value)
191
+ }
192
+
193
+ if (value == null || value === false) {
194
+ element.removeAttribute(name)
195
+ }
196
+ }
197
+ }
198
+
199
+ function createElement(node, isSvg) {
200
+ var element =
201
+ typeof node === "string" || typeof node === "number"
202
+ ? document.createTextNode(node)
203
+ : (isSvg = isSvg || node.nodeName === "svg")
204
+ ? document.createElementNS(
205
+ "http://www.w3.org/2000/svg",
206
+ node.nodeName
207
+ )
208
+ : document.createElement(node.nodeName)
209
+
210
+ var attributes = node.attributes
211
+ if (attributes) {
212
+ if (attributes.oncreate) {
213
+ lifecycle.push(function() {
214
+ attributes.oncreate(element)
215
+ })
216
+ }
217
+ for (var i = 0; i < node.children.length; i++) {
218
+ element.appendChild(
219
+ createElement(
220
+ (node.children[i] = resolveNode(node.children[i])),
221
+ isSvg
222
+ )
223
+ )
224
+ }
225
+
226
+ for (var name in attributes) {
227
+ updateAttribute(element, name, attributes[name], null, isSvg)
228
+ }
229
+ }
230
+
231
+ return element
232
+ }
233
+
234
+ function updateElement(element, oldAttributes, attributes, isSvg) {
235
+ for (var name in clone(oldAttributes, attributes)) {
236
+ if (
237
+ attributes[name] !==
238
+ (name === "value" || name === "checked"
239
+ ? element[name]
240
+ : oldAttributes[name])
241
+ ) {
242
+ updateAttribute(
243
+ element,
244
+ name,
245
+ attributes[name],
246
+ oldAttributes[name],
247
+ isSvg
248
+ )
249
+ }
250
+ }
251
+
252
+ var cb = isRecycling ? attributes.oncreate : attributes.onupdate
253
+ if (cb) {
254
+ lifecycle.push(function() {
255
+ cb(element, oldAttributes)
256
+ })
257
+ }
258
+ }
259
+
260
+ function removeChildren(element, node) {
261
+ var attributes = node.attributes
262
+ if (attributes) {
263
+ for (var i = 0; i < node.children.length; i++) {
264
+ removeChildren(element.childNodes[i], node.children[i])
265
+ }
266
+
267
+ if (attributes.ondestroy) {
268
+ attributes.ondestroy(element)
269
+ }
270
+ }
271
+ return element
272
+ }
273
+
274
+ function removeElement(parent, element, node) {
275
+ function done() {
276
+ parent.removeChild(removeChildren(element, node))
277
+ }
278
+
279
+ var cb = node.attributes && node.attributes.onremove
280
+ if (cb) {
281
+ cb(element, done)
282
+ } else {
283
+ done()
284
+ }
285
+ }
286
+
287
+ function patch(parent, element, oldNode, node, isSvg) {
288
+ if (node === oldNode) {
289
+ } else if (oldNode == null || oldNode.nodeName !== node.nodeName) {
290
+ var newElement = createElement(node, isSvg)
291
+ parent.insertBefore(newElement, element)
292
+
293
+ if (oldNode != null) {
294
+ removeElement(parent, element, oldNode)
295
+ }
296
+
297
+ element = newElement
298
+ } else if (oldNode.nodeName == null) {
299
+ element.nodeValue = node
300
+ } else {
301
+ updateElement(
302
+ element,
303
+ oldNode.attributes,
304
+ node.attributes,
305
+ (isSvg = isSvg || node.nodeName === "svg")
306
+ )
307
+
308
+ var oldKeyed = {}
309
+ var newKeyed = {}
310
+ var oldElements = []
311
+ var oldChildren = oldNode.children
312
+ var children = node.children
313
+
314
+ for (var i = 0; i < oldChildren.length; i++) {
315
+ oldElements[i] = element.childNodes[i]
316
+
317
+ var oldKey = getKey(oldChildren[i])
318
+ if (oldKey != null) {
319
+ oldKeyed[oldKey] = [oldElements[i], oldChildren[i]]
320
+ }
321
+ }
322
+
323
+ var i = 0
324
+ var k = 0
325
+
326
+ while (k < children.length) {
327
+ var oldKey = getKey(oldChildren[i])
328
+ var newKey = getKey((children[k] = resolveNode(children[k])))
329
+
330
+ if (newKeyed[oldKey]) {
331
+ i++
332
+ continue
333
+ }
334
+
335
+ if (newKey != null && newKey === getKey(oldChildren[i + 1])) {
336
+ if (oldKey == null) {
337
+ removeElement(element, oldElements[i], oldChildren[i])
338
+ }
339
+ i++
340
+ continue
341
+ }
342
+
343
+ if (newKey == null || isRecycling) {
344
+ if (oldKey == null) {
345
+ patch(element, oldElements[i], oldChildren[i], children[k], isSvg)
346
+ k++
347
+ }
348
+ i++
349
+ } else {
350
+ var keyedNode = oldKeyed[newKey] || []
351
+
352
+ if (oldKey === newKey) {
353
+ patch(element, keyedNode[0], keyedNode[1], children[k], isSvg)
354
+ i++
355
+ } else if (keyedNode[0]) {
356
+ patch(
357
+ element,
358
+ element.insertBefore(keyedNode[0], oldElements[i]),
359
+ keyedNode[1],
360
+ children[k],
361
+ isSvg
362
+ )
363
+ } else {
364
+ patch(element, oldElements[i], null, children[k], isSvg)
365
+ }
366
+
367
+ newKeyed[newKey] = children[k]
368
+ k++
369
+ }
370
+ }
371
+
372
+ while (i < oldChildren.length) {
373
+ if (getKey(oldChildren[i]) == null) {
374
+ removeElement(element, oldElements[i], oldChildren[i])
375
+ }
376
+ i++
377
+ }
378
+
379
+ for (var i in oldKeyed) {
380
+ if (!newKeyed[i]) {
381
+ removeElement(element, oldKeyed[i][0], oldKeyed[i][1])
382
+ }
383
+ }
384
+ }
385
+ return element
386
+ }
387
+ };
388
+ }
data/lib/ovto/state.rb ADDED
@@ -0,0 +1,69 @@
1
+ module Ovto
2
+ class State
3
+ # Mandatory value is omitted in the argument of State.new
4
+ class MissingValue < StandardError; end
5
+ # Unknown key is given
6
+ class UnknownKey < StandardError; end
7
+
8
+ # (internal) initialize subclass
9
+ def self.inherited(subclass)
10
+ subclass.instance_variable_set('@item_specs', [])
11
+ end
12
+
13
+ # Declare state item
14
+ def self.item(name, options={})
15
+ @item_specs << [name, options]
16
+ # Define accessor
17
+ define_method(name){ @values[name] }
18
+ end
19
+
20
+ # Return list of item specs (Array of `[name, options]`)
21
+ def self.item_specs
22
+ @item_specs
23
+ end
24
+
25
+ def initialize(hash = {})
26
+ unknown_keys = hash.keys - self.class.item_specs.map(&:first)
27
+ if unknown_keys.any?
28
+ raise UnknownKey, "unknown key(s): #{unknown_keys.inspect}"
29
+ end
30
+
31
+ @values = self.class.item_specs.map{|name, options|
32
+ if !hash.key?(name) && !options.key?(:default)
33
+ raise MissingValue, ":#{name} is mandatory for #{self.class.name}.new"
34
+ end
35
+ # Note that `hash[key]` may be false or nil
36
+ value = hash.key?(name) ? hash[name] : options[:default]
37
+ [name, value]
38
+ }.to_h
39
+ end
40
+ attr_reader :values
41
+
42
+ # Create new state object from `self` and `hash`
43
+ def merge(hash)
44
+ unknown_keys = hash.keys - self.class.item_specs.map(&:first)
45
+ if unknown_keys.any?
46
+ raise UnknownKey, "unknown key(s): #{unknown_keys.inspect}"
47
+ end
48
+ self.class.new(@values.merge(hash))
49
+ end
50
+
51
+ # Return the value corresponds to `key`
52
+ def [](key)
53
+ @values[key]
54
+ end
55
+
56
+ # Return true if a State object `other` has same key-value paris as `self`
57
+ def ==(other)
58
+ other.is_a?(State) && self.values == other.values
59
+ end
60
+
61
+ def to_h
62
+ @values
63
+ end
64
+
65
+ def inspect
66
+ "#<#{self.class.name}:#{object_id} #{@values.inspect}>"
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,3 @@
1
+ module Ovto
2
+ VERSION = '0.2.0'
3
+ end
@@ -0,0 +1,33 @@
1
+ require 'promise'
2
+
3
+ module Ovto
4
+ class WiredActions
5
+ def initialize(actions, app, runtime)
6
+ @actions, @app, @runtime = actions, app, runtime
7
+ end
8
+
9
+ def method_missing(name, args_hash={})
10
+ invoke_action(name, args_hash)
11
+ end
12
+
13
+ def respond_to?(name)
14
+ @actions.respond_to?(name)
15
+ end
16
+
17
+ private
18
+
19
+ # Call action and schedule rendering
20
+ def invoke_action(name, args_hash)
21
+ kwargs = {state: @app.state}.merge(args_hash)
22
+ state_diff = @actions.__send__(name, **kwargs)
23
+ return if state_diff.nil? || state_diff.is_a?(Promise) || `!!state_diff.then`
24
+
25
+ new_state = @app.state.merge(state_diff)
26
+ if new_state != @app.state
27
+ @runtime.scheduleRender
28
+ @app._set_state(new_state)
29
+ end
30
+ return new_state
31
+ end
32
+ end
33
+ end
data/lib/ovto.rb ADDED
@@ -0,0 +1,50 @@
1
+ if RUBY_ENGINE == 'opal'
2
+ require 'console'; def console; $console; end
3
+ require_relative 'ovto/actions'
4
+ require_relative 'ovto/app'
5
+ require_relative 'ovto/component'
6
+ require_relative 'ovto/fetch'
7
+ require_relative 'ovto/runtime'
8
+ require_relative 'ovto/state'
9
+ require_relative 'ovto/version'
10
+ require_relative 'ovto/wired_actions'
11
+ else
12
+ require 'ovto/version'
13
+ require 'opal'; Opal.append_path(__dir__)
14
+ end
15
+
16
+ module Ovto
17
+ # JS-object-safe inspect
18
+ def self.inspect(obj)
19
+ if `obj.$inspect`
20
+ obj.inspect
21
+ else
22
+ `JSON.stringify(#{obj}) || "undefined"`
23
+ end
24
+ end
25
+
26
+ # Call block. If an exception is raised and there is a tag with `id='ovto-debug'`,
27
+ # describe the error in that tag
28
+ def self.log_error(&block)
29
+ return block.call
30
+ rescue Exception => ex
31
+ raise ex if `typeof document === 'undefined'` # On unit tests
32
+
33
+ div = `document.getElementById('ovto-debug')`
34
+ if `div && !ex.OvtoPrinted`
35
+ %x{
36
+ div.textContent = "ERROR: " + #{ex.class.name};
37
+ var ul = document.createElement('ul');
38
+ // Note: ex.backtrace may be an Array or a String
39
+ #{Array(ex.backtrace)}.forEach(function(line){
40
+ var li = document.createElement('li');
41
+ li.textContent = line;
42
+ ul.appendChild(li);
43
+ });
44
+ div.appendChild(ul);
45
+ ex.OvtoPrinted = true;
46
+ }
47
+ end
48
+ raise ex
49
+ end
50
+ end
data/ovto.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ require "#{__dir__}/lib/ovto/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "ovto"
5
+ spec.version = Ovto::VERSION
6
+ spec.authors = ["Yutaka HARA"]
7
+ spec.email = ["yutaka.hara+github@gmail.com"]
8
+
9
+ spec.summary = %q{Simple client-side framework for Opal}
10
+ spec.description = %q{Simple client-side framework for Opal}
11
+ spec.homepage = "https://github.com/yhara/ovto"
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
15
+ f.match(%r{^(test|spec|features)/})
16
+ end
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "opal", '~> 0.11'
22
+ end
data/screenshot.png ADDED
Binary file