Adding in TODO MVC example using web-sys

This commit is contained in:
Jonathan Kingston 2018-10-13 01:21:14 +01:00
parent cb170ef94f
commit b322f46303
22 changed files with 1995 additions and 0 deletions

View File

@ -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
View File

@ -0,0 +1,3 @@
target/
todomvc.js
*.swp

View 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',
]

View 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
View 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
View 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;
}
}

View 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>

View File

@ -0,0 +1,3 @@
import('./todomvc').then(todomvc => {
todomvc.run();
});

View 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"
}
}

View File

@ -0,0 +1,5 @@
extern crate askama;
fn main() {
askama::rerun_if_templates_changed();
}

View 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");
}
}

View 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);
}
}
}

View 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");
}

View 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");
}
}

View 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(),
}
}
}

View 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()
}
}
}

View 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(),
);
}
}
}

View File

@ -0,0 +1 @@
{{ active_todos }} item{% if active_todos > 1 %}s{% endif %} left

View 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>

View 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'
};

View File

@ -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)

View 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