-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.rs
276 lines (264 loc) · 9.69 KB
/
main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
use std::collections::BTreeMap;
use ravel::{adapt_ref, with, with_local};
use ravel_web::{
View, any, attr,
collections::{btree_map, iter},
el,
event::{self, on, on_},
format_text,
run::spawn_body,
text::{display, text},
};
use web_sys::{
HtmlInputElement,
wasm_bindgen::{JsCast as _, UnwrapThrowExt},
};
/// Our model type contains the global state of the application.
#[derive(Default)]
struct Model {
count: usize,
message: String,
item_map: BTreeMap<usize, String>,
item_vec: Vec<(usize, String)>,
}
/// We can build our application modularly out of components. A component is
/// just some function which returns a [`View!`].
///
/// ([`View!`] is just a convenience macro for a slightly verbose constraint on
/// [`trait@View`].)
fn basic_html() -> View!(Model) {
// We can build views out of HTML elements, which are defined in the
// [`el`] module. These take another view parameter for their body.
//
// We can also compose views using tuples.
el::header((
el::h1(
// To produce text, we can directly use a [`&'static str`].
"Ravel tutorial",
),
el::p((
"This is a basic introduction to ",
el::a((
"Ravel",
// Likewise, HTML attributes defined in the [`attr`] module take
// their value (typically a string) as a parameter.
attr::Href("https://github.com/kmicklas/ravel"),
)),
".",
)),
))
}
/// Components can take data in parameters, which can be borrowed from shared
/// state such as our [`Model`].
fn state(model: &Model) -> View!(Model) {
(
el::h2("State"),
el::p(
// To generate strings dynamically, we can use standard format
// strings using [`format_text`].
format_text!("Count: {}", model.count),
),
el::p((
"Also count: ",
// In the very common case of just displaying a scalar value like a
// number, it is easier and more efficient to use [`display`].
display(model.count),
)),
el::p((
"Also count: ",
// In fact, if the value is a standard number type, then we can just
// use it directly.
model.count,
)),
el::p((
"Message: ",
// Previously, we only generated static strings, which can be used
// directly. This is also possible for a by-value [`String`].
//
// However, for any other string-like type, we need to use [`text`].
text(&model.message),
)),
)
}
/// So far, we've only read data from our model, but have not changed it.
/// Typically, we want to do this in response to events. Now we get to the
/// [`Model`] parameter to [`View!`]. Event handlers have mutable access to this
/// type when they run.
fn events() -> View!(Model) {
(
el::h2("Events"),
el::p(el::button((
"Increment count",
// We can update the model in response to a chosen HTML event type.
on_(event::Click, |model: &mut Model| {
model.item_map.insert(model.count, model.message.clone());
model.item_vec.push((model.count, model.message.clone()));
model.count += 1;
}),
))),
el::p((
"Message: ",
// [`on`], unlike [`on_`], also gives us access to the underlying
// [`web_sys::Event`].
el::input(on(event::InputEvent, |model: &mut Model, event| {
model.message = event
.target()
.unwrap_throw()
.dyn_into::<HtmlInputElement>()
.unwrap_throw()
.value();
})),
)),
)
}
/// Sometimes, we might not want to store all state in our global [`Model`].
///
/// This is useful when it would be tedious to write down uninteresting state
/// types, or when you want to encapsulate the behavior of a reusable component.
/// However, it generally increases complexity and makes testing harder.
fn local_state() -> View!(Model) {
with_local(
// We provide an initialization callback, which is only run when the
// component is constructed for the first time.
|| 0,
// Inside the body, we have a reference to the current local state.
// Because we now have access to both the global and local state types
// we need to produce a `View!((Model, usize))`.
|cx, local_count| {
/// Displays the value. Since this returns `View!(Model)` (like any
/// other component in our application), we can no longer call it
/// directly here.
fn display_counter(count: usize) -> View!(Model) {
el::p(("Local count: ", count))
}
cx.build((
el::h2("Local state"),
// To use [`display_counter`], we need to "adapt" it to the
// correct type.
adapt_ref(display_counter(*local_count), |(model, _)| model),
el::p(el::button((
"Increment local count",
// Although we have a reference to the current value, we
// cannot mutate it, or store it in an event handler (which
// must remain `'static`).
//
// Instead, [`with_local`] changes our state type to be a
// tuple which has both the outer state ([`Model`]) and our
// local state type.
on_(event::Click, move |(_model, local_count): &mut _| {
*local_count += 1;
}),
))),
))
},
)
}
/// All of our views so far have had a static structure. Sometimes, we need to
/// swap out or hide various components.
fn dynamic_view(model: &Model) -> View!(Model) {
(
el::h2("Dynamic view"),
// In the general case, we need the following pattern to dynamically
// select a component type:
//
// * Use [`with`] with a closure taking our context `cx`.
// * Branch according to our chosen logic.
// * In each branch, return `cx.build(any(...))` for our chosen view.
el::p(with(|cx| {
if model.count % 2 == 0 {
cx.build(any(el::b("Even!")))
} else {
cx.build(any("Odd."))
}
})),
// One very common case of a dynamic component is one which is simply
// present or not. In this case, it is simpler and more efficient to
// just wrap it in an [`Option`].
model
.count
.is_power_of_two()
.then(|| el::p("Power of two!")),
)
}
/// Any non-trivial application will have some dynamically sized list data.
///
/// With a similar structure to our use of [`with`] above, we can generate a
/// [`trait@View`] over a [`BTreeMap`] with [`btree_map()`]. This is useful when
/// the entries are ordered by some type of key, but may be inserted or removed
/// at any position.
///
/// If the data is just an array which grows or shrinks at the end, we can use
/// [`iter()`] to generate a [`trait@View`] over any iterator.
fn lists(model: &Model) -> View!(Model) {
(
el::h2("Map view"),
el::p(el::table((
el::thead(el::tr((el::td(()), el::td("Id"), el::td("Message")))),
el::tbody(btree_map(&model.item_map, |cx, key, value| {
let key = *key;
cx.build(el::tr((
el::td(el::button((
"Remove",
on_(event::Click, {
move |model: &mut Model| {
model.item_map.remove(&key);
}
}),
))),
el::td(format_text!("{}", key)),
el::td(text(value)),
)))
})),
))),
el::h2("Iterator view"),
el::p(el::table((
el::thead(el::tr((el::td(()), el::td("Id"), el::td("Message")))),
el::tbody(iter(&model.item_vec, |cx, i, (key, value)| {
let key = *key;
cx.build(el::tr((
el::td(el::button((
"Truncate",
on_(event::Click, {
move |model: &mut Model| {
model.item_vec.truncate(i);
}
}),
))),
el::td(format_text!("{}", key)),
el::td(text(value)),
)))
})),
))),
)
}
/// Putting it all together...
fn tutorial(model: &Model) -> View!(Model) {
(
basic_html(),
state(model),
events(),
local_state(),
dynamic_view(model),
lists(model),
)
}
fn main() {
// Dump any Rust panics to the browser console.
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
// Direct [`log`] lines to the browser console.
console_log::init_with_level(log::Level::Trace).unwrap();
spawn_body(
// Our initial model state:
Model {
count: 0,
message: String::new(),
item_map: BTreeMap::new(),
item_vec: Vec::new(),
},
// Here we could, for example, synchronize the model to an external
// data store.
|_model| (),
// We need to use the `cx.build(...)` pattern similar to [`with`].
|cx, model| cx.build(tutorial(model)),
);
}