Skip to content

Commit

Permalink
Support boolean attrs, child blocks, and port TodoMVC example.
Browse files Browse the repository at this point in the history
  • Loading branch information
bodil committed Mar 16, 2019
1 parent f628e63 commit a39ccf7
Show file tree
Hide file tree
Showing 20 changed files with 980 additions and 46 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"macros",
"examples/stdweb",
"examples/rocket",
"examples/dodrio",
"examples/dodrio/counter",
"examples/dodrio/todomvc",
"ui",
]
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ console_log = "0.1.2"
dodrio = "0.1.0"
log = "0.4.6"
wasm-bindgen = "0.2.38"
typed-html = { path = "../../typed-html", features = ["dodrio_macro"] }
typed-html = { path = "../../../typed-html", features = ["dodrio_macro"] }

[dependencies.web-sys]
version = "0.3.15"
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl Render for Counter {
// counter on the next animation frame.
vdom.schedule_render();
}}>"+"</button>
{ text(count.into_bump_str()) }
{ vec![text(count.into_bump_str())] }
<button onclick={|root, vdom, _event| {
// Same as above, but decrementing instead of incrementing.
root.unwrap_mut::<Counter>().decrement();
Expand Down
49 changes: 49 additions & 0 deletions examples/dodrio/todomvc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[package]
authors = ["Nick Fitzgerald <[email protected]>"]
edition = "2018"
name = "dodrio-todomvc"
version = "0.1.0"

[dependencies]
cfg-if = "0.1.7"
dodrio = "0.1.0"
futures = "0.1.25"
js-sys = "0.3.15"
serde = { features = ["derive"], version = "1.0.89" }
serde_json = "1.0.39"
wasm-bindgen = "0.2.38"
wasm-bindgen-futures = "0.3.15"
typed-html = { path = "../../../typed-html", features = ["dodrio_macro"] }

# Optional dependencies for logging.
console_error_panic_hook = { optional = true, version = "0.1.6" }
console_log = { optional = true, version = "0.1.2" }
log = { optional = true, version = "0.4.6" }

[dependencies.web-sys]
version = "0.3.15"
features = [
"Document",
"Event",
"EventTarget",
"HtmlElement",
"HtmlInputElement",
"KeyboardEvent",
"Location",
"Storage",
"Node",
"Window",
]

[dev-dependencies]
wasm-bindgen-test = "0.2.38"

[features]
logging = [
"console_log",
"console_error_panic_hook",
"log",
]

[lib]
crate-type = ["cdylib"]
33 changes: 33 additions & 0 deletions examples/dodrio/todomvc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# TodoMVC

`dodrio` implementation of the popular [TodoMVC](http://todomvc.com/) app. It
correctly and completely fulfills [the
specification](https://github.com/tastejs/todomvc/blob/master/app-spec.md) to
the best of my knowledge.

## Source

There are a number of modules in this `dodrio` implementation of TodoMVC. The
most important are:

* `src/lib.rs`: The entry point to the application.
* `src/todos.rs`: Definition of `Todos` model and its rendering.
* `src/todo.rs`: Definition of `Todo` model and its rendering.
* `src/controller.rs`: The controller handles UI interactions and translates
them into updates on the model. Finally, it triggers re-rendering after those
updates.
* `src/router.rs`: A simple URL hash-based router.

## Build

```
wasm-pack build --target no-modules
```

## Serve

Use any HTTP server, for example:

```
python -m SimpleHTTPServer
```
21 changes: 21 additions & 0 deletions examples/dodrio/todomvc/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>dodrio • TodoMVC</title>
<link rel="stylesheet" href="https://unpkg.com/todomvc-common@^1.0.0/base.css">
<link rel="stylesheet" href="https://unpkg.com/todomvc-app-css@^2.0.0/index.css">
</head>
<body>
<section class="todoapp">
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
</footer>
<script src="pkg/dodrio_todomvc.js"></script>
<script>
wasm_bindgen("pkg/dodrio_todomvc_bg.wasm");
</script>
</body>
</html>
140 changes: 140 additions & 0 deletions examples/dodrio/todomvc/src/controller.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//! The controller handles UI events, translates them into updates on the model,
//! and schedules re-renders.
use crate::todo::{Todo, TodoActions};
use crate::todos::{Todos, TodosActions};
use crate::visibility::Visibility;
use dodrio::{RootRender, VdomWeak};
use serde::{Deserialize, Serialize};
use std::ops::{Deref, DerefMut};

/// The controller for the TodoMVC app.
///
/// This `Controller` struct is never actually instantiated. It is only used for
/// its `*Actions` trait implementations, none of which take a `self` parameter.
///
/// One could imagine alternative controller implementations with `*Actions`
/// trait implementations for (e.g.) testing that will assert various expected
/// action methods are called after rendering todo items and sending DOM events.
#[derive(Default, Deserialize, Serialize)]
pub struct Controller;

impl TodosActions for Controller {
fn toggle_all(root: &mut dyn RootRender, vdom: VdomWeak) {
let mut todos = AutoCommitTodos::new(root, vdom);
let all_complete = todos.todos().iter().all(|t| t.is_complete());
for t in todos.todos_mut() {
t.set_complete(!all_complete);
}
}

fn update_draft(root: &mut dyn RootRender, vdom: VdomWeak, draft: String) {
let mut todos = AutoCommitTodos::new(root, vdom);
todos.set_draft(draft);
}

fn finish_draft(root: &mut dyn RootRender, vdom: VdomWeak) {
let mut todos = AutoCommitTodos::new(root, vdom);
let title = todos.take_draft();
let title = title.trim();
if !title.is_empty() {
let id = todos.todos().len();
let new = Todo::new(id, title);
todos.add_todo(new);
}
}

fn change_visibility(root: &mut dyn RootRender, vdom: VdomWeak, vis: Visibility) {
let mut todos = AutoCommitTodos::new(root, vdom);
todos.set_visibility(vis);
}

fn delete_completed(root: &mut dyn RootRender, vdom: VdomWeak) {
let mut todos = AutoCommitTodos::new(root, vdom);
todos.delete_completed();
}
}

impl TodoActions for Controller {
fn toggle_completed(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
let mut todos = AutoCommitTodos::new(root, vdom);
let t = &mut todos.todos_mut()[id];
let completed = t.is_complete();
t.set_complete(!completed);
}

fn delete(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
let mut todos = AutoCommitTodos::new(root, vdom);
todos.delete_todo(id);
}

fn begin_editing(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
let mut todos = AutoCommitTodos::new(root, vdom);
let t = &mut todos.todos_mut()[id];
let desc = t.title().to_string();
t.set_edits(Some(desc));
}

fn update_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize, edits: String) {
let mut todos = AutoCommitTodos::new(root, vdom);
let t = &mut todos.todos_mut()[id];
t.set_edits(Some(edits));
}

fn finish_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
let mut todos = AutoCommitTodos::new(root, vdom);
let t = &mut todos.todos_mut()[id];
if let Some(edits) = t.take_edits() {
let edits = edits.trim();
if edits.is_empty() {
todos.delete_todo(id);
} else {
t.set_title(edits);
}
}
}

fn cancel_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
let mut todos = AutoCommitTodos::new(root, vdom);
let t = &mut todos.todos_mut()[id];
let _ = t.take_edits();
}
}

/// An RAII type that dereferences to a `Todos` and once it is dropped, saves
/// the (presumably just modified) todos to local storage, and schedules a new
/// `dodrio` render.
pub struct AutoCommitTodos<'a> {
todos: &'a mut Todos,
vdom: VdomWeak,
}

impl AutoCommitTodos<'_> {
/// Construct a new `AutoCommitTodos` from the root rendering component and
/// `vdom` handle.
pub fn new(root: &mut dyn RootRender, vdom: VdomWeak) -> AutoCommitTodos {
let todos = root.unwrap_mut::<Todos>();
AutoCommitTodos { todos, vdom }
}
}

impl Drop for AutoCommitTodos<'_> {
fn drop(&mut self) {
self.todos.save_to_local_storage();
self.vdom.schedule_render();
}
}

impl Deref for AutoCommitTodos<'_> {
type Target = Todos;

fn deref(&self) -> &Todos {
&self.todos
}
}

impl DerefMut for AutoCommitTodos<'_> {
fn deref_mut(&mut self) -> &mut Todos {
&mut self.todos
}
}
7 changes: 7 additions & 0 deletions examples/dodrio/todomvc/src/keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Constants for `KeyboardEvent::key_code`.`
/// The key code for the enter key.
pub const ENTER: u32 = 13;

/// The key code for the escape key.
pub const ESCAPE: u32 = 27;
61 changes: 61 additions & 0 deletions examples/dodrio/todomvc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! TodoMVC implemented in `dodrio`!
#![recursion_limit = "1024"]
#![deny(missing_docs)]

pub mod controller;
pub mod keys;
pub mod router;
pub mod todo;
pub mod todos;
pub mod utils;
pub mod visibility;

use crate::controller::Controller;
use crate::todos::Todos;
use dodrio::Vdom;
use wasm_bindgen::prelude::*;

/// Run the TodoMVC app!
///
/// Since this is marked `#[wasm_bindgen(start)]` it is automatically invoked
/// once the wasm module instantiated on the Web page.
#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
// Set up the logging for debugging if/when things go wrong.
init_logging();

// Grab the todo app container.
let document = utils::document();
let container = document
.query_selector(".todoapp")?
.ok_or_else(|| js_sys::Error::new("could not find `.todoapp` container"))?;

// Create a new `Todos` render component.
let todos = Todos::<Controller>::new();

// Create a virtual DOM and mount it and the `Todos` render component.
let vdom = Vdom::new(&container, todos);

// Start the URL router.
router::start(vdom.weak());

// Run the virtual DOM forever and don't unmount it.
vdom.forget();

Ok(())
}

cfg_if::cfg_if! {
if #[cfg(feature = "logging")] {
fn init_logging() {
console_error_panic_hook::set_once();
console_log::init_with_level(log::Level::Trace)
.expect_throw("should initialize logging OK");
}
} else {
fn init_logging() {
// Do nothing.
}
}
}
Loading

0 comments on commit a39ccf7

Please sign in to comment.