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
|
@@ -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')
|
data/lib/ovto/actions.rb
ADDED
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
|