ovto 0.6.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +15 -16
- data/Rakefile +2 -9
- data/book/SUMMARY.md +15 -11
- data/book/book.toml +10 -0
- data/docs/.nojekyll +1 -0
- data/docs/404.html +189 -0
- data/docs/FontAwesome/css/font-awesome.css +4 -0
- data/docs/FontAwesome/fonts/FontAwesome.ttf +0 -0
- data/docs/FontAwesome/fonts/fontawesome-webfont.eot +0 -0
- data/docs/FontAwesome/fonts/fontawesome-webfont.svg +2671 -0
- data/docs/{gitbook/fonts/fontawesome → FontAwesome/fonts}/fontawesome-webfont.ttf +0 -0
- data/docs/FontAwesome/fonts/fontawesome-webfont.woff +0 -0
- data/docs/FontAwesome/fonts/fontawesome-webfont.woff2 +0 -0
- data/docs/api/Array.html +4 -4
- data/docs/api/Hash.html +4 -4
- data/docs/api/MightyInspect.html +238 -0
- data/docs/api/Ovto/Actions.html +4 -4
- data/docs/api/Ovto/App.html +4 -4
- data/docs/api/Ovto/Component/MoreThanOneNode.html +5 -5
- data/docs/api/Ovto/Component.html +4 -4
- data/docs/api/Ovto/Middleware/Actions.html +4 -4
- data/docs/api/Ovto/Middleware/Base.html +5 -5
- data/docs/api/Ovto/Middleware/Component.html +5 -5
- data/docs/api/Ovto/Middleware.html +8 -8
- data/docs/api/Ovto/PureComponent/StateIsNotAvailable.html +4 -4
- data/docs/api/Ovto/PureComponent.html +4 -4
- data/docs/api/Ovto/Runtime.html +4 -4
- data/docs/api/Ovto/State/MissingValue.html +4 -4
- data/docs/api/Ovto/State/UnknownStateKey.html +4 -4
- data/docs/api/Ovto/State.html +12 -12
- data/docs/api/Ovto/WiredActionSet.html +4 -4
- data/docs/api/Ovto/WiredActions.html +4 -4
- data/docs/api/Ovto.html +8 -8
- data/docs/api/_index.html +5 -5
- data/docs/api/actions.html +215 -426
- data/docs/api/app.html +226 -432
- data/docs/api/component.html +268 -480
- data/docs/api/fetch.html +188 -393
- data/docs/api/file.README.html +4 -4
- data/docs/api/frames.html +1 -1
- data/docs/api/index.html +4 -4
- data/docs/api/middleware.html +249 -460
- data/docs/api/pure_component.html +186 -398
- data/docs/api/state.html +226 -438
- data/docs/api/top-level-namespace.html +4 -4
- data/docs/ayu-highlight.css +78 -0
- data/docs/book.js +688 -0
- data/docs/book.toml +10 -0
- data/docs/clipboard.min.js +7 -0
- data/docs/css/chrome.css +545 -0
- data/docs/css/general.css +203 -0
- data/docs/css/print.css +54 -0
- data/docs/css/variables.css +255 -0
- data/docs/elasticlunr.min.js +10 -0
- data/docs/favicon.png +0 -0
- data/docs/favicon.svg +22 -0
- data/docs/fonts/OPEN-SANS-LICENSE.txt +202 -0
- data/docs/fonts/SOURCE-CODE-PRO-LICENSE.txt +93 -0
- data/docs/fonts/fonts.css +100 -0
- data/docs/fonts/open-sans-v17-all-charsets-300.woff2 +0 -0
- data/docs/fonts/open-sans-v17-all-charsets-300italic.woff2 +0 -0
- data/docs/fonts/open-sans-v17-all-charsets-600.woff2 +0 -0
- data/docs/fonts/open-sans-v17-all-charsets-600italic.woff2 +0 -0
- data/docs/fonts/open-sans-v17-all-charsets-700.woff2 +0 -0
- data/docs/fonts/open-sans-v17-all-charsets-700italic.woff2 +0 -0
- data/docs/fonts/open-sans-v17-all-charsets-800.woff2 +0 -0
- data/docs/fonts/open-sans-v17-all-charsets-800italic.woff2 +0 -0
- data/docs/fonts/open-sans-v17-all-charsets-italic.woff2 +0 -0
- data/docs/fonts/open-sans-v17-all-charsets-regular.woff2 +0 -0
- data/docs/fonts/source-code-pro-v11-all-charsets-500.woff2 +0 -0
- data/docs/guides/debugging.html +184 -390
- data/docs/guides/development.html +171 -383
- data/docs/guides/install.html +206 -409
- data/docs/guides/tutorial.html +309 -525
- data/docs/highlight.css +82 -0
- data/docs/highlight.js +6 -0
- data/docs/index.html +390 -391
- data/docs/mark.min.js +7 -0
- data/docs/print.html +958 -0
- data/docs/searcher.js +483 -0
- data/docs/searchindex.js +1 -0
- data/docs/searchindex.json +1 -0
- data/docs/tomorrow-night.css +102 -0
- data/examples/sinatra/Gemfile.lock +25 -25
- data/examples/static/Gemfile.lock +8 -8
- data/lib/ovto/component.rb +2 -3
- data/lib/ovto/version.rb +1 -1
- metadata +47 -26
- 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 +0 -685
- 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 +0 -240
- data/docs/gitbook/gitbook-plugin-fontsettings/website.css +0 -291
- data/docs/gitbook/gitbook-plugin-highlight/ebook.css +0 -135
- data/docs/gitbook/gitbook-plugin-highlight/website.css +0 -434
- data/docs/gitbook/gitbook-plugin-lunr/lunr.min.js +0 -7
- data/docs/gitbook/gitbook-plugin-lunr/search-lunr.js +0 -59
- data/docs/gitbook/gitbook-plugin-search/lunr.min.js +0 -7
- data/docs/gitbook/gitbook-plugin-search/search-engine.js +0 -50
- data/docs/gitbook/gitbook-plugin-search/search.css +0 -35
- data/docs/gitbook/gitbook-plugin-search/search.js +0 -213
- data/docs/gitbook/gitbook-plugin-sharing/buttons.js +0 -90
- data/docs/gitbook/gitbook.js +0 -4
- 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 +0 -9
- data/docs/gitbook/theme.js +0 -4
- data/docs/search_index.json +0 -1
data/docs/print.html
ADDED
@@ -0,0 +1,958 @@
|
|
1
|
+
<!DOCTYPE HTML>
|
2
|
+
<html lang="en" class="sidebar-visible no-js light">
|
3
|
+
<head>
|
4
|
+
<!-- Book generated using mdBook -->
|
5
|
+
<meta charset="UTF-8">
|
6
|
+
<title>Ovto Reference Manual</title>
|
7
|
+
<meta name="robots" content="noindex" />
|
8
|
+
|
9
|
+
|
10
|
+
<!-- Custom HTML head -->
|
11
|
+
|
12
|
+
<meta name="description" content="">
|
13
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
14
|
+
<meta name="theme-color" content="#ffffff" />
|
15
|
+
|
16
|
+
<link rel="icon" href="favicon.svg">
|
17
|
+
<link rel="shortcut icon" href="favicon.png">
|
18
|
+
<link rel="stylesheet" href="css/variables.css">
|
19
|
+
<link rel="stylesheet" href="css/general.css">
|
20
|
+
<link rel="stylesheet" href="css/chrome.css">
|
21
|
+
<link rel="stylesheet" href="css/print.css" media="print">
|
22
|
+
|
23
|
+
<!-- Fonts -->
|
24
|
+
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
25
|
+
<link rel="stylesheet" href="fonts/fonts.css">
|
26
|
+
|
27
|
+
<!-- Highlight.js Stylesheets -->
|
28
|
+
<link rel="stylesheet" href="highlight.css">
|
29
|
+
<link rel="stylesheet" href="tomorrow-night.css">
|
30
|
+
<link rel="stylesheet" href="ayu-highlight.css">
|
31
|
+
|
32
|
+
<!-- Custom theme stylesheets -->
|
33
|
+
|
34
|
+
</head>
|
35
|
+
<body>
|
36
|
+
<div id="body-container">
|
37
|
+
<!-- Provide site root to javascript -->
|
38
|
+
<script>
|
39
|
+
var path_to_root = "";
|
40
|
+
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
|
41
|
+
</script>
|
42
|
+
|
43
|
+
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
44
|
+
<script>
|
45
|
+
try {
|
46
|
+
var theme = localStorage.getItem('mdbook-theme');
|
47
|
+
var sidebar = localStorage.getItem('mdbook-sidebar');
|
48
|
+
|
49
|
+
if (theme.startsWith('"') && theme.endsWith('"')) {
|
50
|
+
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
51
|
+
}
|
52
|
+
|
53
|
+
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
54
|
+
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
55
|
+
}
|
56
|
+
} catch (e) { }
|
57
|
+
</script>
|
58
|
+
|
59
|
+
<!-- Set the theme before any content is loaded, prevents flash -->
|
60
|
+
<script>
|
61
|
+
var theme;
|
62
|
+
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
63
|
+
if (theme === null || theme === undefined) { theme = default_theme; }
|
64
|
+
var html = document.querySelector('html');
|
65
|
+
html.classList.remove('no-js')
|
66
|
+
html.classList.remove('light')
|
67
|
+
html.classList.add(theme);
|
68
|
+
html.classList.add('js');
|
69
|
+
</script>
|
70
|
+
|
71
|
+
<!-- Hide / unhide sidebar before it is displayed -->
|
72
|
+
<script>
|
73
|
+
var html = document.querySelector('html');
|
74
|
+
var sidebar = null;
|
75
|
+
if (document.body.clientWidth >= 1080) {
|
76
|
+
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
77
|
+
sidebar = sidebar || 'visible';
|
78
|
+
} else {
|
79
|
+
sidebar = 'hidden';
|
80
|
+
}
|
81
|
+
html.classList.remove('sidebar-visible');
|
82
|
+
html.classList.add("sidebar-" + sidebar);
|
83
|
+
</script>
|
84
|
+
|
85
|
+
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
86
|
+
<div class="sidebar-scrollbox">
|
87
|
+
<ol class="chapter"><li class="chapter-item expanded "><a href="guides/tutorial.html"><strong aria-hidden="true">1.</strong> Getting Started</a></li><li class="chapter-item expanded "><a href="guides/install.html"><strong aria-hidden="true">2.</strong> Install</a></li><li class="chapter-item expanded affix "><li class="part-title">API</li><li class="chapter-item expanded "><a href="api/app.html"><strong aria-hidden="true">3.</strong> Ovto::App</a></li><li class="chapter-item expanded "><a href="api/state.html"><strong aria-hidden="true">4.</strong> Ovto::State</a></li><li class="chapter-item expanded "><a href="api/actions.html"><strong aria-hidden="true">5.</strong> Ovto::Actions</a></li><li class="chapter-item expanded "><a href="api/component.html"><strong aria-hidden="true">6.</strong> Ovto::Component</a></li><li class="chapter-item expanded "><a href="api/pure_component.html"><strong aria-hidden="true">7.</strong> Ovto::PureComponent</a></li><li class="chapter-item expanded "><a href="api/middleware.html"><strong aria-hidden="true">8.</strong> Ovto::Middleware</a></li><li class="chapter-item expanded "><a href="api/fetch.html"><strong aria-hidden="true">9.</strong> Ovto.fetch</a></li><li class="chapter-item expanded affix "><li class="part-title">Guides</li><li class="chapter-item expanded "><a href="guides/debugging.html"><strong aria-hidden="true">10.</strong> Debugging</a></li><li class="chapter-item expanded "><a href="guides/development.html"><strong aria-hidden="true">11.</strong> Development</a></li></ol>
|
88
|
+
</div>
|
89
|
+
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
90
|
+
</nav>
|
91
|
+
|
92
|
+
<div id="page-wrapper" class="page-wrapper">
|
93
|
+
|
94
|
+
<div class="page">
|
95
|
+
<div id="menu-bar-hover-placeholder"></div>
|
96
|
+
<div id="menu-bar" class="menu-bar sticky bordered">
|
97
|
+
<div class="left-buttons">
|
98
|
+
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
99
|
+
<i class="fa fa-bars"></i>
|
100
|
+
</button>
|
101
|
+
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
102
|
+
<i class="fa fa-paint-brush"></i>
|
103
|
+
</button>
|
104
|
+
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
105
|
+
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
106
|
+
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
107
|
+
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
108
|
+
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
109
|
+
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
110
|
+
</ul>
|
111
|
+
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
112
|
+
<i class="fa fa-search"></i>
|
113
|
+
</button>
|
114
|
+
</div>
|
115
|
+
|
116
|
+
<h1 class="menu-title">Ovto Reference Manual</h1>
|
117
|
+
|
118
|
+
<div class="right-buttons">
|
119
|
+
<a href="print.html" title="Print this book" aria-label="Print this book">
|
120
|
+
<i id="print-button" class="fa fa-print"></i>
|
121
|
+
</a>
|
122
|
+
|
123
|
+
</div>
|
124
|
+
</div>
|
125
|
+
|
126
|
+
<div id="search-wrapper" class="hidden">
|
127
|
+
<form id="searchbar-outer" class="searchbar-outer">
|
128
|
+
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
129
|
+
</form>
|
130
|
+
<div id="searchresults-outer" class="searchresults-outer hidden">
|
131
|
+
<div id="searchresults-header" class="searchresults-header"></div>
|
132
|
+
<ul id="searchresults">
|
133
|
+
</ul>
|
134
|
+
</div>
|
135
|
+
</div>
|
136
|
+
|
137
|
+
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
138
|
+
<script>
|
139
|
+
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
140
|
+
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
141
|
+
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
142
|
+
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
143
|
+
});
|
144
|
+
</script>
|
145
|
+
|
146
|
+
<div id="content" class="content">
|
147
|
+
<main>
|
148
|
+
<h1 id="getting-started"><a class="header" href="#getting-started">Getting Started</a></h1>
|
149
|
+
<p>This is a tutorial of making an Ovto app. We create a static app (.html + .js) here,
|
150
|
+
but you can embed Ovto apps into a Rails or Sinatra app (See <code>./examples/*</code>).</p>
|
151
|
+
<p>This is the final Ruby code.</p>
|
152
|
+
<pre><code class="language-rb">require 'ovto'
|
153
|
+
|
154
|
+
class MyApp < Ovto::App
|
155
|
+
class State < Ovto::State
|
156
|
+
item :celsius, default: 0
|
157
|
+
|
158
|
+
def fahrenheit
|
159
|
+
(celsius * 9 / 5.0) + 32
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
class Actions < Ovto::Actions
|
164
|
+
def set_celsius(value:)
|
165
|
+
return {celsius: value}
|
166
|
+
end
|
167
|
+
|
168
|
+
def set_fahrenheit(value:)
|
169
|
+
new_celsius = (value - 32) * 5 / 9.0
|
170
|
+
return {celsius: new_celsius}
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class MainComponent < Ovto::Component
|
175
|
+
def render
|
176
|
+
o 'div' do
|
177
|
+
o 'span', 'Celcius:'
|
178
|
+
o 'input', {
|
179
|
+
type: 'text',
|
180
|
+
onchange: ->(e){ actions.set_celsius(value: e.target.value.to_i) },
|
181
|
+
value: state.celsius
|
182
|
+
}
|
183
|
+
o 'span', 'Fahrenheit:'
|
184
|
+
o 'input', {
|
185
|
+
type: 'text',
|
186
|
+
onchange: ->(e){ actions.set_fahrenheit(value: e.target.value.to_i) },
|
187
|
+
value: state.fahrenheit
|
188
|
+
}
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
MyApp.run(id: 'ovto')
|
195
|
+
</code></pre>
|
196
|
+
<p>Let's take a look step-by-step.</p>
|
197
|
+
<h2 id="prerequisites"><a class="header" href="#prerequisites">Prerequisites</a></h2>
|
198
|
+
<ul>
|
199
|
+
<li>Ruby</li>
|
200
|
+
<li>Bundler (<code>gem install bundler</code>)</li>
|
201
|
+
</ul>
|
202
|
+
<h2 id="setup"><a class="header" href="#setup">Setup</a></h2>
|
203
|
+
<p>Make a Gemfile:</p>
|
204
|
+
<pre><code class="language-rb">source "https://rubygems.org"
|
205
|
+
gem "ovto", github: 'yhara/ovto' # Use git master because ovto gem is not released yet
|
206
|
+
gem 'rake'
|
207
|
+
</code></pre>
|
208
|
+
<p>Run <code>bundle install</code>.</p>
|
209
|
+
<h2 id="html"><a class="header" href="#html">HTML</a></h2>
|
210
|
+
<p>Make an index.html:</p>
|
211
|
+
<pre><code class="language-html"><!doctype html>
|
212
|
+
<html>
|
213
|
+
<head>
|
214
|
+
<meta charset="utf-8">
|
215
|
+
<script type='text/javascript' src='app.js'></script>
|
216
|
+
</head>
|
217
|
+
<body>
|
218
|
+
<div id='ovto'></div>
|
219
|
+
<div id='ovto-debug'></div>
|
220
|
+
</body>
|
221
|
+
</html>
|
222
|
+
</code></pre>
|
223
|
+
<h2 id="write-code"><a class="header" href="#write-code">Write code</a></h2>
|
224
|
+
<p>app.rb:</p>
|
225
|
+
<pre><code class="language-rb">require 'ovto'
|
226
|
+
|
227
|
+
class MyApp < Ovto::App
|
228
|
+
class State < Ovto::State
|
229
|
+
end
|
230
|
+
|
231
|
+
class Actions < Ovto::Actions
|
232
|
+
end
|
233
|
+
|
234
|
+
class MainComponent < Ovto::Component
|
235
|
+
def render # Don't miss the `:`. This is not a typo but
|
236
|
+
o 'div' do # a "mandatory keyword argument".
|
237
|
+
o 'h1', "HELLO" # All of the Ovto methods take keyword arguments.
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
MyApp.run(id: 'ovto')
|
244
|
+
</code></pre>
|
245
|
+
<ul>
|
246
|
+
<li>The name <code>MyApp</code> is arbitrary.</li>
|
247
|
+
<li>The id <code>ovto</code> corresponds to the <code>div</code> tag in <code>index.html</code>.</li>
|
248
|
+
</ul>
|
249
|
+
<h2 id="compile"><a class="header" href="#compile">Compile</a></h2>
|
250
|
+
<p>Generate app.js from app.rb.</p>
|
251
|
+
<pre><code>$ bundle exec opal -c -g ovto app.rb > app.js
|
252
|
+
</code></pre>
|
253
|
+
<p>(Compile will fail if there is a syntax error in your <code>app.rb</code>.)</p>
|
254
|
+
<p>Now you can run your app by opening <code>index.html</code> in your browser.</p>
|
255
|
+
<h2 id="trouble-shooting"><a class="header" href="#trouble-shooting">Trouble shooting</a></h2>
|
256
|
+
<p>If you see HELLO, the setup is done. Otherwise, check the developer console
|
257
|
+
and you should see some error messages there.</p>
|
258
|
+
<p>For example if you misspelled <code>class State</code> to <code>class Stat</code>, you will see:</p>
|
259
|
+
<pre><code>app.js:5022 Uncaught $NameError {name: "State", message: "uninitialized constant MyApp::State", stack: "State: uninitialized constant MyApp::State"}
|
260
|
+
</code></pre>
|
261
|
+
<p>because an Ovto app must have a <code>State</code> class in its namespace.</p>
|
262
|
+
<h2 id="tips-auto-compile"><a class="header" href="#tips-auto-compile">(Tips: auto-compile)</a></h2>
|
263
|
+
<p>If you get tired to run <code>bundle exec opal</code> manually, try <code>ifchanged</code> gem:</p>
|
264
|
+
<ol>
|
265
|
+
<li>Add <code>gem "ifchanged"</code> to Gemfile</li>
|
266
|
+
<li><code>bundle install</code></li>
|
267
|
+
<li><code>bundle exec ifchanged ./app.rb --do 'bundle exec opal -c -g ovto app.rb > app.js'</code></li>
|
268
|
+
</ol>
|
269
|
+
<p>Now you just edit and save <code>app.rb</code> and it runs <code>opal -c</code> for you.</p>
|
270
|
+
<h2 id="add-some-state"><a class="header" href="#add-some-state">Add some state</a></h2>
|
271
|
+
<p>In this tutorial, we make an app that convers Celsius and Fahrenheit degrees to
|
272
|
+
each other. First, add an item to <code>MyApp::State</code>.</p>
|
273
|
+
<pre><code class="language-rb"> class State < Ovto::State
|
274
|
+
item :celsius, default: 0
|
275
|
+
end
|
276
|
+
</code></pre>
|
277
|
+
<p>Now an item <code>celsius</code> is added to the global app state. Its value is <code>0</code> when
|
278
|
+
the app starts. You can read this value by <code>state.celsius</code>. Let's display the
|
279
|
+
value with <code>MyApp::MainComponent</code>.</p>
|
280
|
+
<pre><code class="language-rb"> class MainComponent < Ovto::Component
|
281
|
+
def render
|
282
|
+
o 'div' do
|
283
|
+
o 'span', 'Celcius:'
|
284
|
+
o 'input', type: 'text', value: state.celsius
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
</code></pre>
|
289
|
+
<p>Now you should see <code>Celsius: [0 ]</code> in the browser.</p>
|
290
|
+
<h2 id="add-a-method-to-state"><a class="header" href="#add-a-method-to-state">Add a method to State</a></h2>
|
291
|
+
<p>Next, we want to know what degree is it in Fahrenheit. Let's add a method to
|
292
|
+
convert.</p>
|
293
|
+
<pre><code class="language-rb"> class State < Ovto::State
|
294
|
+
item :celsius, default: 0
|
295
|
+
|
296
|
+
def fahrenheit
|
297
|
+
(celsius * 9 / 5.0) + 32
|
298
|
+
end
|
299
|
+
end
|
300
|
+
</code></pre>
|
301
|
+
<p>Now you can know the value by <code>state.fahrenheit</code>. Update <code>MainComponent</code> to show the value too.</p>
|
302
|
+
<pre><code class="language-rb"> class MainComponent < Ovto::Component
|
303
|
+
def render
|
304
|
+
o 'div' do
|
305
|
+
o 'span', 'Celcius:'
|
306
|
+
o 'input', type: 'text', value: state.celsius
|
307
|
+
o 'span', 'Fahrenheit:'
|
308
|
+
o 'input', type: 'text', value: state.fahrenheit
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
</code></pre>
|
313
|
+
<h2 id="add-an-action"><a class="header" href="#add-an-action">Add an action</a></h2>
|
314
|
+
<p>Now we know 0 degrees Celsius is 32 degrees Fahrenheit. But how about 10 degrees or
|
315
|
+
100 degrees Celsius? Let's update the app to we can specify a Celsius value.</p>
|
316
|
+
<p>You may think that you can change the value with <code>state.celsius = 100</code>, but this is
|
317
|
+
prohibited. In Ovto, you can only modify app state with Actions.</p>
|
318
|
+
<p>Our first action looks like this. An action is a method defined in <code>MyApp::Actions</code>.
|
319
|
+
It takes an old state (and its own parameters) and returns a Hash that describes
|
320
|
+
the updates to the state. This return value is <code>merge</code>d into the global app state.</p>
|
321
|
+
<pre><code class="language-rb"> class Actions < Ovto::Actions
|
322
|
+
def set_celsius(value:)
|
323
|
+
return {celsius: value}
|
324
|
+
end
|
325
|
+
end
|
326
|
+
</code></pre>
|
327
|
+
<p>This action can be called by <code>actions.set_celsius</code> from MainComponent. Replace the
|
328
|
+
first input tag with this:</p>
|
329
|
+
<pre><code class="language-rb"> o 'input', {
|
330
|
+
type: 'text',
|
331
|
+
onchange: ->(e){ actions.set_celsius(value: e.target.value.to_i) },
|
332
|
+
value: state.celsius
|
333
|
+
}
|
334
|
+
</code></pre>
|
335
|
+
<p><code>onchange:</code> is a special attribute that takes an event handler as its value.
|
336
|
+
The argument <code>e</code> is an instance of <code>Opal::Native</code> and wraps the event object of
|
337
|
+
JavaScript. In this case you can get the input string by <code>e.target.value</code>.</p>
|
338
|
+
<p>Now reload your browser and input <code>100</code> to the left input box. Next, press Tab key
|
339
|
+
(or click somewhere in the page) to commit the value. Then you should see <code>212</code>
|
340
|
+
in the right input box. 100 degrees Celsius is 212 degrees Fahrenheit!</p>
|
341
|
+
<h2 id="what-has-happend"><a class="header" href="#what-has-happend">What has happend</a></h2>
|
342
|
+
<p>In case you are curious, here is what happens when you give 100 to the input box.</p>
|
343
|
+
<ol>
|
344
|
+
<li>JavaScript's <code>onchange</code> event is executed.</li>
|
345
|
+
<li>Ovto calls the event handler.</li>
|
346
|
+
<li>It calls <code>actions.set_celsius</code>. <code>actions</code> is an instance of <code>Ovto::WiredActions</code>.
|
347
|
+
It is a proxy to the <code>MyApp::Actions</code>. It has the same methods as those in
|
348
|
+
<code>MyApp::Actions</code> but does some more:</li>
|
349
|
+
</ol>
|
350
|
+
<ul>
|
351
|
+
<li>It passes <code>state</code> to the user-defined action.</li>
|
352
|
+
<li>It merges the result to the global app state.</li>
|
353
|
+
<li>It schedules re-rendering the view to represent the new state.</li>
|
354
|
+
</ul>
|
355
|
+
<h2 id="reverse-conversion"><a class="header" href="#reverse-conversion">Reverse conversion</a></h2>
|
356
|
+
<p>It is easy to update the app to support Fahrenheit-to-Celsius conversion.
|
357
|
+
The second input should be updated to:</p>
|
358
|
+
<pre><code class="language-rb"> o 'input', {
|
359
|
+
type: 'text',
|
360
|
+
onchange: ->(e){ actions.set_fahrenheit(value: e.target.value.to_i) },
|
361
|
+
value: state.fahrenheit
|
362
|
+
}
|
363
|
+
</code></pre>
|
364
|
+
<p>Then add an action <code>set_fahrenheit</code> to <code>MyApp::Actions</code>. This action convers the
|
365
|
+
Fahrenheit degree into Celsius and set it to the global state.</p>
|
366
|
+
<pre><code class="language-rb"> def set_fahrenheit(value:)
|
367
|
+
new_celsius = (value - 32) * 5 / 9.0
|
368
|
+
return {celsius: new_celsius}
|
369
|
+
end
|
370
|
+
</code></pre>
|
371
|
+
<p>Now your app should react to the change of the Fahrenheit value too. </p>
|
372
|
+
<div style="break-before: page; page-break-before: always;"></div><h1 id="install"><a class="header" href="#install">Install</a></h1>
|
373
|
+
<h2 id="use-ovto-with-static-files"><a class="header" href="#use-ovto-with-static-files">Use Ovto with static files</a></h2>
|
374
|
+
<pre><code>$ gem i ovto
|
375
|
+
$ ovto new myapp --static
|
376
|
+
</code></pre>
|
377
|
+
<h2 id="use-ovto-with-sinatra"><a class="header" href="#use-ovto-with-sinatra">Use Ovto with Sinatra</a></h2>
|
378
|
+
<pre><code>$ gem i ovto
|
379
|
+
$ ovto new myapp --sinatra
|
380
|
+
</code></pre>
|
381
|
+
<h2 id="install-ovto-into-rails-apps"><a class="header" href="#install-ovto-into-rails-apps">Install Ovto into Rails apps</a></h2>
|
382
|
+
<p>Edit <code>Gemfile</code></p>
|
383
|
+
<pre><code class="language-rb">gem 'opal-rails'
|
384
|
+
gem 'ovto'
|
385
|
+
</code></pre>
|
386
|
+
<p>Run <code>bundle install</code></p>
|
387
|
+
<p>Remove <code>app/assets/javascripts/application.js</code></p>
|
388
|
+
<p>Create <code>app/assets/javascripts/application.js.rb</code></p>
|
389
|
+
<pre><code class="language-rb">require 'opal'
|
390
|
+
require 'rails-ujs'
|
391
|
+
require 'activestorage'
|
392
|
+
require 'turbolinks'
|
393
|
+
require_tree '.'
|
394
|
+
</code></pre>
|
395
|
+
<p>Create <code>app/assets/javascripts/foo.js.rb</code> (file name is arbitrary)</p>
|
396
|
+
<pre><code class="language-rb">require 'ovto'
|
397
|
+
|
398
|
+
class Foo < Ovto::App
|
399
|
+
class State < Ovto::State
|
400
|
+
end
|
401
|
+
|
402
|
+
class Actions < Ovto::Actions
|
403
|
+
end
|
404
|
+
|
405
|
+
class MainComponent < Ovto::Component
|
406
|
+
def render(state:)
|
407
|
+
o 'h1', "HELLO"
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
</code></pre>
|
412
|
+
<p>Edit <code>app/views/<some controller/<some view>.html.erb</code></p>
|
413
|
+
<pre><code><div id='foo-app'></div>
|
414
|
+
<%= opal_tag do %>
|
415
|
+
Foo.run(id: 'foo-app')
|
416
|
+
<% end %>
|
417
|
+
</code></pre>
|
418
|
+
<p>This should render <code>HELLO</code> in the browser.</p>
|
419
|
+
<p>You also need to edit config/environments/production.rb like this before deploy it to production.</p>
|
420
|
+
<pre><code class="language-rb"> #config.assets.js_compressor = :uglifier
|
421
|
+
config.assets.js_compressor = Uglifier.new(harmony: true)
|
422
|
+
</code></pre>
|
423
|
+
<div style="break-before: page; page-break-before: always;"></div><h1 id="ovtoapp"><a class="header" href="#ovtoapp">Ovto::App</a></h1>
|
424
|
+
<p>First of all, you need to define a subclass of <code>Ovto::App</code> and define <code>class State</code>,
|
425
|
+
<code>class Actions</code> and <code>class MainComponent</code> in it.</p>
|
426
|
+
<h2 id="example"><a class="header" href="#example">Example</a></h2>
|
427
|
+
<p>This is a smallest Ovto app.</p>
|
428
|
+
<pre><code class="language-rb">require 'opal'
|
429
|
+
require 'ovto'
|
430
|
+
|
431
|
+
class MyApp < Ovto::App
|
432
|
+
class State < Ovto::State
|
433
|
+
end
|
434
|
+
|
435
|
+
class Actions < Ovto::Actions
|
436
|
+
end
|
437
|
+
|
438
|
+
class MainComponent < Ovto::Component
|
439
|
+
def render
|
440
|
+
o 'input', type: 'button', value: 'Hello'
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
MyApp.run(id: 'ovto')
|
446
|
+
</code></pre>
|
447
|
+
<p>It renders a button and does nothing else. Let's have some fun:</p>
|
448
|
+
<pre><code class="language-rb">require 'opal'
|
449
|
+
require 'ovto'
|
450
|
+
|
451
|
+
class MyApp < Ovto::App
|
452
|
+
COLORS = ["red", "blue", "green"]
|
453
|
+
|
454
|
+
class State < Ovto::State
|
455
|
+
item :color_idx, default: 0
|
456
|
+
end
|
457
|
+
|
458
|
+
class Actions < Ovto::Actions
|
459
|
+
def update_color
|
460
|
+
new_idx = (state.color_idx + 1) % COLORS.length
|
461
|
+
return {color_idx: new_idx}
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
class MainComponent < Ovto::Component
|
466
|
+
def render
|
467
|
+
o 'input', {
|
468
|
+
type: 'button',
|
469
|
+
value: 'Hello',
|
470
|
+
style: {background: COLORS[state.color_idx]},
|
471
|
+
onclick: ->{ actions.update_color },
|
472
|
+
}
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
MyApp.run(id: 'ovto')
|
478
|
+
</code></pre>
|
479
|
+
<p>Here we added <code>color_idx</code> to app state and <code>update_color</code> action to change it.
|
480
|
+
The button is updated to have the color indicated by <code>color_idx</code> and
|
481
|
+
now has <code>onclick</code> event handler which executes the action.</p>
|
482
|
+
<h2 id="calling-actions-on-startup"><a class="header" href="#calling-actions-on-startup">Calling actions on startup</a></h2>
|
483
|
+
<p>To invoke certain actions on app startup, define <code>MyApp#setup</code> and use <code>MyApp#actions</code>.</p>
|
484
|
+
<p>Example:</p>
|
485
|
+
<pre><code class="language-rb">class MyApp < Ovto::App
|
486
|
+
def setup
|
487
|
+
actions.fetch_data()
|
488
|
+
end
|
489
|
+
|
490
|
+
...
|
491
|
+
end
|
492
|
+
|
493
|
+
MyApp.run(id: 'ovto')
|
494
|
+
</code></pre>
|
495
|
+
<div style="break-before: page; page-break-before: always;"></div><h1 id="ovtostate"><a class="header" href="#ovtostate">Ovto::State</a></h1>
|
496
|
+
<p><code>Ovto::State</code> is like a hash, but members are accessible with name rather than <code>[]</code>.</p>
|
497
|
+
<h2 id="example-1"><a class="header" href="#example-1">Example</a></h2>
|
498
|
+
<pre><code class="language-rb">class State < Ovto::State
|
499
|
+
item :foo
|
500
|
+
item :bar
|
501
|
+
end
|
502
|
+
|
503
|
+
state = State.new(foo: 1, bar: 2)
|
504
|
+
state.foo #=> 1
|
505
|
+
state.bar #=> 2
|
506
|
+
</code></pre>
|
507
|
+
<h2 id="default-value"><a class="header" href="#default-value">Default value</a></h2>
|
508
|
+
<pre><code class="language-rb">class State < Ovto::State
|
509
|
+
item :foo, default: 1
|
510
|
+
item :bar, default: 2
|
511
|
+
end
|
512
|
+
|
513
|
+
state = State.new
|
514
|
+
state.foo #=> 1
|
515
|
+
state.bar #=> 2
|
516
|
+
</code></pre>
|
517
|
+
<h2 id="immutable"><a class="header" href="#immutable">Immutable</a></h2>
|
518
|
+
<p>State objects are immutable. i.e. you cannot update value of a key. Instead, use <code>State#merge</code>.</p>
|
519
|
+
<pre><code class="language-rb">state = State.new(foo: 1, bar: 2)
|
520
|
+
new_state = state.merge(bar: 3)
|
521
|
+
new_state.foo #=> 1
|
522
|
+
new_state.bar #=> 3
|
523
|
+
</code></pre>
|
524
|
+
<h2 id="nesting-state"><a class="header" href="#nesting-state">Nesting state</a></h2>
|
525
|
+
<p>For practical apps, you can nest State like this.</p>
|
526
|
+
<pre><code class="language-rb">class Book < Ovto::State
|
527
|
+
item :title
|
528
|
+
item :author
|
529
|
+
end
|
530
|
+
|
531
|
+
class State < Ovto::State
|
532
|
+
item :books, []
|
533
|
+
end
|
534
|
+
|
535
|
+
book = Book.new('Hello world', 'taro')
|
536
|
+
state = State.new(books: [book])
|
537
|
+
</code></pre>
|
538
|
+
<h2 id="defining-instance-methods-of-state"><a class="header" href="#defining-instance-methods-of-state">Defining instance methods of state</a></h2>
|
539
|
+
<p>You can define instance methods of state.</p>
|
540
|
+
<pre><code class="language-rb">class Book < Ovto::State
|
541
|
+
item :title
|
542
|
+
item :author
|
543
|
+
|
544
|
+
def to_text
|
545
|
+
"#{self.title} (#{self.author})"
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
book = Book.new('Hello world', 'taro')
|
550
|
+
book.to_text #=> "Hello world (taro)"
|
551
|
+
</code></pre>
|
552
|
+
<h2 id="defining-class-methods-of-state"><a class="header" href="#defining-class-methods-of-state">Defining class methods of state</a></h2>
|
553
|
+
<p>Ovto does not have a class like <code>StateList</code>. Just use Array to represent a list of state.</p>
|
554
|
+
<p>You can define class methods to manipulate a list of state.</p>
|
555
|
+
<pre><code class="language-rb">class Book < Ovto::State
|
556
|
+
item :title
|
557
|
+
item :author
|
558
|
+
|
559
|
+
def self.of_author(books, author)
|
560
|
+
books.select{|x| x.author == author}
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
# Example
|
565
|
+
taro_books = Book.of_author(books, "taro")
|
566
|
+
</code></pre>
|
567
|
+
<div style="break-before: page; page-break-before: always;"></div><h1 id="ovtoactions"><a class="header" href="#ovtoactions">Ovto::Actions</a></h1>
|
568
|
+
<p>Actions are the only way to change the state. Actions must be defined as methods of
|
569
|
+
the <code>Actions</code> class. Here is some more conventions:</p>
|
570
|
+
<ul>
|
571
|
+
<li>You must use keyword arguments</li>
|
572
|
+
<li>You must return state updates as a Hash. It will be merged into the app state.</li>
|
573
|
+
<li>You can get the current state by <code>state</code> method</li>
|
574
|
+
</ul>
|
575
|
+
<p>Example:</p>
|
576
|
+
<pre><code class="language-rb">require 'opal'
|
577
|
+
require 'ovto'
|
578
|
+
|
579
|
+
class MyApp < Ovto::App
|
580
|
+
class State < Ovto::State
|
581
|
+
item :count, default: 0
|
582
|
+
end
|
583
|
+
|
584
|
+
class Actions < Ovto::Actions
|
585
|
+
def increment(by:)
|
586
|
+
return {count: state.count + by}
|
587
|
+
end
|
588
|
+
end
|
589
|
+
|
590
|
+
class MainComponent < Ovto::Component
|
591
|
+
def render
|
592
|
+
o 'span', state.count
|
593
|
+
o 'button', onclick: ->{ actions.increment(by: 1) }
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
MyApp.run(id: 'ovto')
|
599
|
+
</code></pre>
|
600
|
+
<h2 id="calling-actions"><a class="header" href="#calling-actions">Calling actions</a></h2>
|
601
|
+
<p>Actions can be called from components via <code>actions</code> method. This is an instance of
|
602
|
+
<code>Ovto::WiredActions</code> and has methods of the same name as your <code>Actions</code> class.</p>
|
603
|
+
<pre><code> o 'button', onclick: ->{ actions.increment(by: 1) }
|
604
|
+
</code></pre>
|
605
|
+
<p>Arguments are almost the same as the original but you don't need to provide <code>state</code>;
|
606
|
+
it is automatically passed by <code>Ovto::WiredActions</code> class. It also updates the app
|
607
|
+
state with the return value of the action, and schedules rendering the view.</p>
|
608
|
+
<h2 id="skipping-state-update"><a class="header" href="#skipping-state-update">Skipping state update</a></h2>
|
609
|
+
<p>An action may return <code>nil</code> when no app state changes are needed.</p>
|
610
|
+
<p>Promises are also special values which does not cause state changes (see the next section).</p>
|
611
|
+
<h2 id="async-actions"><a class="header" href="#async-actions">Async actions</a></h2>
|
612
|
+
<p>When calling server apis, you cannot tell how the app state will change until the server responds.
|
613
|
+
In such cases, you can call another action via <code>actions</code> to tell Ovto to reflect the api result to the app state.</p>
|
614
|
+
<p>Example:</p>
|
615
|
+
<pre><code class="language-rb"> class Actions < Ovto::Actions
|
616
|
+
def fetch_tasks
|
617
|
+
Ovto.fetch('/tasks.json').then {|tasks_json|
|
618
|
+
actions.receive_tasks(tasks: tasks_json)
|
619
|
+
}.fail {|e|
|
620
|
+
console.log("failed:", e)
|
621
|
+
}
|
622
|
+
end
|
623
|
+
|
624
|
+
def receive_tasks(tasks_json:)
|
625
|
+
tasks = tasks_json.map{|item| Task.new(**item)}
|
626
|
+
return {tasks: tasks}
|
627
|
+
end
|
628
|
+
end
|
629
|
+
</code></pre>
|
630
|
+
<div style="break-before: page; page-break-before: always;"></div><h1 id="ovtocomponent"><a class="header" href="#ovtocomponent">Ovto::Component</a></h1>
|
631
|
+
<p>An Ovto app must have <code>MainComponent</code> class, a subclass of <code>Ovto::Component</code>.</p>
|
632
|
+
<h2 id="render-method"><a class="header" href="#render-method">'render' method</a></h2>
|
633
|
+
<p><code>render</code> is the only method you need to define in the <code>MainComponent</code> class.
|
634
|
+
You can get the global app state by calling <code>state</code> method.</p>
|
635
|
+
<pre><code class="language-rb"> class MainComponent < Ovto::Component
|
636
|
+
def render
|
637
|
+
o 'div' do
|
638
|
+
o 'h1', 'Your todos'
|
639
|
+
o 'ul' do
|
640
|
+
state.todos.each do |todo|
|
641
|
+
o 'li', todo.title
|
642
|
+
end
|
643
|
+
end
|
644
|
+
end
|
645
|
+
end
|
646
|
+
end
|
647
|
+
</code></pre>
|
648
|
+
<h3 id="morethanonenode-error"><a class="header" href="#morethanonenode-error">MoreThanOneNode error</a></h3>
|
649
|
+
<p>If you missed the surrounding 'div' tag, Ovto raises an <code>MoreThanOneNode</code> error. <code>render</code> must create a single DOM node.</p>
|
650
|
+
<pre><code class="language-rb"> def render
|
651
|
+
o 'h1', 'Your todos'
|
652
|
+
o 'ul' do
|
653
|
+
state.todos.each do |todo|
|
654
|
+
o 'li', todo.title
|
655
|
+
end
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
#=> $MoreThanOneNode {name: "MoreThanOneNode", ...}
|
660
|
+
</code></pre>
|
661
|
+
<h2 id="the-o-method"><a class="header" href="#the-o-method">The 'o' method</a></h2>
|
662
|
+
<a name='the-o-method' />
|
663
|
+
<p><code>Ovto::Component#o</code> describes your app's view. For example:</p>
|
664
|
+
<pre><code class="language-rb">o 'div'
|
665
|
+
#=> <div></div>
|
666
|
+
|
667
|
+
o 'div', 'Hello.'
|
668
|
+
#=> <div>Hello.</div>
|
669
|
+
</code></pre>
|
670
|
+
<p>You can pass attributes with a Hash.</p>
|
671
|
+
<pre><code class="language-rb">o 'div', class: 'main', 'Hello.'
|
672
|
+
#=> <div class='main'>Hello.</div>
|
673
|
+
</code></pre>
|
674
|
+
<p>There are shorthand notations for classes and ids.</p>
|
675
|
+
<pre><code class="language-rb">o 'div.main'
|
676
|
+
#=> <div class='main'>Hello.</div>
|
677
|
+
|
678
|
+
o 'div#main'
|
679
|
+
#=> <div id='main'>Hello.</div>
|
680
|
+
</code></pre>
|
681
|
+
<p>You can also give a block to specify its content.</p>
|
682
|
+
<pre><code class="language-rb">o 'div' do
|
683
|
+
'Hello.'
|
684
|
+
end
|
685
|
+
#=> <div>Hello.</div>
|
686
|
+
|
687
|
+
o 'div' do
|
688
|
+
o 'h1', 'Hello.'
|
689
|
+
end
|
690
|
+
#=> <div><h1>Hello.</h1></div>
|
691
|
+
</code></pre>
|
692
|
+
<h3 id="special-attribute-style"><a class="header" href="#special-attribute-style">Special attribute: <code>style</code></a></h3>
|
693
|
+
<a name='special-attributes' />
|
694
|
+
<p>There are some special keys for the attributes Hash. <code>style:</code> key takes a hash as
|
695
|
+
its value and specifies styles of the tag.</p>
|
696
|
+
<pre><code class="language-rb">o 'div', style: {color: 'red'}, 'Hello.'
|
697
|
+
#=> <div style='color: red;'>Hello.</div>
|
698
|
+
</code></pre>
|
699
|
+
<h3 id="special-attribute-onxx"><a class="header" href="#special-attribute-onxx">Special attribute: <code>onxx</code></a></h3>
|
700
|
+
<p>An attribute starts with <code>"on"</code> specifies an event handler.</p>
|
701
|
+
<p>For example:</p>
|
702
|
+
<pre><code class="language-rb">o 'input', {
|
703
|
+
type: 'button',
|
704
|
+
onclick: ->(e){ p e.target.value },
|
705
|
+
value: 'Hello.'
|
706
|
+
}
|
707
|
+
</code></pre>
|
708
|
+
<p>The argument <code>e</code> is an instance of Opal::Native and wraps the JavaScript event object.
|
709
|
+
You can get the input value with <code>e.target.value</code> here.</p>
|
710
|
+
<h4 id="lifecycle-events"><a class="header" href="#lifecycle-events">Lifecycle events</a></h4>
|
711
|
+
<p>There are special events <code>oncreate</code>, <code>onupdate</code>, <code>onremove</code>, <code>ondestroy</code>.</p>
|
712
|
+
<p>https://github.com/hyperapp/hyperapp#lifecycle-events</p>
|
713
|
+
<h3 id="special-attribute-key"><a class="header" href="#special-attribute-key">Special attribute: <code>key</code></a></h3>
|
714
|
+
<p>https://github.com/hyperapp/hyperapp#keys</p>
|
715
|
+
<h2 id="sub-components"><a class="header" href="#sub-components">Sub components</a></h2>
|
716
|
+
<p><code>o</code> can take another component class to render.</p>
|
717
|
+
<pre><code class="language-rb"> # Sub component
|
718
|
+
class TodoList < Ovto::Component
|
719
|
+
def render(todos:)
|
720
|
+
o 'ul' do
|
721
|
+
todos.each do |todo|
|
722
|
+
o 'li', todo.title
|
723
|
+
end
|
724
|
+
end
|
725
|
+
end
|
726
|
+
end
|
727
|
+
|
728
|
+
# Main component
|
729
|
+
class MainComponent < Ovto::Component
|
730
|
+
def render
|
731
|
+
o 'div' do
|
732
|
+
o 'h1', 'Your todos'
|
733
|
+
o TodoList, todos: state.todos
|
734
|
+
end
|
735
|
+
end
|
736
|
+
end
|
737
|
+
</code></pre>
|
738
|
+
<h2 id="text-node"><a class="header" href="#text-node">Text node</a></h2>
|
739
|
+
<p>Sometimes you may want to create a text node.</p>
|
740
|
+
<pre><code class="language-rb">#=> <div>Age: <span class='age'>12</a></div>
|
741
|
+
# ~~~~~
|
742
|
+
# |
|
743
|
+
# +--Raw text (not enclosed by an inner tag)
|
744
|
+
</code></pre>
|
745
|
+
<p><code>o</code> generates a text node when <code>'text'</code> is specified as tag name. The above
|
746
|
+
HTML could be described like this.</p>
|
747
|
+
<pre><code class="language-rb">o 'div' do
|
748
|
+
o 'text', 'Age:'
|
749
|
+
o 'span', '12'
|
750
|
+
end
|
751
|
+
</code></pre>
|
752
|
+
<div style="break-before: page; page-break-before: always;"></div><h1 id="ovtopurecomponent"><a class="header" href="#ovtopurecomponent">Ovto::PureComponent</a></h1>
|
753
|
+
<p>It almost the same as <code>Ovto::Component</code>, but it caches the <code>render</code> method calling with arguments of the method.</p>
|
754
|
+
<h2 id="when-to-use-purecomponent"><a class="header" href="#when-to-use-purecomponent">When to use PureComponent?</a></h2>
|
755
|
+
<p>Use it when your app is slow and need more speed.</p>
|
756
|
+
<h2 id="cache-strategy"><a class="header" href="#cache-strategy">Cache strategy</a></h2>
|
757
|
+
<p>It compares <code>render</code> method arguments and the previous arguments.</p>
|
758
|
+
<pre><code class="language-rb">def render
|
759
|
+
o 'div' do
|
760
|
+
o Pure, foo: state.foo
|
761
|
+
o NotPure bar: state.bar
|
762
|
+
end
|
763
|
+
end
|
764
|
+
</code></pre>
|
765
|
+
<p>In this case, <code>NotPure</code> component's render method is called even if <code>state.foo</code> is changed.
|
766
|
+
Whereas <code>Pure</code> component's render method is called only if <code>state.foo</code> is changed.</p>
|
767
|
+
<h2 id="state"><a class="header" href="#state">State</a></h2>
|
768
|
+
<p><code>state</code> method is not available in <code>PureComponent</code>, because <code>PureComponent</code> does not treat state as the cache key.
|
769
|
+
If you'd like to use state in <code>PureComponent</code>, pass the state from the parent component.</p>
|
770
|
+
<div style="break-before: page; page-break-before: always;"></div><h1 id="ovtomiddleware"><a class="header" href="#ovtomiddleware">Ovto::Middleware</a></h1>
|
771
|
+
<p>When you are making a big app with Ovto, you may want to extract
|
772
|
+
certain parts which are independent from the app. Ovto::Middleware is
|
773
|
+
for such cases.</p>
|
774
|
+
<ul>
|
775
|
+
<li>A middleware has its own namespace of state and actions
|
776
|
+
<ul>
|
777
|
+
<li>that is, you don't need to add prefixes to the names of states and actions of a middleware.</li>
|
778
|
+
</ul>
|
779
|
+
</li>
|
780
|
+
</ul>
|
781
|
+
<h2 id="example-2"><a class="header" href="#example-2">Example</a></h2>
|
782
|
+
<pre><code class="language-rb"># 1. Middleware name must be valid as a method name in Ruby
|
783
|
+
class MyMiddleware < Ovto::Middleware("my_middleware")
|
784
|
+
def setup
|
785
|
+
# Called on app startup
|
786
|
+
end
|
787
|
+
|
788
|
+
# 2. Make a subclass of MyMiddleware's State
|
789
|
+
class State < MyMiddleware::State
|
790
|
+
item :count, default: 0
|
791
|
+
end
|
792
|
+
|
793
|
+
# 3. Make a subclass of MyMiddleware's Actions
|
794
|
+
class Actions < MyMiddleware::Actions
|
795
|
+
def increment(by:)
|
796
|
+
return {count: state.count + by}
|
797
|
+
end
|
798
|
+
end
|
799
|
+
|
800
|
+
# 4. Make a subclass of MyMiddleware's Component
|
801
|
+
class SomeComponent < MyMiddleware::Component
|
802
|
+
def render
|
803
|
+
o 'div' do
|
804
|
+
o 'span', state.count
|
805
|
+
o 'button', onclick: ->{ actions.increment(by: 1) }
|
806
|
+
end
|
807
|
+
end
|
808
|
+
end
|
809
|
+
end
|
810
|
+
|
811
|
+
class MyApp < Ovto::App
|
812
|
+
# 5. Declare middlewares to use
|
813
|
+
use MyMiddleware
|
814
|
+
|
815
|
+
class State < Ovto::State; end
|
816
|
+
class Actions < Ovto::Actions; end
|
817
|
+
|
818
|
+
class MainComponent < Ovto::Component
|
819
|
+
def render
|
820
|
+
o 'div.counter' do
|
821
|
+
o MyMiddleware::SomeComponent
|
822
|
+
end
|
823
|
+
end
|
824
|
+
end
|
825
|
+
end
|
826
|
+
</code></pre>
|
827
|
+
<h2 id="advanced"><a class="header" href="#advanced">Advanced</a></h2>
|
828
|
+
<h3 id="getting-middlware-state-from-app"><a class="header" href="#getting-middlware-state-from-app">Getting middlware state from app</a></h3>
|
829
|
+
<pre><code class="language-rb">class MyApp < Ovto::App
|
830
|
+
def MainComponent < Ovto::Component
|
831
|
+
def render
|
832
|
+
o 'span', state._middlewares.middleware1.some_state
|
833
|
+
end
|
834
|
+
end
|
835
|
+
end
|
836
|
+
</code></pre>
|
837
|
+
<h3 id="calling-middlware-action-from-app"><a class="header" href="#calling-middlware-action-from-app">Calling middlware action from app</a></h3>
|
838
|
+
<pre><code class="language-rb">class MyApp < Ovto::App
|
839
|
+
# From actions
|
840
|
+
def Actions < Ovto::Actions
|
841
|
+
def some_action
|
842
|
+
actions.middleware1.some_action()
|
843
|
+
end
|
844
|
+
end
|
845
|
+
|
846
|
+
# From component
|
847
|
+
def MainComponent < Ovto::Component
|
848
|
+
def render
|
849
|
+
o 'button', onclick: ->{ actions.middleware1.some_action() }
|
850
|
+
end
|
851
|
+
end
|
852
|
+
end
|
853
|
+
</code></pre>
|
854
|
+
<h3 id="using-a-middleware-from-another-middleware"><a class="header" href="#using-a-middleware-from-another-middleware">Using a middleware from another middleware</a></h3>
|
855
|
+
<pre><code class="language-rb">class Middleware1 < Ovto::Middleware("middleware1")
|
856
|
+
use Middleware2
|
857
|
+
end
|
858
|
+
</code></pre>
|
859
|
+
<div style="break-before: page; page-break-before: always;"></div><h1 id="ovtofetch"><a class="header" href="#ovtofetch">Ovto.fetch</a></h1>
|
860
|
+
<p>Ovto provides wrapper of <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch">Fetch API</a>, for convenience of calling typical server-side APIs (eg. those generated by <code>rails scaffold</code> command.)</p>
|
861
|
+
<p><code>Ovto.fetch</code> returns Opal's Promise object that calls the API with the specified parameters.</p>
|
862
|
+
<h2 id="examples"><a class="header" href="#examples">Examples</a></h2>
|
863
|
+
<p>GET</p>
|
864
|
+
<pre><code class="language-rb">Ovto.fetch('/api/tasks').then{|json_data|
|
865
|
+
p json_data
|
866
|
+
}.fail{|e| # Network error, 404 Not Found, JSON parse error, etc.
|
867
|
+
p e
|
868
|
+
}
|
869
|
+
</code></pre>
|
870
|
+
<p>POST</p>
|
871
|
+
<pre><code class="language-rb">Ovto.fetch('/api/new_task', 'POST', {title: "do something"}).then{|json_data|
|
872
|
+
p json_data
|
873
|
+
}.fail{|e| # Network error, 404 Not Found, JSON parse error, etc.
|
874
|
+
p e
|
875
|
+
}
|
876
|
+
|
877
|
+
</code></pre>
|
878
|
+
<p>PUT</p>
|
879
|
+
<pre><code class="language-rb">Ovto.fetch('/api/tasks/1', 'PUT', {title: "do something"}).then{|json_data|
|
880
|
+
p json_data
|
881
|
+
}.fail{|e| # Network error, 404 Not Found, JSON parse error, etc.
|
882
|
+
p e
|
883
|
+
}
|
884
|
+
</code></pre>
|
885
|
+
<h2 id="csrf-tokens"><a class="header" href="#csrf-tokens">CSRF tokens</a></h2>
|
886
|
+
<p>You don't need to care about CSRF tokens if the server is a Rails app because <code>Ovto.fetch</code> automatically send <code>X-CSRF-Token</code> header if the page contains a meta tag like <code><meta name='csrf-token' content='....' /></code>.</p>
|
887
|
+
<div style="break-before: page; page-break-before: always;"></div><h1 id="debugging-ovto-app"><a class="header" href="#debugging-ovto-app">Debugging Ovto app</a></h1>
|
888
|
+
<h2 id="consolelog"><a class="header" href="#consolelog">console.log</a></h2>
|
889
|
+
<p>In an Ovto app, you can print any object to developer console by <code>console.log</code>
|
890
|
+
like in JavaScript. </p>
|
891
|
+
<pre><code class="language-rb">console.log(state: State.new)
|
892
|
+
</code></pre>
|
893
|
+
<p>This is mostly equal to <code>p state: State.new</code> but <code>console.log</code> supports
|
894
|
+
JavaScript objects too.</p>
|
895
|
+
<p>(Note: this is not an official feature of Opal. You can do this setup by this:)</p>
|
896
|
+
<pre><code class="language-rb"> require 'console'; def console; $console; end
|
897
|
+
</code></pre>
|
898
|
+
<h2 id="ovto-debug"><a class="header" href="#ovto-debug">ovto-debug</a></h2>
|
899
|
+
<p>If the page has a tag with <code>id='ovto-debug'</code>, exception is shown in the tag.</p>
|
900
|
+
<h2 id="ovtodebug_trace"><a class="header" href="#ovtodebug_trace">Ovto.debug_trace</a></h2>
|
901
|
+
<p>If <code>Ovto.debug_trace</code> is set to <code>true</code>, some diagnostic messages are shown in the browser console.</p>
|
902
|
+
<pre><code class="language-rb">Ovto.debug_trace = true
|
903
|
+
MyApp.run(id: 'ovto')
|
904
|
+
</code></pre>
|
905
|
+
<div style="break-before: page; page-break-before: always;"></div><h1 id="development-notes"><a class="header" href="#development-notes">Development notes</a></h1>
|
906
|
+
<h2 id="how-to-run-unit-test"><a class="header" href="#how-to-run-unit-test">How to run unit test</a></h2>
|
907
|
+
<ol>
|
908
|
+
<li>git clone</li>
|
909
|
+
<li>bundle install</li>
|
910
|
+
<li>bundle exec rake</li>
|
911
|
+
</ol>
|
912
|
+
<h2 id="how-to-rebuild-docs"><a class="header" href="#how-to-rebuild-docs">How to rebuild docs</a></h2>
|
913
|
+
<p><code>bundle exec doc:build</code></p>
|
914
|
+
|
915
|
+
</main>
|
916
|
+
|
917
|
+
<nav class="nav-wrapper" aria-label="Page navigation">
|
918
|
+
<!-- Mobile navigation buttons -->
|
919
|
+
|
920
|
+
|
921
|
+
<div style="clear: both"></div>
|
922
|
+
</nav>
|
923
|
+
</div>
|
924
|
+
</div>
|
925
|
+
|
926
|
+
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
927
|
+
|
928
|
+
</nav>
|
929
|
+
|
930
|
+
</div>
|
931
|
+
|
932
|
+
|
933
|
+
|
934
|
+
|
935
|
+
<script>
|
936
|
+
window.playground_copyable = true;
|
937
|
+
</script>
|
938
|
+
|
939
|
+
|
940
|
+
<script src="elasticlunr.min.js"></script>
|
941
|
+
<script src="mark.min.js"></script>
|
942
|
+
<script src="searcher.js"></script>
|
943
|
+
|
944
|
+
<script src="clipboard.min.js"></script>
|
945
|
+
<script src="highlight.js"></script>
|
946
|
+
<script src="book.js"></script>
|
947
|
+
|
948
|
+
<!-- Custom JS scripts -->
|
949
|
+
|
950
|
+
<script>
|
951
|
+
window.addEventListener('load', function() {
|
952
|
+
window.setTimeout(window.print, 100);
|
953
|
+
});
|
954
|
+
</script>
|
955
|
+
|
956
|
+
</div>
|
957
|
+
</body>
|
958
|
+
</html>
|