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