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