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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/Gemfile.lock +15 -16
  4. data/Rakefile +2 -9
  5. data/book/SUMMARY.md +15 -11
  6. data/book/book.toml +10 -0
  7. data/docs/.nojekyll +1 -0
  8. data/docs/404.html +189 -0
  9. data/docs/FontAwesome/css/font-awesome.css +4 -0
  10. data/docs/FontAwesome/fonts/FontAwesome.ttf +0 -0
  11. data/docs/FontAwesome/fonts/fontawesome-webfont.eot +0 -0
  12. data/docs/FontAwesome/fonts/fontawesome-webfont.svg +2671 -0
  13. data/docs/{gitbook/fonts/fontawesome → FontAwesome/fonts}/fontawesome-webfont.ttf +0 -0
  14. data/docs/FontAwesome/fonts/fontawesome-webfont.woff +0 -0
  15. data/docs/FontAwesome/fonts/fontawesome-webfont.woff2 +0 -0
  16. data/docs/api/Array.html +4 -4
  17. data/docs/api/Hash.html +4 -4
  18. data/docs/api/MightyInspect.html +238 -0
  19. data/docs/api/Ovto/Actions.html +4 -4
  20. data/docs/api/Ovto/App.html +4 -4
  21. data/docs/api/Ovto/Component/MoreThanOneNode.html +5 -5
  22. data/docs/api/Ovto/Component.html +4 -4
  23. data/docs/api/Ovto/Middleware/Actions.html +4 -4
  24. data/docs/api/Ovto/Middleware/Base.html +5 -5
  25. data/docs/api/Ovto/Middleware/Component.html +5 -5
  26. data/docs/api/Ovto/Middleware.html +8 -8
  27. data/docs/api/Ovto/PureComponent/StateIsNotAvailable.html +4 -4
  28. data/docs/api/Ovto/PureComponent.html +4 -4
  29. data/docs/api/Ovto/Runtime.html +4 -4
  30. data/docs/api/Ovto/State/MissingValue.html +4 -4
  31. data/docs/api/Ovto/State/UnknownStateKey.html +4 -4
  32. data/docs/api/Ovto/State.html +12 -12
  33. data/docs/api/Ovto/WiredActionSet.html +4 -4
  34. data/docs/api/Ovto/WiredActions.html +4 -4
  35. data/docs/api/Ovto.html +8 -8
  36. data/docs/api/_index.html +5 -5
  37. data/docs/api/actions.html +215 -426
  38. data/docs/api/app.html +226 -432
  39. data/docs/api/component.html +268 -480
  40. data/docs/api/fetch.html +188 -393
  41. data/docs/api/file.README.html +4 -4
  42. data/docs/api/frames.html +1 -1
  43. data/docs/api/index.html +4 -4
  44. data/docs/api/middleware.html +249 -460
  45. data/docs/api/pure_component.html +186 -398
  46. data/docs/api/state.html +226 -438
  47. data/docs/api/top-level-namespace.html +4 -4
  48. data/docs/ayu-highlight.css +78 -0
  49. data/docs/book.js +688 -0
  50. data/docs/book.toml +10 -0
  51. data/docs/clipboard.min.js +7 -0
  52. data/docs/css/chrome.css +545 -0
  53. data/docs/css/general.css +203 -0
  54. data/docs/css/print.css +54 -0
  55. data/docs/css/variables.css +255 -0
  56. data/docs/elasticlunr.min.js +10 -0
  57. data/docs/favicon.png +0 -0
  58. data/docs/favicon.svg +22 -0
  59. data/docs/fonts/OPEN-SANS-LICENSE.txt +202 -0
  60. data/docs/fonts/SOURCE-CODE-PRO-LICENSE.txt +93 -0
  61. data/docs/fonts/fonts.css +100 -0
  62. data/docs/fonts/open-sans-v17-all-charsets-300.woff2 +0 -0
  63. data/docs/fonts/open-sans-v17-all-charsets-300italic.woff2 +0 -0
  64. data/docs/fonts/open-sans-v17-all-charsets-600.woff2 +0 -0
  65. data/docs/fonts/open-sans-v17-all-charsets-600italic.woff2 +0 -0
  66. data/docs/fonts/open-sans-v17-all-charsets-700.woff2 +0 -0
  67. data/docs/fonts/open-sans-v17-all-charsets-700italic.woff2 +0 -0
  68. data/docs/fonts/open-sans-v17-all-charsets-800.woff2 +0 -0
  69. data/docs/fonts/open-sans-v17-all-charsets-800italic.woff2 +0 -0
  70. data/docs/fonts/open-sans-v17-all-charsets-italic.woff2 +0 -0
  71. data/docs/fonts/open-sans-v17-all-charsets-regular.woff2 +0 -0
  72. data/docs/fonts/source-code-pro-v11-all-charsets-500.woff2 +0 -0
  73. data/docs/guides/debugging.html +184 -390
  74. data/docs/guides/development.html +171 -383
  75. data/docs/guides/install.html +206 -409
  76. data/docs/guides/tutorial.html +309 -525
  77. data/docs/highlight.css +82 -0
  78. data/docs/highlight.js +6 -0
  79. data/docs/index.html +390 -391
  80. data/docs/mark.min.js +7 -0
  81. data/docs/print.html +958 -0
  82. data/docs/searcher.js +483 -0
  83. data/docs/searchindex.js +1 -0
  84. data/docs/searchindex.json +1 -0
  85. data/docs/tomorrow-night.css +102 -0
  86. data/examples/sinatra/Gemfile.lock +25 -25
  87. data/examples/static/Gemfile.lock +8 -8
  88. data/lib/ovto/component.rb +2 -3
  89. data/lib/ovto/version.rb +1 -1
  90. metadata +47 -26
  91. data/docs/gitbook/fonts/fontawesome/FontAwesome.otf +0 -0
  92. data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.eot +0 -0
  93. data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.svg +0 -685
  94. data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff +0 -0
  95. data/docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff2 +0 -0
  96. data/docs/gitbook/gitbook-plugin-fontsettings/fontsettings.js +0 -240
  97. data/docs/gitbook/gitbook-plugin-fontsettings/website.css +0 -291
  98. data/docs/gitbook/gitbook-plugin-highlight/ebook.css +0 -135
  99. data/docs/gitbook/gitbook-plugin-highlight/website.css +0 -434
  100. data/docs/gitbook/gitbook-plugin-lunr/lunr.min.js +0 -7
  101. data/docs/gitbook/gitbook-plugin-lunr/search-lunr.js +0 -59
  102. data/docs/gitbook/gitbook-plugin-search/lunr.min.js +0 -7
  103. data/docs/gitbook/gitbook-plugin-search/search-engine.js +0 -50
  104. data/docs/gitbook/gitbook-plugin-search/search.css +0 -35
  105. data/docs/gitbook/gitbook-plugin-search/search.js +0 -213
  106. data/docs/gitbook/gitbook-plugin-sharing/buttons.js +0 -90
  107. data/docs/gitbook/gitbook.js +0 -4
  108. data/docs/gitbook/images/apple-touch-icon-precomposed-152.png +0 -0
  109. data/docs/gitbook/images/favicon.ico +0 -0
  110. data/docs/gitbook/style.css +0 -9
  111. data/docs/gitbook/theme.js +0 -4
  112. 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 &lt; Ovto::App
155
+ class State &lt; 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 &lt; 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 &lt; Ovto::Component
175
+ def render
176
+ o 'div' do
177
+ o 'span', 'Celcius:'
178
+ o 'input', {
179
+ type: 'text',
180
+ onchange: -&gt;(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: -&gt;(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 &quot;https://rubygems.org&quot;
205
+ gem &quot;ovto&quot;, 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">&lt;!doctype html&gt;
212
+ &lt;html&gt;
213
+ &lt;head&gt;
214
+ &lt;meta charset=&quot;utf-8&quot;&gt;
215
+ &lt;script type='text/javascript' src='app.js'&gt;&lt;/script&gt;
216
+ &lt;/head&gt;
217
+ &lt;body&gt;
218
+ &lt;div id='ovto'&gt;&lt;/div&gt;
219
+ &lt;div id='ovto-debug'&gt;&lt;/div&gt;
220
+ &lt;/body&gt;
221
+ &lt;/html&gt;
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 &lt; Ovto::App
228
+ class State &lt; Ovto::State
229
+ end
230
+
231
+ class Actions &lt; Ovto::Actions
232
+ end
233
+
234
+ class MainComponent &lt; Ovto::Component
235
+ def render # Don't miss the `:`. This is not a typo but
236
+ o 'div' do # a &quot;mandatory keyword argument&quot;.
237
+ o 'h1', &quot;HELLO&quot; # 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 &gt; 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: &quot;State&quot;, message: &quot;uninitialized constant MyApp::State&quot;, stack: &quot;State: uninitialized constant MyApp::State&quot;}
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 &quot;ifchanged&quot;</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 &gt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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: -&gt;(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: -&gt;(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 &lt; Ovto::App
399
+ class State &lt; Ovto::State
400
+ end
401
+
402
+ class Actions &lt; Ovto::Actions
403
+ end
404
+
405
+ class MainComponent &lt; Ovto::Component
406
+ def render(state:)
407
+ o 'h1', &quot;HELLO&quot;
408
+ end
409
+ end
410
+ end
411
+ </code></pre>
412
+ <p>Edit <code>app/views/&lt;some controller/&lt;some view&gt;.html.erb</code></p>
413
+ <pre><code>&lt;div id='foo-app'&gt;&lt;/div&gt;
414
+ &lt;%= opal_tag do %&gt;
415
+ Foo.run(id: 'foo-app')
416
+ &lt;% end %&gt;
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 &lt; Ovto::App
432
+ class State &lt; Ovto::State
433
+ end
434
+
435
+ class Actions &lt; Ovto::Actions
436
+ end
437
+
438
+ class MainComponent &lt; 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 &lt; Ovto::App
452
+ COLORS = [&quot;red&quot;, &quot;blue&quot;, &quot;green&quot;]
453
+
454
+ class State &lt; Ovto::State
455
+ item :color_idx, default: 0
456
+ end
457
+
458
+ class Actions &lt; 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 &lt; Ovto::Component
466
+ def render
467
+ o 'input', {
468
+ type: 'button',
469
+ value: 'Hello',
470
+ style: {background: COLORS[state.color_idx]},
471
+ onclick: -&gt;{ 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 &lt; 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 &lt; Ovto::State
499
+ item :foo
500
+ item :bar
501
+ end
502
+
503
+ state = State.new(foo: 1, bar: 2)
504
+ state.foo #=&gt; 1
505
+ state.bar #=&gt; 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 &lt; Ovto::State
509
+ item :foo, default: 1
510
+ item :bar, default: 2
511
+ end
512
+
513
+ state = State.new
514
+ state.foo #=&gt; 1
515
+ state.bar #=&gt; 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 #=&gt; 1
522
+ new_state.bar #=&gt; 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 &lt; Ovto::State
527
+ item :title
528
+ item :author
529
+ end
530
+
531
+ class State &lt; 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 &lt; Ovto::State
541
+ item :title
542
+ item :author
543
+
544
+ def to_text
545
+ &quot;#{self.title} (#{self.author})&quot;
546
+ end
547
+ end
548
+
549
+ book = Book.new('Hello world', 'taro')
550
+ book.to_text #=&gt; &quot;Hello world (taro)&quot;
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 &lt; 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, &quot;taro&quot;)
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 &lt; Ovto::App
580
+ class State &lt; Ovto::State
581
+ item :count, default: 0
582
+ end
583
+
584
+ class Actions &lt; Ovto::Actions
585
+ def increment(by:)
586
+ return {count: state.count + by}
587
+ end
588
+ end
589
+
590
+ class MainComponent &lt; Ovto::Component
591
+ def render
592
+ o 'span', state.count
593
+ o 'button', onclick: -&gt;{ 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: -&gt;{ 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 &lt; 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(&quot;failed:&quot;, 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 &lt; 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
+ #=&gt; $MoreThanOneNode {name: &quot;MoreThanOneNode&quot;, ...}
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
+ #=&gt; &lt;div&gt;&lt;/div&gt;
666
+
667
+ o 'div', 'Hello.'
668
+ #=&gt; &lt;div&gt;Hello.&lt;/div&gt;
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
+ #=&gt; &lt;div class='main'&gt;Hello.&lt;/div&gt;
673
+ </code></pre>
674
+ <p>There are shorthand notations for classes and ids.</p>
675
+ <pre><code class="language-rb">o 'div.main'
676
+ #=&gt; &lt;div class='main'&gt;Hello.&lt;/div&gt;
677
+
678
+ o 'div#main'
679
+ #=&gt; &lt;div id='main'&gt;Hello.&lt;/div&gt;
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
+ #=&gt; &lt;div&gt;Hello.&lt;/div&gt;
686
+
687
+ o 'div' do
688
+ o 'h1', 'Hello.'
689
+ end
690
+ #=&gt; &lt;div&gt;&lt;h1&gt;Hello.&lt;/h1&gt;&lt;/div&gt;
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
+ #=&gt; &lt;div style='color: red;'&gt;Hello.&lt;/div&gt;
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>&quot;on&quot;</code> specifies an event handler.</p>
701
+ <p>For example:</p>
702
+ <pre><code class="language-rb">o 'input', {
703
+ type: 'button',
704
+ onclick: -&gt;(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 &lt; 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 &lt; 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">#=&gt; &lt;div&gt;Age: &lt;span class='age'&gt;12&lt;/a&gt;&lt;/div&gt;
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 &lt; Ovto::Middleware(&quot;my_middleware&quot;)
784
+ def setup
785
+ # Called on app startup
786
+ end
787
+
788
+ # 2. Make a subclass of MyMiddleware's State
789
+ class State &lt; MyMiddleware::State
790
+ item :count, default: 0
791
+ end
792
+
793
+ # 3. Make a subclass of MyMiddleware's Actions
794
+ class Actions &lt; 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 &lt; MyMiddleware::Component
802
+ def render
803
+ o 'div' do
804
+ o 'span', state.count
805
+ o 'button', onclick: -&gt;{ actions.increment(by: 1) }
806
+ end
807
+ end
808
+ end
809
+ end
810
+
811
+ class MyApp &lt; Ovto::App
812
+ # 5. Declare middlewares to use
813
+ use MyMiddleware
814
+
815
+ class State &lt; Ovto::State; end
816
+ class Actions &lt; Ovto::Actions; end
817
+
818
+ class MainComponent &lt; 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 &lt; Ovto::App
830
+ def MainComponent &lt; 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 &lt; Ovto::App
839
+ # From actions
840
+ def Actions &lt; Ovto::Actions
841
+ def some_action
842
+ actions.middleware1.some_action()
843
+ end
844
+ end
845
+
846
+ # From component
847
+ def MainComponent &lt; Ovto::Component
848
+ def render
849
+ o 'button', onclick: -&gt;{ 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 &lt; Ovto::Middleware(&quot;middleware1&quot;)
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: &quot;do something&quot;}).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: &quot;do something&quot;}).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>&lt;meta name='csrf-token' content='....' /&gt;</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>