ovto 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.gitmodules +3 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +57 -0
- data/LICENSE.txt +44 -0
- data/README.md +84 -0
- data/Rakefile +41 -0
- data/book/README.md +1 -0
- data/book/SUMMARY.md +13 -0
- data/book/api/actions.md +77 -0
- data/book/api/app.md +86 -0
- data/book/api/component.md +175 -0
- data/book/api/fetch.md +42 -0
- data/book/api/state.md +97 -0
- data/book/guides/debugging.md +23 -0
- data/book/guides/development.md +11 -0
- data/book/guides/tutorial.md +288 -0
- data/book/screenshot.png +0 -0
- data/docs/api/Ovto/Actions.html +135 -0
- data/docs/api/Ovto/App.html +531 -0
- data/docs/api/Ovto/Component/MoreThanOneNode.html +135 -0
- data/docs/api/Ovto/Component.html +350 -0
- data/docs/api/Ovto/Runtime.html +315 -0
- data/docs/api/Ovto/State/MissingValue.html +135 -0
- data/docs/api/Ovto/State/UnknownKey.html +135 -0
- data/docs/api/Ovto/State.html +699 -0
- data/docs/api/Ovto/WiredActions.html +343 -0
- data/docs/api/Ovto.html +319 -0
- data/docs/api/_index.html +229 -0
- data/docs/api/actions.html +398 -0
- data/docs/api/app.html +411 -0
- data/docs/api/class_list.html +51 -0
- data/docs/api/component.html +469 -0
- data/docs/api/css/common.css +1 -0
- data/docs/api/css/full_list.css +58 -0
- data/docs/api/css/style.css +499 -0
- data/docs/api/file.README.html +162 -0
- data/docs/api/file_list.html +56 -0
- data/docs/api/frames.html +17 -0
- data/docs/api/index.html +162 -0
- data/docs/api/js/app.js +248 -0
- data/docs/api/js/full_list.js +216 -0
- data/docs/api/js/jquery.js +4 -0
- data/docs/api/method_list.html +243 -0
- data/docs/api/state.html +430 -0
- data/docs/api/top-level-namespace.html +110 -0
- data/docs/gitbook/fonts/fontawesome/FontAwesome.otf +0 -0
- data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.eot +0 -0
- data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.svg +685 -0
- data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.ttf +0 -0
- data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff +0 -0
- data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff2 +0 -0
- data/docs/gitbook/gitbook-plugin-fontsettings/fontsettings.js +240 -0
- data/docs/gitbook/gitbook-plugin-fontsettings/website.css +291 -0
- data/docs/gitbook/gitbook-plugin-highlight/ebook.css +135 -0
- data/docs/gitbook/gitbook-plugin-highlight/website.css +434 -0
- data/docs/gitbook/gitbook-plugin-lunr/lunr.min.js +7 -0
- data/docs/gitbook/gitbook-plugin-lunr/search-lunr.js +59 -0
- data/docs/gitbook/gitbook-plugin-search/lunr.min.js +7 -0
- data/docs/gitbook/gitbook-plugin-search/search-engine.js +50 -0
- data/docs/gitbook/gitbook-plugin-search/search.css +35 -0
- data/docs/gitbook/gitbook-plugin-search/search.js +213 -0
- data/docs/gitbook/gitbook-plugin-sharing/buttons.js +90 -0
- data/docs/gitbook/gitbook.js +4 -0
- data/docs/gitbook/images/apple-touch-icon-precomposed-152.png +0 -0
- data/docs/gitbook/images/favicon.ico +0 -0
- data/docs/gitbook/style.css +9 -0
- data/docs/gitbook/theme.js +4 -0
- data/docs/guides/debugging.html +355 -0
- data/docs/guides/development.html +361 -0
- data/docs/guides/tutorial.html +571 -0
- data/docs/index.html +422 -0
- data/docs/screenshot.png +0 -0
- data/docs/search_index.json +1 -0
- data/example/sinatra/Gemfile +6 -0
- data/example/sinatra/Gemfile.lock +59 -0
- data/example/sinatra/README.md +21 -0
- data/example/sinatra/app.rb +18 -0
- data/example/sinatra/config.ru +30 -0
- data/example/sinatra/ovto/app.rb +171 -0
- data/example/sinatra/public/style.css +4 -0
- data/example/sinatra/public/todomvc-app-css_index.css +376 -0
- data/example/sinatra/public/todomvc-common_base.css +141 -0
- data/example/sinatra/views/index.erb +21 -0
- data/example/static/Gemfile +3 -0
- data/example/static/Gemfile.lock +30 -0
- data/example/static/README.md +10 -0
- data/example/static/Rakefile +4 -0
- data/example/static/app.js +24808 -0
- data/example/static/app.rb +43 -0
- data/example/static/index.html +11 -0
- data/lib/ovto/actions.rb +10 -0
- data/lib/ovto/app.rb +58 -0
- data/lib/ovto/component.rb +191 -0
- data/lib/ovto/fetch.rb +53 -0
- data/lib/ovto/runtime.rb +388 -0
- data/lib/ovto/state.rb +69 -0
- data/lib/ovto/version.rb +3 -0
- data/lib/ovto/wired_actions.rb +33 -0
- data/lib/ovto.rb +50 -0
- data/ovto.gemspec +22 -0
- data/screenshot.png +0 -0
- metadata +161 -0
data/lib/ovto/runtime.rb
ADDED
@@ -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
|
data/lib/ovto/version.rb
ADDED
@@ -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
|