mirror of
https://github.com/rustwasm/wasm-bindgen.git
synced 2024-12-27 03:55:20 +03:00
Adding in TODO MVC example using web-sys
This commit is contained in:
parent
cb170ef94f
commit
b322f46303
@ -73,6 +73,7 @@ members = [
|
||||
"examples/paint",
|
||||
"examples/performance",
|
||||
"examples/raytrace-parallel",
|
||||
"examples/todomvc",
|
||||
"examples/wasm-in-wasm",
|
||||
"examples/wasm2js",
|
||||
"examples/webaudio",
|
||||
|
3
examples/todomvc/.gitignore
vendored
Normal file
3
examples/todomvc/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
target/
|
||||
todomvc.js
|
||||
*.swp
|
41
examples/todomvc/Cargo.toml
Normal file
41
examples/todomvc/Cargo.toml
Normal file
@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "todomvc"
|
||||
version = "0.1.0"
|
||||
authors = ["The wasm-bindgen Developers"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 's'
|
||||
|
||||
[build-dependencies]
|
||||
askama = "0.7.2"
|
||||
|
||||
[dependencies]
|
||||
js-sys = { path = "../../crates/js-sys" }
|
||||
wasm-bindgen = { path = "../../" }
|
||||
askama = "0.7.2"
|
||||
|
||||
[dependencies.web-sys]
|
||||
path = "../../crates/web-sys"
|
||||
features = [
|
||||
'console',
|
||||
'CssStyleDeclaration',
|
||||
'Document',
|
||||
'DomStringMap',
|
||||
'DomTokenList',
|
||||
'Element',
|
||||
'Event',
|
||||
'EventTarget',
|
||||
'HtmlBodyElement',
|
||||
'HtmlElement',
|
||||
'HtmlInputElement',
|
||||
'KeyboardEvent',
|
||||
'Location',
|
||||
'Node',
|
||||
'NodeList',
|
||||
'Storage',
|
||||
'Window',
|
||||
]
|
17
examples/todomvc/README.md
Normal file
17
examples/todomvc/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# TODO MVC
|
||||
|
||||
[View documentation for this example online][dox] or [View compiled example
|
||||
online][compiled]
|
||||
|
||||
[compiled]: https://rustwasm.github.io/wasm-bindgen/exbuild/todomvc/
|
||||
[dox]: https://rustwasm.github.io/wasm-bindgen/examples/todomvc.html
|
||||
|
||||
You can build the example locally with:
|
||||
|
||||
```
|
||||
$ ./build.sh
|
||||
```
|
||||
|
||||
(or running the commands on Windows manually)
|
||||
|
||||
and then visiting http://localhost:8080 in a browser should run the example!
|
15
examples/todomvc/build.sh
Executable file
15
examples/todomvc/build.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
|
||||
# For more comments about what's going on here, see the `hello_world` example
|
||||
|
||||
set -ex
|
||||
cd "$(dirname $0)"
|
||||
|
||||
cargo +nightly build --target wasm32-unknown-unknown
|
||||
|
||||
cargo +nightly run --manifest-path ../../crates/cli/Cargo.toml \
|
||||
--bin wasm-bindgen -- \
|
||||
../../target/wasm32-unknown-unknown/debug/todomvc.wasm --out-dir .
|
||||
|
||||
npm install
|
||||
npm run serve
|
379
examples/todomvc/index.css
Normal file
379
examples/todomvc/index.css
Normal file
@ -0,0 +1,379 @@
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4em;
|
||||
background: #f5f5f5;
|
||||
color: #4d4d4d;
|
||||
min-width: 230px;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todoapp {
|
||||
background: #fff;
|
||||
margin: 130px 0 40px 0;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
|
||||
0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.todoapp input::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::-moz-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
position: absolute;
|
||||
top: -155px;
|
||||
width: 100%;
|
||||
font-size: 100px;
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
color: rgba(175, 47, 47, 0.15);
|
||||
-webkit-text-rendering: optimizeLegibility;
|
||||
-moz-text-rendering: optimizeLegibility;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.new-todo,
|
||||
.edit {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 1.4em;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
padding: 6px;
|
||||
border: 1px solid #999;
|
||||
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.new-todo {
|
||||
padding: 16px 16px 16px 60px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border: none; /* Mobile Safari */
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.toggle-all + label {
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
font-size: 0;
|
||||
position: absolute;
|
||||
top: -52px;
|
||||
left: -13px;
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.toggle-all + label:before {
|
||||
content: '❯';
|
||||
font-size: 22px;
|
||||
color: #e6e6e6;
|
||||
padding: 10px 27px 10px 27px;
|
||||
}
|
||||
|
||||
.toggle-all:checked + label:before {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.todo-list li {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.todo-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-list li.editing .edit {
|
||||
display: block;
|
||||
width: 506px;
|
||||
padding: 12px 16px;
|
||||
margin: 0 0 0 43px;
|
||||
}
|
||||
|
||||
.todo-list li.editing .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
border: none; /* Mobile Safari */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.todo-list li .toggle + label {
|
||||
/*
|
||||
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
|
||||
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
|
||||
*/
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
.todo-list li .toggle:checked + label {
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
|
||||
}
|
||||
|
||||
.todo-list li label {
|
||||
word-break: break-all;
|
||||
padding: 15px 15px 15px 60px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
transition: color 0.4s;
|
||||
}
|
||||
|
||||
.todo-list li.completed label {
|
||||
color: #d9d9d9;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todo-list li .destroy {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto 0;
|
||||
font-size: 30px;
|
||||
color: #cc9a9a;
|
||||
margin-bottom: 11px;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:hover {
|
||||
color: #af5b5e;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:after {
|
||||
content: '×';
|
||||
}
|
||||
|
||||
.todo-list li:hover .destroy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-list li .edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: #777;
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.footer:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
|
||||
0 8px 0 -3px #f6f6f6,
|
||||
0 9px 1px -3px rgba(0, 0, 0, 0.2),
|
||||
0 16px 0 -6px #f6f6f6,
|
||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.todo-count {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.todo-count strong {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.filters li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.filters li a {
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filters li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.filters li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
||||
|
||||
.clear-completed,
|
||||
html .clear-completed:active {
|
||||
float: right;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-completed:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 65px auto 0;
|
||||
color: #bfbfbf;
|
||||
font-size: 10px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info p {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.info a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
Hack to remove background from Mobile Safari.
|
||||
Can't use it globally since it destroys checkboxes in Firefox
|
||||
*/
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
.toggle-all,
|
||||
.todo-list li .toggle {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.footer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
43
examples/todomvc/index.html
Normal file
43
examples/todomvc/index.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
|
||||
<title>web-sys WASM • TodoMVC</title>
|
||||
<link rel="stylesheet" href="./index.css">
|
||||
</head>
|
||||
<body>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>todos</h1>
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus>
|
||||
</header>
|
||||
<section hidden class="main">
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox">
|
||||
<label for="toggle-all">Mark all as complete</label>
|
||||
<ul class="todo-list"></ul>
|
||||
<footer class="footer">
|
||||
<span class="todo-count"></span>
|
||||
<ul class="filters">
|
||||
<li>
|
||||
<a href="#/" class="selected">All</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/active">Active</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/completed">Completed</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="clear-completed">Clear completed</button>
|
||||
</footer>
|
||||
</section>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>Double-click to edit a todo</p>
|
||||
<p>Written by <a href="http://twitter.com/KingstonTime/">Jonathan Kingston</a></p>
|
||||
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
|
||||
</footer>
|
||||
<link rel="stylesheet" href="./base.css">
|
||||
<script src='./index.js'></script>
|
||||
</body>
|
||||
</html>
|
3
examples/todomvc/index.js
Normal file
3
examples/todomvc/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import('./todomvc').then(todomvc => {
|
||||
todomvc.run();
|
||||
});
|
13
examples/todomvc/package.json
Normal file
13
examples/todomvc/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"serve": "webpack-dev-server"
|
||||
},
|
||||
"devDependencies": {
|
||||
"text-encoding": "^0.7.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"webpack": "^4.11.1",
|
||||
"webpack-cli": "^3.1.1",
|
||||
"webpack-dev-server": "^3.1.0"
|
||||
}
|
||||
}
|
5
examples/todomvc/src/build.rs
Normal file
5
examples/todomvc/src/build.rs
Normal file
@ -0,0 +1,5 @@
|
||||
extern crate askama;
|
||||
|
||||
fn main() {
|
||||
askama::rerun_if_templates_changed();
|
||||
}
|
196
examples/todomvc/src/controller.rs
Normal file
196
examples/todomvc/src/controller.rs
Normal file
@ -0,0 +1,196 @@
|
||||
use crate::exit;
|
||||
use crate::store::*;
|
||||
use crate::view::ViewMessage;
|
||||
use crate::{Message, Scheduler};
|
||||
use js_sys::Date;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Weak;
|
||||
|
||||
/// The controller of the application turns page state into functionality
|
||||
pub struct Controller {
|
||||
store: Store,
|
||||
sched: RefCell<Option<Weak<Scheduler>>>,
|
||||
active_route: String,
|
||||
last_active_route: String,
|
||||
}
|
||||
|
||||
/// Messages that represent the methods to be called on the Controller
|
||||
pub enum ControllerMessage {
|
||||
AddItem(String),
|
||||
SetPage(String),
|
||||
EditItemSave(String, String),
|
||||
ToggleItem(String, bool),
|
||||
EditItemCancel(String),
|
||||
RemoveCompleted(),
|
||||
RemoveItem(String),
|
||||
ToggleAll(bool),
|
||||
}
|
||||
|
||||
impl Controller {
|
||||
pub fn new(store: Store, sched: Weak<Scheduler>) -> Controller {
|
||||
Controller {
|
||||
store,
|
||||
sched: RefCell::new(Some(sched)),
|
||||
active_route: "".into(),
|
||||
last_active_route: "none".into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Used by `Scheduler` to convert a `ControllerMessage` into a function call on a `Controller`
|
||||
pub fn call(&mut self, method_name: ControllerMessage) {
|
||||
use self::ControllerMessage::*;
|
||||
match method_name {
|
||||
AddItem(title) => self.add_item(title),
|
||||
SetPage(hash) => self.set_page(hash),
|
||||
EditItemSave(id, value) => self.edit_item_save(id, value),
|
||||
EditItemCancel(id) => self.edit_item_cancel(id),
|
||||
RemoveCompleted() => self.remove_completed_items(),
|
||||
RemoveItem(id) => self.remove_item(&id),
|
||||
ToggleAll(completed) => self.toggle_all(completed),
|
||||
ToggleItem(id, completed) => self.toggle_item(id, completed),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_item(&mut self, id: String, completed: bool) {
|
||||
self.toggle_completed(id, completed);
|
||||
self._filter(completed);
|
||||
}
|
||||
|
||||
fn add_message(&self, view_message: ViewMessage) {
|
||||
if let Ok(sched) = self.sched.try_borrow_mut() {
|
||||
if let Some(ref sched) = *sched {
|
||||
if let Some(sched) = sched.upgrade() {
|
||||
sched.add_message(Message::View(view_message));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_page(&mut self, raw: String) {
|
||||
let route = raw.trim_left_matches("#/");
|
||||
self.active_route = route.to_string();
|
||||
self._filter(false);
|
||||
self.add_message(ViewMessage::UpdateFilterButtons(route.to_string()));
|
||||
}
|
||||
|
||||
/// Add an Item to the Store and display it in the list.
|
||||
fn add_item(&mut self, title: String) {
|
||||
self.store.insert(Item {
|
||||
id: Date::now().to_string(),
|
||||
title,
|
||||
completed: false,
|
||||
});
|
||||
self.add_message(ViewMessage::ClearNewTodo());
|
||||
self._filter(true);
|
||||
}
|
||||
|
||||
/// Save an Item in edit.
|
||||
fn edit_item_save(&mut self, id: String, title: String) {
|
||||
if !title.is_empty() {
|
||||
self.store.update(ItemUpdate::Title {
|
||||
id: id.clone(),
|
||||
title: title.clone(),
|
||||
});
|
||||
self.add_message(ViewMessage::EditItemDone(id.to_string(), title.to_string()));
|
||||
} else {
|
||||
self.remove_item(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the item editing mode.
|
||||
fn edit_item_cancel(&mut self, id: String) {
|
||||
let mut message = None;
|
||||
if let Some(data) = self.store.find(ItemQuery::Id { id: id.clone() }) {
|
||||
if let Some(todo) = data.get(0) {
|
||||
let title = todo.title.to_string();
|
||||
let citem = id.to_string();
|
||||
message = Some(ViewMessage::EditItemDone(citem, title));
|
||||
}
|
||||
}
|
||||
if let Some(message) = message {
|
||||
self.add_message(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the data and elements related to an Item.
|
||||
fn remove_item(&mut self, id: &String) {
|
||||
self.store.remove(ItemQuery::Id { id: id.clone() });
|
||||
self._filter(false);
|
||||
let ritem = id.to_string();
|
||||
self.add_message(ViewMessage::RemoveItem(ritem));
|
||||
}
|
||||
|
||||
/// Remove all completed items.
|
||||
fn remove_completed_items(&mut self) {
|
||||
self.store.remove(ItemQuery::Completed { completed: true });
|
||||
self._filter(true);
|
||||
}
|
||||
|
||||
/// Update an Item in storage based on the state of completed.
|
||||
fn toggle_completed(&mut self, id: String, completed: bool) {
|
||||
self.store.update(ItemUpdate::Completed {
|
||||
id: id.clone(),
|
||||
completed,
|
||||
});
|
||||
let tid = id.to_string();
|
||||
self.add_message(ViewMessage::SetItemComplete(tid, completed));
|
||||
}
|
||||
|
||||
/// Set all items to complete or active.
|
||||
fn toggle_all(&mut self, completed: bool) {
|
||||
let mut vals = Vec::new();
|
||||
self.store
|
||||
.find(
|
||||
ItemQuery::EmptyItemQuery,
|
||||
).map(|data| {
|
||||
for item in data.iter() {
|
||||
vals.push(item.id.clone());
|
||||
}
|
||||
});
|
||||
for id in vals.iter() {
|
||||
self.toggle_completed(id.to_string(), completed);
|
||||
}
|
||||
self._filter(false);
|
||||
}
|
||||
|
||||
/// Refresh the list based on the current route.
|
||||
fn _filter(&mut self, force: bool) {
|
||||
let route = &self.active_route;
|
||||
|
||||
if force || self.last_active_route != "" || &self.last_active_route != route {
|
||||
let query = match route.as_str() {
|
||||
"completed" => ItemQuery::Completed { completed: true },
|
||||
"active" => ItemQuery::Completed { completed: false },
|
||||
_ => ItemQuery::EmptyItemQuery,
|
||||
};
|
||||
let mut v = None;
|
||||
{
|
||||
let store = &mut self.store;
|
||||
if let Some(res) = store.find(query) {
|
||||
v = Some(res.into());
|
||||
}
|
||||
}
|
||||
if let Some(res) = v {
|
||||
self.add_message(ViewMessage::ShowItems(res));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((total, active, completed)) = self.store.count() {
|
||||
self.add_message(ViewMessage::SetItemsLeft(active));
|
||||
self.add_message(ViewMessage::SetClearCompletedButtonVisibility(
|
||||
completed > 0,
|
||||
));
|
||||
self.add_message(ViewMessage::SetCompleteAllCheckbox(completed == total));
|
||||
self.add_message(ViewMessage::SetMainVisibility(total > 0));
|
||||
}
|
||||
|
||||
self.last_active_route = route.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Controller {
|
||||
fn drop(&mut self) {
|
||||
exit("calling drop on Controller");
|
||||
}
|
||||
}
|
208
examples/todomvc/src/element.rs
Normal file
208
examples/todomvc/src/element.rs
Normal file
@ -0,0 +1,208 @@
|
||||
extern crate wasm_bindgen;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
/// Wrapper for `web_sys::Element` to simplify calling different interfaces
|
||||
pub struct Element {
|
||||
el: Option<web_sys::Element>,
|
||||
}
|
||||
|
||||
impl From<Element> for Option<web_sys::Node> {
|
||||
fn from(obj: Element) -> Option<web_sys::Node> {
|
||||
if let Some(el) = obj.el {
|
||||
Some(el.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Element> for Option<web_sys::EventTarget> {
|
||||
fn from(obj: Element) -> Option<web_sys::EventTarget> {
|
||||
if let Some(el) = obj.el {
|
||||
Some(el.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element {
|
||||
pub fn qs(selector: &str) -> Option<Element> {
|
||||
let body: web_sys::Element = web_sys::window()?.document()?.body()?.into();
|
||||
let el = body.query_selector(selector).ok()?;
|
||||
Some(Element { el })
|
||||
}
|
||||
|
||||
/// Add event listener to this node
|
||||
pub fn add_event_listener<T>(&mut self, event_name: &str, handler: T)
|
||||
where
|
||||
T: 'static + FnMut(web_sys::Event),
|
||||
{
|
||||
let cb = Closure::wrap(Box::new(handler) as Box<FnMut(_)>);
|
||||
if let Some(el) = self.el.take() {
|
||||
let el_et: web_sys::EventTarget = el.into();
|
||||
el_et.add_event_listener_with_callback(event_name, cb.as_ref().unchecked_ref());
|
||||
cb.forget();
|
||||
if let Ok(el) = el_et.dyn_into::<web_sys::Element>() {
|
||||
self.el = Some(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delegate an event to a selector
|
||||
pub fn delegate<T>(
|
||||
&mut self,
|
||||
selector: &'static str,
|
||||
event: &str,
|
||||
mut handler: T,
|
||||
use_capture: bool,
|
||||
) where
|
||||
T: 'static + FnMut(web_sys::Event) -> (),
|
||||
{
|
||||
let el = match self.el.take() {
|
||||
Some(e) => e,
|
||||
None => return,
|
||||
};
|
||||
if let Some(dyn_el) = &el.dyn_ref::<web_sys::EventTarget>()
|
||||
{
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Some(document) = window.document() {
|
||||
// TODO document selector to the target element
|
||||
let tg_el = document;
|
||||
|
||||
let cb = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
||||
if let Some(target_element) = event.target() {
|
||||
let dyn_target_el: Option<
|
||||
&web_sys::Node,
|
||||
> = wasm_bindgen::JsCast::dyn_ref(&target_element);
|
||||
if let Some(target_element) = dyn_target_el {
|
||||
if let Ok(potential_elements) =
|
||||
tg_el.query_selector_all(selector)
|
||||
{
|
||||
let mut has_match = false;
|
||||
for i in 0..potential_elements.length() {
|
||||
if let Some(el) = potential_elements.get(i) {
|
||||
if target_element.is_equal_node(Some(&el)) {
|
||||
has_match = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if has_match {
|
||||
handler(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as Box<FnMut(_)>);
|
||||
|
||||
dyn_el.add_event_listener_with_callback_and_bool(
|
||||
event,
|
||||
cb.as_ref().unchecked_ref(),
|
||||
use_capture,
|
||||
);
|
||||
cb.forget(); // TODO cycle collect
|
||||
}
|
||||
}
|
||||
}
|
||||
self.el = Some(el);
|
||||
}
|
||||
|
||||
/// Find child `Element`s from this node
|
||||
pub fn qs_from(&mut self, selector: &str) -> Option<Element> {
|
||||
let mut found_el = None;
|
||||
if let Some(el) = self.el.as_ref() {
|
||||
found_el = Some(Element {
|
||||
el: el.query_selector(selector).ok()?,
|
||||
});
|
||||
}
|
||||
found_el
|
||||
}
|
||||
|
||||
/// Sets the inner HTML of the `self.el` element
|
||||
pub fn set_inner_html(&mut self, value: String) {
|
||||
if let Some(el) = self.el.take() {
|
||||
el.set_inner_html(&value);
|
||||
self.el = Some(el);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the text content of the `self.el` element
|
||||
pub fn set_text_content(&mut self, value: &str) {
|
||||
if let Some(el) = self.el.as_ref() {
|
||||
if let Some(node) = &el.dyn_ref::<web_sys::Node>() {
|
||||
node.set_text_content(Some(&value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a class list item from the element
|
||||
///
|
||||
/// ```
|
||||
/// e.class_list_remove(String::from("clickable"));
|
||||
/// // removes the class 'clickable' from e.el
|
||||
/// ```
|
||||
pub fn class_list_remove(&mut self, value: &str) {
|
||||
if let Some(el) = self.el.take() {
|
||||
el.class_list().remove_1(&value);
|
||||
self.el = Some(el);
|
||||
}
|
||||
}
|
||||
|
||||
/// Given another `Element` it will remove that child from the DOM from this element
|
||||
/// Consumes `child` so it can't be used after it's removal.
|
||||
pub fn remove_child(&mut self, mut child: Element) {
|
||||
if let Some(child_el) = child.el.take() {
|
||||
if let Some(el) = self.el.take() {
|
||||
if let Some(el_node) = el.dyn_ref::<web_sys::Node>() {
|
||||
let child_node: web_sys::Node = child_el.into();
|
||||
el_node.remove_child(&child_node);
|
||||
}
|
||||
self.el = Some(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the whole class value for `self.el`
|
||||
pub fn set_class_name(&mut self, class_name: &str) {
|
||||
if let Some(el) = self.el.take() {
|
||||
el.set_class_name(&class_name);
|
||||
self.el = Some(el);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the visibility for the element in `self.el`
|
||||
pub fn set_visibility(&mut self, visible: bool) {
|
||||
if let Some(el) = self.el.take() {
|
||||
{
|
||||
let dyn_el: Option<&web_sys::HtmlElement> = wasm_bindgen::JsCast::dyn_ref(&el);
|
||||
if let Some(el) = dyn_el {
|
||||
el.set_hidden(!visible);
|
||||
}
|
||||
}
|
||||
self.el = Some(el);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the visibility for the element in `self.el` (The element must be an input)
|
||||
pub fn set_value(&mut self, value: &str) {
|
||||
if let Some(el) = self.el.take() {
|
||||
if let Some(el) = wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&el) {
|
||||
el.set_value(&value);
|
||||
}
|
||||
self.el = Some(el);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the checked state for the element in `self.el` (The element must be an input)
|
||||
pub fn set_checked(&mut self, checked: bool) {
|
||||
if let Some(el) = self.el.take() {
|
||||
if let Some(el) = wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&el) {
|
||||
el.set_checked(checked);
|
||||
}
|
||||
self.el = Some(el);
|
||||
}
|
||||
}
|
||||
}
|
70
examples/todomvc/src/lib.rs
Normal file
70
examples/todomvc/src/lib.rs
Normal file
@ -0,0 +1,70 @@
|
||||
//! # TODO MVC
|
||||
//!
|
||||
//! A [TODO MVC](https://todomvc.com/) implementation written using [web-sys](https://rustwasm.github.io/wasm-bindgen/web-sys/overview.html)
|
||||
#![warn(missing_docs)]
|
||||
|
||||
extern crate wasm_bindgen;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
extern crate js_sys;
|
||||
extern crate web_sys;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[macro_use]
|
||||
extern crate askama;
|
||||
|
||||
/// Controller of the program
|
||||
pub mod controller;
|
||||
/// Element wrapper to the DOM
|
||||
pub mod element;
|
||||
/// Schedule messages to the Controller and View
|
||||
pub mod scheduler;
|
||||
/// Stores items into localstorage
|
||||
pub mod store;
|
||||
/// Handles constructing template strings from data
|
||||
pub mod template;
|
||||
/// Presentation layer
|
||||
pub mod view;
|
||||
use crate::controller::{Controller, ControllerMessage};
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::store::Store;
|
||||
use crate::view::{View, ViewMessage};
|
||||
|
||||
/// Message wrapper enum used to pass through the scheduler to the Controller or View
|
||||
pub enum Message {
|
||||
/// Message wrapper to send to the controller
|
||||
Controller(ControllerMessage),
|
||||
/// Message wrapper to send to the view
|
||||
View(ViewMessage),
|
||||
}
|
||||
|
||||
/// Used for debugging to the console
|
||||
pub fn exit(message: &str) {
|
||||
let v = wasm_bindgen::JsValue::from_str(&message.to_string());
|
||||
web_sys::console::exception_1(&v);
|
||||
std::process::abort();
|
||||
}
|
||||
|
||||
fn app(name: &str) {
|
||||
let sched = Rc::new(Scheduler::new());
|
||||
let store = match Store::new(name) {
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
let controller = Controller::new(store, Rc::downgrade(&sched));
|
||||
if let Some(mut view) = View::new(sched.clone()) {
|
||||
let sch: &Rc<Scheduler> = &sched;
|
||||
view.init();
|
||||
sch.set_view(view);
|
||||
sch.set_controller(controller);
|
||||
sched.add_message(Message::Controller(ControllerMessage::SetPage(
|
||||
"".to_string(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Entry point into the program from JavaScript
|
||||
#[wasm_bindgen]
|
||||
pub fn run() {
|
||||
app("todos-wasmbindgen");
|
||||
}
|
138
examples/todomvc/src/scheduler.rs
Normal file
138
examples/todomvc/src/scheduler.rs
Normal file
@ -0,0 +1,138 @@
|
||||
use crate::controller::Controller;
|
||||
use crate::exit;
|
||||
use crate::view::View;
|
||||
use crate::Message;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Creates an event loop that starts each time a message is added
|
||||
pub struct Scheduler {
|
||||
controller: Rc<RefCell<Option<Controller>>>,
|
||||
view: Rc<RefCell<Option<View>>>,
|
||||
events: RefCell<Vec<Message>>,
|
||||
running: RefCell<bool>,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
/// Construct a new `Scheduler`
|
||||
pub fn new() -> Scheduler {
|
||||
Scheduler {
|
||||
controller: Rc::new(RefCell::new(None)),
|
||||
view: Rc::new(RefCell::new(None)),
|
||||
events: RefCell::new(Vec::new()),
|
||||
running: RefCell::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_controller(&self, controller: Controller) {
|
||||
if let Ok(mut controller_data) = self.controller.try_borrow_mut() {
|
||||
controller_data.replace(controller);
|
||||
} else {
|
||||
exit("This might be a deadlock");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_view(&self, view: View) {
|
||||
if let Ok(mut view_data) = self.view.try_borrow_mut() {
|
||||
view_data.replace(view);
|
||||
} else {
|
||||
exit("This might be a deadlock");
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new message onto the event stack
|
||||
///
|
||||
/// Triggers running the event loop if it's not already running
|
||||
pub fn add_message(&self, message: Message) {
|
||||
let running = {
|
||||
if let Ok(running) = self.running.try_borrow() {
|
||||
running.clone()
|
||||
} else {
|
||||
exit("This might be a deadlock");
|
||||
false
|
||||
}
|
||||
};
|
||||
{
|
||||
if let Ok(mut events) = self.events.try_borrow_mut() {
|
||||
events.push(message);
|
||||
} else {
|
||||
exit("This might be a deadlock");
|
||||
}
|
||||
}
|
||||
if !running {
|
||||
self.run();
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the event loop, taking messages from the stack to run
|
||||
fn run(&self) {
|
||||
let mut events_len = 0;
|
||||
{
|
||||
if let Ok(events) = self.events.try_borrow() {
|
||||
events_len = events.len().clone();
|
||||
} else {
|
||||
exit("This might be a deadlock");
|
||||
}
|
||||
}
|
||||
if events_len == 0 {
|
||||
if let Ok(mut running) = self.running.try_borrow_mut() {
|
||||
*running = false;
|
||||
} else {
|
||||
exit("This might be a deadlock");
|
||||
}
|
||||
} else {
|
||||
{
|
||||
if let Ok(mut running) = self.running.try_borrow_mut() {
|
||||
*running = true;
|
||||
} else {
|
||||
exit("This might be a deadlock");
|
||||
}
|
||||
}
|
||||
self.next_message();
|
||||
}
|
||||
}
|
||||
|
||||
fn next_message(&self) {
|
||||
let event = {
|
||||
if let Ok(mut events) = self.events.try_borrow_mut() {
|
||||
Some(events.pop())
|
||||
} else {
|
||||
exit("This might be a deadlock");
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(Some(event)) = event {
|
||||
match event {
|
||||
Message::Controller(e) => {
|
||||
if let Ok(mut controller) = self.controller.try_borrow_mut() {
|
||||
if let Some(ref mut ag) = *controller {
|
||||
ag.call(e);
|
||||
}
|
||||
} else {
|
||||
exit("This might be a deadlock");
|
||||
}
|
||||
}
|
||||
Message::View(e) => {
|
||||
if let Ok(mut view) = self.view.try_borrow_mut() {
|
||||
if let Some(ref mut ag) = *view {
|
||||
ag.call(e);
|
||||
}
|
||||
} else {
|
||||
exit("This might be a deadlock");
|
||||
}
|
||||
}
|
||||
}
|
||||
self.run();
|
||||
} else if let Ok(mut running) = self.running.try_borrow_mut() {
|
||||
*running = false;
|
||||
} else {
|
||||
exit("This might be a deadlock");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Scheduler {
|
||||
fn drop(&mut self) {
|
||||
exit("calling drop on Scheduler");
|
||||
}
|
||||
}
|
285
examples/todomvc/src/store.rs
Normal file
285
examples/todomvc/src/store.rs
Normal file
@ -0,0 +1,285 @@
|
||||
use js_sys::JSON;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use crate::exit;
|
||||
|
||||
/// Stores items into localstorage
|
||||
pub struct Store {
|
||||
local_storage: web_sys::Storage,
|
||||
data: ItemList,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
/// Creates a new store with `name` as the localstorage value name
|
||||
pub fn new(name: &str) -> Option<Store> {
|
||||
let window = web_sys::window()?;
|
||||
if let Ok(Some(local_storage)) = window.local_storage() {
|
||||
let mut store = Store {
|
||||
local_storage,
|
||||
data: ItemList::new(),
|
||||
name: String::from(name),
|
||||
};
|
||||
store.fetch_local_storage();
|
||||
Some(store)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the local ItemList from localStorage.
|
||||
/// Returns an &Option<ItemList> of the stored database
|
||||
/// Caches the store into `self.data` to reduce calls to JS
|
||||
///
|
||||
/// Uses mut here as the return is something we might want to manipulate
|
||||
///
|
||||
fn fetch_local_storage(&mut self) -> Option<()> {
|
||||
let mut item_list = ItemList::new();
|
||||
// If we have an existing cached value, return early.
|
||||
if let Ok(Some(value)) = self.local_storage.get_item(&self.name) {
|
||||
let data = JSON::parse(&value).ok()?;
|
||||
let iter = js_sys::try_iter(&data).ok()??;
|
||||
for item in iter {
|
||||
let item = item.ok()?;
|
||||
let item_array: &js_sys::Array = wasm_bindgen::JsCast::dyn_ref(&item)?;
|
||||
let title = item_array.shift().as_string()?;
|
||||
let completed = item_array.shift().as_bool()?;
|
||||
let id = item_array.shift().as_string()?;
|
||||
let mut temp_item = Item {
|
||||
title,
|
||||
completed,
|
||||
id,
|
||||
};
|
||||
item_list.push(temp_item);
|
||||
}
|
||||
}
|
||||
self.data = item_list;
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Write the local ItemList to localStorage.
|
||||
fn sync_local_storage(&mut self) {
|
||||
let array = js_sys::Array::new();
|
||||
for item in self.data.iter() {
|
||||
let mut child = js_sys::Array::new();
|
||||
let s = item.title.clone();
|
||||
child.push(&JsValue::from(&s));
|
||||
child.push(&JsValue::from(item.completed));
|
||||
child.push(&JsValue::from(item.id.to_string()));
|
||||
|
||||
array.push(&JsValue::from(child));
|
||||
}
|
||||
if let Ok(storage_string) = JSON::stringify(&JsValue::from(array)) {
|
||||
let storage_string: String = storage_string.to_string().into();
|
||||
self.local_storage
|
||||
.set_item(&self.name, storage_string.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
/// Find items with properties matching those on query.
|
||||
/// `ItemQuery` query Query to match
|
||||
///
|
||||
/// ```
|
||||
/// let data = db.find(ItemQuery::Completed {completed: true});
|
||||
/// // data will contain items whose completed properties are true
|
||||
/// ```
|
||||
pub fn find(&mut self, query: ItemQuery) -> Option<ItemListSlice> {
|
||||
Some(self.data.iter().filter(|todo| query.matches(*todo)).collect())
|
||||
}
|
||||
|
||||
/// Update an item in the Store.
|
||||
///
|
||||
/// `ItemUpdate` update Record with an id and a property to update
|
||||
pub fn update(&mut self, update: ItemUpdate) {
|
||||
let id = update.id();
|
||||
self.data.iter_mut().for_each(|todo| {
|
||||
if id == todo.id {
|
||||
todo.update(&update);
|
||||
}
|
||||
});
|
||||
self.sync_local_storage();
|
||||
}
|
||||
|
||||
/// Insert an item into the Store.
|
||||
///
|
||||
/// `Item` item Item to insert
|
||||
pub fn insert(&mut self, item: Item) {
|
||||
self.data.push(item);
|
||||
self.sync_local_storage();
|
||||
}
|
||||
|
||||
/// Remove items from the Store based on a query.
|
||||
/// query is an `ItemQuery` query Query matching the items to remove
|
||||
pub fn remove(&mut self, query: ItemQuery) {
|
||||
self.data.retain(|todo| !query.matches(todo));
|
||||
self.sync_local_storage();
|
||||
}
|
||||
|
||||
/// Count total, active, and completed todos.
|
||||
pub fn count(&mut self) -> Option<(usize, usize, usize)> {
|
||||
self.find(ItemQuery::EmptyItemQuery).map(|data| {
|
||||
let total = data.length();
|
||||
|
||||
let mut completed = 0;
|
||||
for item in data.iter() {
|
||||
if item.completed {
|
||||
completed += 1;
|
||||
}
|
||||
}
|
||||
(total, total - completed, completed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a todo item
|
||||
pub struct Item {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
pub fn update(&mut self, update: &ItemUpdate) {
|
||||
match update {
|
||||
ItemUpdate::Title { title, .. } => {
|
||||
self.title = title.to_string();
|
||||
}
|
||||
ItemUpdate::Completed { completed, .. } => {
|
||||
self.completed = *completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ItemListTrait<T> {
|
||||
fn new() -> Self;
|
||||
fn get(&self, i: usize) -> Option<&T>;
|
||||
fn length(&self) -> usize;
|
||||
fn push(&mut self, item: T);
|
||||
fn iter(&self) -> std::slice::Iter<T>;
|
||||
}
|
||||
|
||||
pub struct ItemList {
|
||||
list: Vec<Item>,
|
||||
}
|
||||
impl ItemList {
|
||||
fn into_iter(self) -> std::vec::IntoIter<Item> {
|
||||
self.list.into_iter()
|
||||
}
|
||||
fn retain<F>(&mut self, f: F)
|
||||
where
|
||||
F: FnMut(&Item) -> bool {
|
||||
self.list.retain(f);
|
||||
}
|
||||
fn iter_mut(&mut self) -> std::slice::IterMut<Item> {
|
||||
self.list.iter_mut()
|
||||
}
|
||||
}
|
||||
impl ItemListTrait<Item> for ItemList {
|
||||
fn new() -> ItemList {
|
||||
ItemList { list: Vec::new() }
|
||||
}
|
||||
fn get(&self, i: usize) -> Option<&Item> {
|
||||
self.list.get(i)
|
||||
}
|
||||
fn length(&self) -> usize {
|
||||
self.list.len()
|
||||
}
|
||||
fn push(&mut self, item: Item) {
|
||||
self.list.push(item)
|
||||
}
|
||||
fn iter(&self) -> std::slice::Iter<Item> {
|
||||
self.list.iter()
|
||||
}
|
||||
}
|
||||
use std::iter::FromIterator;
|
||||
impl<'a> FromIterator<Item> for ItemList {
|
||||
fn from_iter<I: IntoIterator<Item = Item>>(iter: I) -> Self {
|
||||
let mut c = ItemList::new();
|
||||
for i in iter {
|
||||
c.push(i);
|
||||
}
|
||||
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
/// A borrowed set of Items filtered from the store
|
||||
pub struct ItemListSlice<'a> {
|
||||
list: Vec<&'a Item>,
|
||||
}
|
||||
|
||||
impl<'a> ItemListTrait<&'a Item> for ItemListSlice<'a> {
|
||||
fn new() -> ItemListSlice<'a> {
|
||||
ItemListSlice { list: Vec::new() }
|
||||
}
|
||||
fn get(&self, i: usize) -> Option<&&'a Item> {
|
||||
self.list.get(i)
|
||||
}
|
||||
fn length(&self) -> usize {
|
||||
self.list.len()
|
||||
}
|
||||
fn push(&mut self, item: &'a Item) {
|
||||
self.list.push(item)
|
||||
}
|
||||
fn iter(&self) -> std::slice::Iter<&'a Item> {
|
||||
self.list.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromIterator<&'a Item> for ItemListSlice<'a> {
|
||||
fn from_iter<I: IntoIterator<Item = &'a Item>>(iter: I) -> Self {
|
||||
let mut c = ItemListSlice::new();
|
||||
for i in iter {
|
||||
c.push(i);
|
||||
}
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Into<ItemList> for ItemListSlice<'a> {
|
||||
fn into(self) -> ItemList {
|
||||
let mut i = ItemList::new();
|
||||
let items = self.list.into_iter();
|
||||
for j in items {
|
||||
// TODO neaten this cloning?
|
||||
let item = Item {
|
||||
id: j.id.clone(),
|
||||
completed: j.completed,
|
||||
title: j.title.clone(),
|
||||
};
|
||||
i.push(item);
|
||||
}
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a search into the store
|
||||
pub enum ItemQuery {
|
||||
Id { id: String },
|
||||
Completed { completed: bool },
|
||||
EmptyItemQuery,
|
||||
}
|
||||
|
||||
impl ItemQuery {
|
||||
fn matches(&self, item: &Item) -> bool {
|
||||
match *self {
|
||||
ItemQuery::EmptyItemQuery => true,
|
||||
ItemQuery::Id { ref id } => &item.id == id,
|
||||
ItemQuery::Completed { completed } => item.completed == completed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ItemUpdate {
|
||||
Title { id: String, title: String },
|
||||
Completed { id: String, completed: bool },
|
||||
}
|
||||
|
||||
impl ItemUpdate {
|
||||
fn id(&self) -> String {
|
||||
match self {
|
||||
ItemUpdate::Title { id, .. } => id.clone(),
|
||||
ItemUpdate::Completed { id, .. } => id.clone(),
|
||||
}
|
||||
}
|
||||
}
|
58
examples/todomvc/src/template.rs
Normal file
58
examples/todomvc/src/template.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use store::{ItemList, ItemListTrait, Item};
|
||||
use askama::Template as AskamaTemplate;
|
||||
|
||||
#[derive(AskamaTemplate)]
|
||||
#[template(path = "row.html")]
|
||||
struct RowTemplate<'a> {
|
||||
id: &'a str,
|
||||
title: &'a str,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
|
||||
#[derive(AskamaTemplate)]
|
||||
#[template(path = "itemsLeft.html")]
|
||||
struct ItemsLeftTemplate {
|
||||
active_todos: usize,
|
||||
}
|
||||
|
||||
pub struct Template {}
|
||||
|
||||
impl Template {
|
||||
/// Format the contents of a todo list.
|
||||
///
|
||||
/// items `ItemList` contains keys you want to find in the template to replace.
|
||||
/// Returns the contents for a todo list
|
||||
///
|
||||
pub fn item_list(items: ItemList) -> String {
|
||||
let mut output = String::from("");
|
||||
for item in items.iter() {
|
||||
let row = RowTemplate {
|
||||
id: &item.id,
|
||||
completed: item.completed,
|
||||
title: &item.title,
|
||||
};
|
||||
if let Ok(res) = row.render() {
|
||||
output.push_str(&res);
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
///
|
||||
/// Format the contents of an "items left" indicator.
|
||||
///
|
||||
/// `active_todos` Number of active todos
|
||||
///
|
||||
/// Returns the contents for an "items left" indicator
|
||||
pub fn item_counter(active_todos: usize) -> String {
|
||||
let items_left = ItemsLeftTemplate {
|
||||
active_todos
|
||||
};
|
||||
if let Ok(res) = items_left.render() {
|
||||
res
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
478
examples/todomvc/src/view.rs
Normal file
478
examples/todomvc/src/view.rs
Normal file
@ -0,0 +1,478 @@
|
||||
use crate::controller::ControllerMessage;
|
||||
use crate::exit;
|
||||
use crate::element::Element;
|
||||
use crate::store::ItemList;
|
||||
use crate::{Message, Scheduler};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
use crate::template::Template;
|
||||
|
||||
const ENTER_KEY: u32 = 13;
|
||||
const ESCAPE_KEY: u32 = 27;
|
||||
|
||||
extern crate wasm_bindgen;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Messages that represent the methods to be called on the View
|
||||
pub enum ViewMessage {
|
||||
UpdateFilterButtons(String),
|
||||
ClearNewTodo(),
|
||||
ShowItems(ItemList),
|
||||
SetItemsLeft(usize),
|
||||
SetClearCompletedButtonVisibility(bool),
|
||||
SetCompleteAllCheckbox(bool),
|
||||
SetMainVisibility(bool),
|
||||
RemoveItem(String),
|
||||
EditItemDone(String, String),
|
||||
SetItemComplete(String, bool),
|
||||
}
|
||||
|
||||
fn item_id(element: &web_sys::EventTarget) -> Option<String> {
|
||||
//TODO ugly reformat
|
||||
let dyn_el: Option<&web_sys::Node> = wasm_bindgen::JsCast::dyn_ref(element);
|
||||
if let Some(element_node) = dyn_el {
|
||||
element_node.parent_node().map(|parent| {
|
||||
let mut res = None;
|
||||
if let Some(e) = wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&parent) {
|
||||
if e.dataset().get("id") != "" {
|
||||
res = Some(e.dataset().get("id"))
|
||||
}
|
||||
};
|
||||
if None == res {
|
||||
if let Some(ep) = parent.parent_node() {
|
||||
if let Some(dyn_el) = wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&ep)
|
||||
{
|
||||
res = Some(dyn_el.dataset().get("id"));
|
||||
}
|
||||
}
|
||||
}
|
||||
res.unwrap()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Presentation layer
|
||||
#[wasm_bindgen]
|
||||
pub struct View {
|
||||
sched: RefCell<Rc<Scheduler>>,
|
||||
todo_list: Element,
|
||||
todo_item_counter: Element,
|
||||
clear_completed: Element,
|
||||
main: Element,
|
||||
toggle_all: Element,
|
||||
new_todo: Element,
|
||||
callbacks: Vec<(web_sys::EventTarget, String, Closure<FnMut()>)>,
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// Construct a new view
|
||||
pub fn new(sched: Rc<Scheduler>) -> Option<View> {
|
||||
let todo_list = Element::qs(".todo-list")?;
|
||||
let todo_item_counter = Element::qs(".todo-count")?;
|
||||
let clear_completed = Element::qs(".clear-completed")?;
|
||||
let main = Element::qs(".main")?;
|
||||
let toggle_all = Element::qs(".toggle-all")?;
|
||||
let new_todo = Element::qs(".new-todo")?;
|
||||
Some(View {
|
||||
sched: RefCell::new(sched),
|
||||
todo_list,
|
||||
todo_item_counter,
|
||||
clear_completed,
|
||||
main,
|
||||
toggle_all,
|
||||
new_todo,
|
||||
callbacks: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(&mut self) {
|
||||
let window = match web_sys::window() {
|
||||
Some(w) => w,
|
||||
None => return,
|
||||
};
|
||||
let document = match window.document() {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
let sched = self.sched.clone();
|
||||
let set_page = Closure::wrap(Box::new(move || {
|
||||
if let Some(location) = document.location() {
|
||||
if let Ok(hash) = location.hash() {
|
||||
if let Ok(sched) = &(sched.try_borrow_mut()) {
|
||||
sched.add_message(Message::Controller(ControllerMessage::SetPage(
|
||||
hash,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as Box<FnMut()>);
|
||||
|
||||
let window_et: web_sys::EventTarget = window.into();
|
||||
window_et.add_event_listener_with_callback(
|
||||
"hashchange",
|
||||
set_page.as_ref().unchecked_ref(),
|
||||
);
|
||||
set_page.forget(); // Cycle collect this
|
||||
//self.callbacks.push((window_et, "hashchange".to_string(), set_page));
|
||||
self.bind_add_item();
|
||||
self.bind_edit_item_save();
|
||||
self.bind_edit_item_cancel();
|
||||
self.bind_remove_item();
|
||||
self.bind_toggle_item();
|
||||
self.bind_edit_item();
|
||||
self.bind_remove_completed();
|
||||
self.bind_toggle_all();
|
||||
}
|
||||
|
||||
fn bind_edit_item(&mut self) {
|
||||
self.todo_list.delegate(
|
||||
"li label",
|
||||
"dblclick",
|
||||
|e: web_sys::Event| {
|
||||
if let Some(target) = e.target() {
|
||||
if let Ok(el) = wasm_bindgen::JsCast::dyn_into::<web_sys::Element>(target) {
|
||||
View::edit_item(el);
|
||||
}
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Put an item into edit mode.
|
||||
fn edit_item(target: web_sys::Element) {
|
||||
let target_node: web_sys::Node = target.into();
|
||||
if let Some(parent_element) = target_node.parent_element() {
|
||||
let parent_node: web_sys::Node = parent_element.into();
|
||||
if let Some(list_item) = parent_node.parent_element() {
|
||||
list_item.class_list().add_1("editing");
|
||||
if let Some(input) = create_element("input") {
|
||||
input.set_class_name("edit");
|
||||
let list_item_node: web_sys::Node = list_item.into();
|
||||
list_item_node.append_child(&input.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(el) = wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&target_node) {
|
||||
if let Some(input_el) =
|
||||
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&target_node)
|
||||
{
|
||||
input_el.set_value(&el.inner_text());
|
||||
}
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Used by scheduler to convert a `ViewMessage` into a function call on the `View`
|
||||
pub fn call(&mut self, method_name: ViewMessage) {
|
||||
use self::ViewMessage::*;
|
||||
match method_name {
|
||||
UpdateFilterButtons(route) => self.update_filter_buttons(&route),
|
||||
ClearNewTodo() => self.clear_new_todo(),
|
||||
ShowItems(item_list) => self.show_items(item_list),
|
||||
SetItemsLeft(count) => self.set_items_left(count),
|
||||
SetClearCompletedButtonVisibility(visible) => {
|
||||
self.set_clear_completed_button_visibility(visible)
|
||||
}
|
||||
SetCompleteAllCheckbox(complete) => self.set_complete_all_checkbox(complete),
|
||||
SetMainVisibility(complete) => self.set_main_visibility(complete),
|
||||
RemoveItem(id) => self.remove_item(&id),
|
||||
EditItemDone(id, title) => self.edit_item_done(&id, &title),
|
||||
SetItemComplete(id, completed) => self.set_item_complete(&id, completed),
|
||||
}
|
||||
}
|
||||
|
||||
/// Populate the todo list with a list of items.
|
||||
fn show_items(&mut self, items: ItemList) {
|
||||
self.todo_list.set_inner_html(Template::item_list(items));
|
||||
}
|
||||
|
||||
/// Gets the selector to find a todo item in the DOM
|
||||
fn get_selector_string(id: &str) -> String {
|
||||
let mut selector = String::from("[data-id=\"");
|
||||
selector.push_str(id);
|
||||
selector.push_str("\"]");
|
||||
selector
|
||||
}
|
||||
|
||||
/// Remove an item from the view.
|
||||
fn remove_item(&mut self, id: &str) {
|
||||
let elem = Element::qs(&View::get_selector_string(id));
|
||||
|
||||
if let Some(elem) = elem {
|
||||
self.todo_list.remove_child(elem);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the number in the 'items left' display.
|
||||
fn set_items_left(&mut self, items_left: usize) {
|
||||
// TODO what is items left?
|
||||
self.todo_item_counter
|
||||
.set_inner_html(Template::item_counter(items_left));
|
||||
}
|
||||
|
||||
/// Set the visibility of the "Clear completed" button.
|
||||
fn set_clear_completed_button_visibility(&mut self, visible: bool) {
|
||||
self.clear_completed.set_visibility(visible);
|
||||
}
|
||||
|
||||
/// Set the visibility of the main content and footer.
|
||||
fn set_main_visibility(&mut self, visible: bool) {
|
||||
self.main.set_visibility(visible);
|
||||
}
|
||||
|
||||
/// Set the checked state of the Complete All checkbox.
|
||||
fn set_complete_all_checkbox(&mut self, checked: bool) {
|
||||
self.toggle_all.set_checked(checked);
|
||||
}
|
||||
|
||||
/// Change the appearance of the filter buttons based on the route.
|
||||
fn update_filter_buttons(&self, route: &str) {
|
||||
if let Some(mut el) = Element::qs(".filters .selected") {
|
||||
el.set_class_name("");
|
||||
}
|
||||
|
||||
let mut selector = String::from(".filters [href=\"");
|
||||
selector.push_str(route);
|
||||
selector.push_str("\"]");
|
||||
|
||||
if let Some(mut el) = Element::qs(&selector) {
|
||||
el.set_class_name("selected");
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the new todo input
|
||||
fn clear_new_todo(&mut self) {
|
||||
self.new_todo.set_value("");
|
||||
}
|
||||
|
||||
/// Render an item as either completed or not.
|
||||
fn set_item_complete(&self, id: &str, completed: bool) {
|
||||
if let Some(mut list_item) = Element::qs(&View::get_selector_string(id)) {
|
||||
let class_name = if completed { "completed" } else { "" };
|
||||
list_item.set_class_name(class_name);
|
||||
|
||||
// In case it was toggled from an event and not by clicking the checkbox
|
||||
if let Some(mut el) = list_item.qs_from("input") {
|
||||
el.set_checked(completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bring an item out of edit mode.
|
||||
fn edit_item_done(&self, id: &str, title: &str) {
|
||||
if let Some(mut list_item) = Element::qs(&View::get_selector_string(id)) {
|
||||
if let Some(input) = list_item.qs_from("input.edit") {
|
||||
list_item.class_list_remove("editing");
|
||||
|
||||
if let Some(mut list_item_label) = list_item.qs_from("label") {
|
||||
list_item_label.set_text_content(title);
|
||||
}
|
||||
|
||||
list_item.remove_child(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_add_item(&mut self) {
|
||||
let sched = self.sched.clone();
|
||||
let cb = move |event: web_sys::Event| {
|
||||
if let Some(target) = event.target() {
|
||||
if let Some(input_el) =
|
||||
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&target)
|
||||
{
|
||||
let v = input_el.value(); // TODO remove with nll
|
||||
let title = v.trim();
|
||||
if title != "" {
|
||||
if let Ok(sched) = &(sched.try_borrow_mut()) {
|
||||
sched.add_message(Message::Controller(ControllerMessage::AddItem(
|
||||
String::from(title),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
self.new_todo.add_event_listener("change", cb);
|
||||
}
|
||||
|
||||
fn bind_remove_completed(&mut self) {
|
||||
let sched = self.sched.clone();
|
||||
let handler = move |_| {
|
||||
if let Ok(sched) = &(sched.try_borrow_mut()) {
|
||||
sched.add_message(Message::Controller(ControllerMessage::RemoveCompleted()));
|
||||
}
|
||||
};
|
||||
self.clear_completed.add_event_listener("click", handler);
|
||||
}
|
||||
|
||||
fn bind_toggle_all(&mut self) {
|
||||
let sched = self.sched.clone();
|
||||
self.toggle_all
|
||||
.add_event_listener("click", move |event: web_sys::Event| {
|
||||
if let Some(target) = event.target() {
|
||||
if let Some(input_el) =
|
||||
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&target)
|
||||
{
|
||||
if let Ok(sched) = &(sched.try_borrow_mut()) {
|
||||
sched.add_message(Message::Controller(ControllerMessage::ToggleAll(
|
||||
input_el.checked(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn bind_remove_item(&mut self) {
|
||||
let sched = self.sched.clone();
|
||||
self.todo_list.delegate(
|
||||
".destroy",
|
||||
"click",
|
||||
move |e: web_sys::Event| {
|
||||
if let Some(target) = e.target() {
|
||||
if let Some(item_id) = item_id(&target) {
|
||||
if let Ok(sched) = &(sched.try_borrow_mut()) {
|
||||
sched.add_message(Message::Controller(ControllerMessage::RemoveItem(
|
||||
item_id,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
fn bind_toggle_item(&mut self) {
|
||||
let sched = self.sched.clone();
|
||||
self.todo_list.delegate(
|
||||
".toggle",
|
||||
"click",
|
||||
move |e: web_sys::Event| {
|
||||
if let Some(target) = e.target() {
|
||||
if let Some(input_el) =
|
||||
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&target)
|
||||
{
|
||||
if let Some(item_id) = item_id(&target) {
|
||||
if let Ok(sched) = &(sched.try_borrow_mut()) {
|
||||
sched.add_message(Message::Controller(
|
||||
ControllerMessage::ToggleItem(item_id, input_el.checked()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
fn bind_edit_item_save(&mut self) {
|
||||
let sched = self.sched.clone();
|
||||
|
||||
self.todo_list.delegate(
|
||||
"li .edit",
|
||||
"blur",
|
||||
move |e: web_sys::Event| {
|
||||
if let Some(target) = e.target() {
|
||||
if let Some(target_el) =
|
||||
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&target)
|
||||
{
|
||||
if target_el.dataset().get("iscanceled") != "true" {
|
||||
if let Some(input_el) =
|
||||
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&target)
|
||||
{
|
||||
if let Some(item) = item_id(&target) {
|
||||
// TODO refactor back into fn
|
||||
// Was: &self.add_message(ControllerMessage::SetPage(hash));
|
||||
if let Ok(sched) = &(sched.try_borrow_mut()) {
|
||||
sched.add_message(Message::Controller(
|
||||
ControllerMessage::EditItemSave(item, input_el.value()),
|
||||
));
|
||||
}
|
||||
|
||||
// TODO refactor back into fn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
// Remove the cursor from the input when you hit enter just like if it were a real form
|
||||
self.todo_list.delegate(
|
||||
"li .edit",
|
||||
"keypress",
|
||||
|e: web_sys::Event| {
|
||||
if let Some(key_e) = wasm_bindgen::JsCast::dyn_ref::<web_sys::KeyboardEvent>(&e) {
|
||||
if key_e.key_code() == ENTER_KEY {
|
||||
if let Some(target) = e.target() {
|
||||
if let Some(el) =
|
||||
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&target)
|
||||
{
|
||||
el.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
fn bind_edit_item_cancel(&mut self) {
|
||||
let sched = self.sched.clone();
|
||||
self.todo_list.delegate(
|
||||
"li .edit",
|
||||
"keyup",
|
||||
move |e: web_sys::Event| {
|
||||
if let Some(key_e) = wasm_bindgen::JsCast::dyn_ref::<web_sys::KeyboardEvent>(&e) {
|
||||
if key_e.key_code() == ESCAPE_KEY {
|
||||
if let Some(target) = e.target() {
|
||||
if let Some(el) =
|
||||
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&target)
|
||||
{
|
||||
el.dataset().set("iscanceled", "true");
|
||||
el.blur();
|
||||
}
|
||||
|
||||
if let Some(item_id) = item_id(&target) {
|
||||
if let Ok(sched) = &(sched.try_borrow_mut()) {
|
||||
sched.add_message(Message::Controller(
|
||||
ControllerMessage::EditItemCancel(item_id),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_element(tag: &str) -> Option<web_sys::Element> {
|
||||
web_sys::window()?.document()?.create_element(tag).ok()
|
||||
}
|
||||
|
||||
impl Drop for View {
|
||||
fn drop(&mut self) {
|
||||
exit("calling drop on view");
|
||||
let callbacks: Vec<(web_sys::EventTarget, String, Closure<FnMut()>)> =
|
||||
self.callbacks.drain(..).collect();
|
||||
for callback in callbacks {
|
||||
callback.0.remove_event_listener_with_callback(
|
||||
callback.1.as_str(),
|
||||
&callback.2.as_ref().unchecked_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
1
examples/todomvc/templates/itemsLeft.html
Normal file
1
examples/todomvc/templates/itemsLeft.html
Normal file
@ -0,0 +1 @@
|
||||
{{ active_todos }} item{% if active_todos > 1 %}s{% endif %} left
|
7
examples/todomvc/templates/row.html
Normal file
7
examples/todomvc/templates/row.html
Normal file
@ -0,0 +1,7 @@
|
||||
<li data-id="{{ id }}"{% if completed %} class="completed"{% endif %}>
|
||||
<div class="view">
|
||||
<input class="toggle" type="checkbox"{% if completed %} checked{% endif %}>
|
||||
<label>{{ title }}</label>
|
||||
<button class="destroy"></button>
|
||||
</div>
|
||||
</li>
|
10
examples/todomvc/webpack.config.js
Normal file
10
examples/todomvc/webpack.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: './index.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'index.js',
|
||||
},
|
||||
mode: 'development'
|
||||
};
|
@ -27,6 +27,7 @@
|
||||
- [web-sys: WebGL](./examples/webgl.md)
|
||||
- [web-sys: A Simple Paint Program](./examples/paint.md)
|
||||
- [Parallel Raytracing](./examples/raytrace.md)
|
||||
- [web-sys: A TODO MVC App](./examples/todomvc.md)
|
||||
- [Reference](./reference/index.md)
|
||||
- [Passing Rust Closures to JS](./reference/passing-rust-closures-to-js.md)
|
||||
- [Receiving JS Closures in Rust](./reference/receiving-js-closures-in-rust.md)
|
||||
|
23
guide/src/examples/todomvc.md
Normal file
23
guide/src/examples/todomvc.md
Normal file
@ -0,0 +1,23 @@
|
||||
# TODO MVC using wasm-bingen and web-sys
|
||||
|
||||
[View full source code][code] or [view the compiled example online][online]
|
||||
|
||||
[online]: https://rustwasm.github.io/wasm-bindgen/exbuild/todomvc/
|
||||
[code]: https://github.com/rustwasm/wasm-bindgen/tree/master/examples/todomvc
|
||||
|
||||
[wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) and [web-sys](https://rustwasm.github.io/wasm-bindgen/api/web_sys/) coded [TODO MVC](https://todomvc.com/)
|
||||
|
||||
The code was rewritten from the [ES6 version](http://todomvc.com/examples/vanilla-es6/).
|
||||
|
||||
The core differences are:
|
||||
- Having an [Element wrapper](/src/element.rs) that takes care of dyn and into refs in web-sys,
|
||||
- A [Scheduler](/src/scheduler.rs) that allows Controller and View to communicate to each other by emulating something similar to the JS event loop.
|
||||
|
||||
|
||||
## Size
|
||||
|
||||
The size of the project hasn't undergone much work to make it optimised yet.
|
||||
|
||||
- ~96kb release build
|
||||
- ~76kb optimised with binaryen
|
||||
- ~28kb brotli compressed
|
Loading…
Reference in New Issue
Block a user