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,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