ovto 0.6.2 → 0.7.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 +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>
|