Merge pull request #38 from bodil/dodrio
Dodrio support with a bespoke macro.
This commit is contained in:
commit
359a7a66ed
|
@ -4,5 +4,7 @@ members = [
|
|||
"macros",
|
||||
"examples/stdweb",
|
||||
"examples/rocket",
|
||||
"examples/dodrio/counter",
|
||||
"examples/dodrio/todomvc",
|
||||
"ui",
|
||||
]
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "dodrio-counter"
|
||||
version = "0.1.0"
|
||||
authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1.6"
|
||||
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"] }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.15"
|
||||
features = [
|
||||
"console",
|
||||
"Document",
|
||||
"Event",
|
||||
"EventTarget",
|
||||
"HtmlElement",
|
||||
"MouseEvent",
|
||||
"Node",
|
||||
"Window",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.2.38"
|
|
@ -0,0 +1,21 @@
|
|||
# Counter
|
||||
|
||||
A counter that can be incremented and decremented.
|
||||
|
||||
## Source
|
||||
|
||||
See `src/lib.rs`.
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
wasm-pack build --target no-modules
|
||||
```
|
||||
|
||||
## Serve
|
||||
|
||||
Use any HTTP server, for example:
|
||||
|
||||
```
|
||||
python -m SimpleHTTPServer
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<title>Counter</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="pkg/dodrio_counter.js"></script>
|
||||
<script>
|
||||
wasm_bindgen("pkg/dodrio_counter_bg.wasm");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,86 @@
|
|||
#![recursion_limit = "128"]
|
||||
|
||||
use dodrio::builder::text;
|
||||
use dodrio::bumpalo::{self, Bump};
|
||||
use dodrio::Render;
|
||||
use log::*;
|
||||
use typed_html::dodrio;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// A counter that can be incremented and decrmented!
|
||||
struct Counter {
|
||||
count: isize,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
/// Construct a new, zeroed counter.
|
||||
fn new() -> Counter {
|
||||
Counter { count: 0 }
|
||||
}
|
||||
|
||||
/// Increment this counter's count.
|
||||
fn increment(&mut self) {
|
||||
self.count += 1;
|
||||
}
|
||||
|
||||
/// Decrement this counter's count.
|
||||
fn decrement(&mut self) {
|
||||
self.count -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// The `Render` implementation for `Counter`s displays the current count and has
|
||||
// buttons to increment and decrement the count.
|
||||
impl Render for Counter {
|
||||
fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump>
|
||||
where
|
||||
'a: 'bump,
|
||||
{
|
||||
// Stringify the count as a bump-allocated string.
|
||||
let count = bumpalo::format!(in bump, "{}", self.count);
|
||||
|
||||
dodrio!(bump,
|
||||
<div id="counter">
|
||||
<button onclick={|root, vdom, _event| {
|
||||
// Cast the root render component to a `Counter`, since
|
||||
// we know that's what it is.
|
||||
let counter = root.unwrap_mut::<Counter>();
|
||||
|
||||
// Increment the counter.
|
||||
counter.increment();
|
||||
|
||||
// Since the count has updated, we should re-render the
|
||||
// counter on the next animation frame.
|
||||
vdom.schedule_render();
|
||||
}}>"+"</button>
|
||||
{ vec![text(count.into_bump_str())] }
|
||||
<button onclick={|root, vdom, _event| {
|
||||
// Same as above, but decrementing instead of incrementing.
|
||||
root.unwrap_mut::<Counter>().decrement();
|
||||
vdom.schedule_render();
|
||||
}}>"-"</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run() {
|
||||
// Initialize debug logging for if/when things go wrong.
|
||||
console_error_panic_hook::set_once();
|
||||
console_log::init_with_level(Level::Trace).expect("should initialize logging OK");
|
||||
|
||||
// Get the document's `<body>`.
|
||||
let window = web_sys::window().unwrap();
|
||||
let document = window.document().unwrap();
|
||||
let body = document.body().unwrap();
|
||||
|
||||
// Construct a new counter component.
|
||||
let counter = Counter::new();
|
||||
|
||||
// Mount our counter component to the `<body>`.
|
||||
let vdom = dodrio::Vdom::new(&body, counter);
|
||||
|
||||
// Run the virtual DOM and its listeners forever.
|
||||
vdom.forget();
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
[package]
|
||||
authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
|
||||
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"]
|
|
@ -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
|
||||
```
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
//! A simple `#`-fragment router.
|
||||
|
||||
use crate::todos::Todos;
|
||||
use crate::utils;
|
||||
use crate::visibility::Visibility;
|
||||
use dodrio::VdomWeak;
|
||||
use futures::prelude::*;
|
||||
use std::str::FromStr;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
/// Start the router.
|
||||
pub fn start(vdom: VdomWeak) {
|
||||
// Callback fired whenever the URL's hash fragment changes. Keeps the root
|
||||
// todos collection's visibility in sync with the `#` fragment.
|
||||
let on_hash_change = move || {
|
||||
let new_vis = utils::hash()
|
||||
.and_then(|hash| {
|
||||
if hash.starts_with("#/") {
|
||||
Visibility::from_str(&hash[2..]).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
// If we couldn't parse a visibility, make sure we canonicalize
|
||||
// it back to our default hash.
|
||||
let v = Visibility::default();
|
||||
utils::set_hash(&format!("#/{}", v));
|
||||
v
|
||||
});
|
||||
|
||||
wasm_bindgen_futures::spawn_local(
|
||||
vdom.with_component({
|
||||
let vdom = vdom.clone();
|
||||
move |root| {
|
||||
let todos = root.unwrap_mut::<Todos>();
|
||||
// If the todos' visibility already matches the event's
|
||||
// visibility, then there is nothing to do (ha). If they
|
||||
// don't match, then we need to update the todos' visibility
|
||||
// and re-render.
|
||||
if todos.visibility() != new_vis {
|
||||
todos.set_visibility(new_vis);
|
||||
vdom.schedule_render();
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_err(|_| ()),
|
||||
);
|
||||
};
|
||||
|
||||
// Call it once to handle the initial `#` fragment.
|
||||
on_hash_change();
|
||||
|
||||
// Now listen for hash changes forever.
|
||||
//
|
||||
// Note that if we ever intended to unmount our todos app, we would want to
|
||||
// provide a method for removing this router's event listener and cleaning
|
||||
// up after ourselves.
|
||||
let on_hash_change = Closure::wrap(Box::new(on_hash_change) as Box<FnMut()>);
|
||||
let window = utils::window();
|
||||
window
|
||||
.add_event_listener_with_callback("hashchange", on_hash_change.as_ref().unchecked_ref())
|
||||
.unwrap_throw();
|
||||
on_hash_change.forget();
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
//! Type definition and `dodrio::Render` implementation for a single todo item.
|
||||
|
||||
use crate::keys;
|
||||
use dodrio::{bumpalo::Bump, Node, Render, RootRender, VdomWeak};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::marker::PhantomData;
|
||||
use typed_html::dodrio;
|
||||
use wasm_bindgen::{prelude::*, JsCast};
|
||||
|
||||
/// A single todo item.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Todo<C> {
|
||||
id: usize,
|
||||
title: String,
|
||||
completed: bool,
|
||||
|
||||
#[serde(skip)]
|
||||
edits: Option<String>,
|
||||
|
||||
#[serde(skip)]
|
||||
_controller: PhantomData<C>,
|
||||
}
|
||||
|
||||
/// Actions on a single todo item that can be triggered from the UI.
|
||||
pub trait TodoActions {
|
||||
/// Toggle the completion state of the todo item with the given id.
|
||||
fn toggle_completed(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
|
||||
|
||||
/// Delete the todo item with the given id.
|
||||
fn delete(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
|
||||
|
||||
/// Begin editing the todo item with the given id.
|
||||
fn begin_editing(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
|
||||
|
||||
/// Update the edits for the todo with the given id.
|
||||
fn update_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize, edits: String);
|
||||
|
||||
/// Finish editing the todo with the given id.
|
||||
fn finish_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
|
||||
|
||||
/// Cancel editing the todo with the given id.
|
||||
fn cancel_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
|
||||
}
|
||||
|
||||
impl<C> Todo<C> {
|
||||
/// Construct a new `Todo` with the given identifier and title.
|
||||
pub fn new<S: Into<String>>(id: usize, title: S) -> Self {
|
||||
let title = title.into();
|
||||
let completed = false;
|
||||
let edits = None;
|
||||
Todo {
|
||||
id,
|
||||
title,
|
||||
completed,
|
||||
edits,
|
||||
_controller: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set this todo item's id.
|
||||
pub fn set_id(&mut self, id: usize) {
|
||||
self.id = id;
|
||||
}
|
||||
|
||||
/// Is this `Todo` complete?
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.completed
|
||||
}
|
||||
|
||||
/// Mark the `Todo` as complete or not.
|
||||
pub fn set_complete(&mut self, to: bool) {
|
||||
self.completed = to;
|
||||
}
|
||||
|
||||
/// Get this todo's title.
|
||||
pub fn title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
/// Set this todo item's title.
|
||||
pub fn set_title<S: Into<String>>(&mut self, title: S) {
|
||||
self.title = title.into();
|
||||
}
|
||||
|
||||
/// Set the edits for this todo.
|
||||
pub fn set_edits<S: Into<String>>(&mut self, edits: Option<S>) {
|
||||
self.edits = edits.map(Into::into);
|
||||
}
|
||||
|
||||
/// Take this todo's edits, leaving `None` in their place.
|
||||
pub fn take_edits(&mut self) -> Option<String> {
|
||||
self.edits.take()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: TodoActions> Render for Todo<C> {
|
||||
fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
|
||||
where
|
||||
'a: 'bump,
|
||||
{
|
||||
use dodrio::{builder::text, bumpalo};
|
||||
use typed_html::types::ClassList;
|
||||
|
||||
let id = self.id;
|
||||
let title = self.edits.as_ref().unwrap_or(&self.title);
|
||||
|
||||
dodrio!(bump,
|
||||
<li class={
|
||||
let mut class = ClassList::new();
|
||||
if self.completed {
|
||||
class.add("completed");
|
||||
}
|
||||
if self.edits.is_some() {
|
||||
class.add("editing");
|
||||
}
|
||||
class
|
||||
}>
|
||||
<div class="view">
|
||||
<input class="toggle" type="checkbox" checked={self.completed}
|
||||
onclick={move |root, vdom, _event| {
|
||||
C::toggle_completed(root, vdom, id)
|
||||
}}
|
||||
/>
|
||||
<label ondblclick={move |root, vdom, _event| {
|
||||
C::begin_editing(root, vdom, id)
|
||||
}}>
|
||||
{ bumpalo::vec![in ≎ text(title)] }
|
||||
</label>
|
||||
<button class="destroy" onclick={move |root, vdom, _event| {
|
||||
C::delete(root, vdom, id)
|
||||
}}/>
|
||||
</div>
|
||||
<input class="edit" value={title.as_str()} name="title" id={
|
||||
bumpalo::format!(in bump, "todo-{}", id).into_bump_str()
|
||||
} oninput={move |root, vdom, event| {
|
||||
let input = event
|
||||
.target()
|
||||
.unwrap_throw()
|
||||
.unchecked_into::<web_sys::HtmlInputElement>();
|
||||
C::update_edits(root, vdom, id, input.value());
|
||||
}} onblur={move |root, vdom, _event| {
|
||||
C::finish_edits(root, vdom, id)
|
||||
}} onkeydown={move |root, vdom, event| {
|
||||
let event = event.unchecked_into::<web_sys::KeyboardEvent>();
|
||||
match event.key_code() {
|
||||
keys::ENTER => C::finish_edits(root, vdom, id),
|
||||
keys::ESCAPE => C::cancel_edits(root, vdom, id),
|
||||
_ => {}
|
||||
}
|
||||
}}/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,280 @@
|
|||
//! Type definitions and `dodrio::Render` implementation for a collection of
|
||||
//! todo items.
|
||||
|
||||
use crate::controller::Controller;
|
||||
use crate::todo::{Todo, TodoActions};
|
||||
use crate::visibility::Visibility;
|
||||
use crate::{keys, utils};
|
||||
use dodrio::{
|
||||
builder::text,
|
||||
bumpalo::{self, Bump},
|
||||
Node, Render, RootRender, VdomWeak,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::marker::PhantomData;
|
||||
use std::mem;
|
||||
use typed_html::dodrio;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
/// A collection of todos.
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[serde(rename = "todos-dodrio", bound = "")]
|
||||
pub struct Todos<C = Controller> {
|
||||
todos: Vec<Todo<C>>,
|
||||
|
||||
#[serde(skip)]
|
||||
draft: String,
|
||||
|
||||
#[serde(skip)]
|
||||
visibility: Visibility,
|
||||
|
||||
#[serde(skip)]
|
||||
_controller: PhantomData<C>,
|
||||
}
|
||||
|
||||
/// Actions for `Todos` that can be triggered by UI interactions.
|
||||
pub trait TodosActions: TodoActions {
|
||||
/// Toggle the completion state of all todo items.
|
||||
fn toggle_all(root: &mut dyn RootRender, vdom: VdomWeak);
|
||||
|
||||
/// Update the draft todo item's text.
|
||||
fn update_draft(root: &mut dyn RootRender, vdom: VdomWeak, draft: String);
|
||||
|
||||
/// Finish the current draft todo item and add it to the collection of
|
||||
/// todos.
|
||||
fn finish_draft(root: &mut dyn RootRender, vdom: VdomWeak);
|
||||
|
||||
/// Change the todo item visibility filtering to the given `Visibility`.
|
||||
fn change_visibility(root: &mut dyn RootRender, vdom: VdomWeak, vis: Visibility);
|
||||
|
||||
/// Delete all completed todo items.
|
||||
fn delete_completed(root: &mut dyn RootRender, vdom: VdomWeak);
|
||||
}
|
||||
|
||||
impl<C> Todos<C> {
|
||||
/// Construct a new todos set.
|
||||
///
|
||||
/// If an existing set is available in local storage, then us that,
|
||||
/// otherwise create a new set.
|
||||
pub fn new() -> Self
|
||||
where
|
||||
C: Default,
|
||||
{
|
||||
Self::from_local_storage().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Deserialize a set of todos from local storage.
|
||||
pub fn from_local_storage() -> Option<Self> {
|
||||
utils::local_storage()
|
||||
.get("todomvc-dodrio")
|
||||
.ok()
|
||||
.and_then(|opt| opt)
|
||||
.and_then(|json| serde_json::from_str(&json).ok())
|
||||
}
|
||||
|
||||
/// Serialize this set of todos to local storage.
|
||||
pub fn save_to_local_storage(&self) {
|
||||
let serialized = serde_json::to_string(self).unwrap_throw();
|
||||
utils::local_storage()
|
||||
.set("todomvc-dodrio", &serialized)
|
||||
.unwrap_throw();
|
||||
}
|
||||
|
||||
/// Add a new todo item to this collection.
|
||||
pub fn add_todo(&mut self, todo: Todo<C>) {
|
||||
self.todos.push(todo);
|
||||
}
|
||||
|
||||
/// Delete the todo with the given id.
|
||||
pub fn delete_todo(&mut self, id: usize) {
|
||||
self.todos.remove(id);
|
||||
self.fix_ids();
|
||||
}
|
||||
|
||||
/// Delete all completed todo items.
|
||||
pub fn delete_completed(&mut self) {
|
||||
self.todos.retain(|t| !t.is_complete());
|
||||
self.fix_ids();
|
||||
}
|
||||
|
||||
// Fix all todo identifiers so that they match their index once again.
|
||||
fn fix_ids(&mut self) {
|
||||
for (id, todo) in self.todos.iter_mut().enumerate() {
|
||||
todo.set_id(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a shared slice of the underlying set of todo items.
|
||||
pub fn todos(&self) -> &[Todo<C>] {
|
||||
&self.todos
|
||||
}
|
||||
|
||||
/// Get an exclusive slice of the underlying set of todo items.
|
||||
pub fn todos_mut(&mut self) -> &mut [Todo<C>] {
|
||||
&mut self.todos
|
||||
}
|
||||
|
||||
/// Set the draft todo item text.
|
||||
pub fn set_draft<S: Into<String>>(&mut self, draft: S) {
|
||||
self.draft = draft.into();
|
||||
}
|
||||
|
||||
/// Take the current draft text and replace it with an empty string.
|
||||
pub fn take_draft(&mut self) -> String {
|
||||
mem::replace(&mut self.draft, String::new())
|
||||
}
|
||||
|
||||
/// Get the current visibility for these todos.
|
||||
pub fn visibility(&self) -> Visibility {
|
||||
self.visibility
|
||||
}
|
||||
|
||||
/// Set the visibility for these todoS.
|
||||
pub fn set_visibility(&mut self, vis: Visibility) {
|
||||
self.visibility = vis;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rendering helpers.
|
||||
impl<C: TodosActions> Todos<C> {
|
||||
fn header<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
|
||||
where
|
||||
'a: 'bump,
|
||||
{
|
||||
dodrio!(bump,
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input oninput={|root, vdom, event| {
|
||||
let input = event
|
||||
.target()
|
||||
.unwrap_throw()
|
||||
.unchecked_into::<web_sys::HtmlInputElement>();
|
||||
C::update_draft(root, vdom, input.value());
|
||||
}} onkeydown={|root, vdom, event| {
|
||||
let event = event.unchecked_into::<web_sys::KeyboardEvent>();
|
||||
if event.key_code() == keys::ENTER {
|
||||
C::finish_draft(root, vdom);
|
||||
}
|
||||
}} class="new-todo" placeholder="What needs to be done?" autofocus=true value={self.draft.as_str()}/>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
fn todos_list<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
|
||||
where
|
||||
'a: 'bump,
|
||||
{
|
||||
use dodrio::bumpalo::collections::Vec;
|
||||
|
||||
let mut todos = Vec::with_capacity_in(self.todos.len(), bump);
|
||||
todos.extend(
|
||||
self.todos
|
||||
.iter()
|
||||
.filter(|t| match self.visibility {
|
||||
Visibility::All => true,
|
||||
Visibility::Active => !t.is_complete(),
|
||||
Visibility::Completed => t.is_complete(),
|
||||
})
|
||||
.map(|t| t.render(bump)),
|
||||
);
|
||||
|
||||
dodrio!(bump,
|
||||
<section class="main" style={
|
||||
if self.todos.is_empty() {
|
||||
"visibility: hidden"
|
||||
} else {
|
||||
"visibility: visible"
|
||||
}
|
||||
}>
|
||||
<input
|
||||
class="toggle-all" id="toggle-all" type="checkbox" name="toggle"
|
||||
checked={self.todos.iter().all(|t| t.is_complete())}
|
||||
onclick={|root, vdom, _event| C::toggle_all(root, vdom)}
|
||||
/>
|
||||
<label for="toggle-all">"Mark as complete"</label>
|
||||
<ul class="todo-list">
|
||||
{ todos }
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
fn footer<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
|
||||
where
|
||||
'a: 'bump,
|
||||
{
|
||||
let completed_count = self.todos.iter().filter(|t| t.is_complete()).count();
|
||||
let incomplete_count = self.todos.len() - completed_count;
|
||||
let items_left = if incomplete_count == 1 {
|
||||
" item left"
|
||||
} else {
|
||||
" items left"
|
||||
};
|
||||
let incomplete_count = bumpalo::format!(in bump, "{}", incomplete_count);
|
||||
|
||||
let clear_completed_text = bumpalo::format!(
|
||||
in bump,
|
||||
"Clear completed ({})",
|
||||
self.todos.iter().filter(|t| t.is_complete()).count()
|
||||
);
|
||||
|
||||
dodrio!(bump,
|
||||
<footer class="footer" hidden={self.todos.is_empty()}>
|
||||
<span class="todo-count">
|
||||
<strong>{
|
||||
bumpalo::vec![in ≎ text(incomplete_count.into_bump_str())]
|
||||
}</strong>
|
||||
{ bumpalo::vec![in ≎ text(items_left)] }
|
||||
</span>
|
||||
<ul class="filters">
|
||||
{ bumpalo::vec![in ≎
|
||||
self.visibility_swap(bump, "#/", Visibility::All),
|
||||
self.visibility_swap(bump, "#/active", Visibility::Active),
|
||||
self.visibility_swap(bump, "#/completed", Visibility::Completed)
|
||||
] }
|
||||
</ul>
|
||||
<button class="clear-completed" hidden={completed_count == 0} onclick={|root, vdom, _event| {
|
||||
C::delete_completed(root, vdom);
|
||||
}}>{ bumpalo::vec![in ≎ text(clear_completed_text.into_bump_str())] }</button>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
fn visibility_swap<'a, 'bump>(
|
||||
&'a self,
|
||||
bump: &'bump Bump,
|
||||
url: &'static str,
|
||||
target_vis: Visibility,
|
||||
) -> Node<'bump>
|
||||
where
|
||||
'a: 'bump,
|
||||
{
|
||||
dodrio!(bump,
|
||||
<li onclick={move |root, vdom, _event| {
|
||||
C::change_visibility(root, vdom, target_vis);
|
||||
}}>
|
||||
<a href={url} class={
|
||||
if self.visibility == target_vis {
|
||||
"selected"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}>{ bumpalo::vec![in ≎ text(target_vis.label())] }</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: TodosActions> Render for Todos<C> {
|
||||
fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
|
||||
where
|
||||
'a: 'bump,
|
||||
{
|
||||
dodrio!(bump,
|
||||
<div>{ bumpalo::vec![in ≎
|
||||
self.header(bump), self.todos_list(bump), self.footer(bump)
|
||||
] }</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
//! Small utility functions.
|
||||
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
/// Get the top-level window.
|
||||
pub fn window() -> web_sys::Window {
|
||||
web_sys::window().unwrap_throw()
|
||||
}
|
||||
|
||||
/// Get the current location hash, if any.
|
||||
pub fn hash() -> Option<String> {
|
||||
window()
|
||||
.location()
|
||||
.hash()
|
||||
.ok()
|
||||
.and_then(|h| if h.is_empty() { None } else { Some(h) })
|
||||
}
|
||||
|
||||
/// Set the current location hash.
|
||||
pub fn set_hash(hash: &str) {
|
||||
window().location().set_hash(hash).unwrap_throw();
|
||||
}
|
||||
|
||||
/// Get the top-level document.
|
||||
pub fn document() -> web_sys::Document {
|
||||
window().document().unwrap_throw()
|
||||
}
|
||||
|
||||
/// Get the top-level window's local storage.
|
||||
pub fn local_storage() -> web_sys::Storage {
|
||||
window().local_storage().unwrap_throw().unwrap_throw()
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
//! Visibility filtering.
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// The visibility filtering for todo items.
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Visibility {
|
||||
/// Show all todos.
|
||||
All,
|
||||
/// Show only active, incomplete todos.
|
||||
Active,
|
||||
/// Show only inactive, completed todos.
|
||||
Completed,
|
||||
}
|
||||
|
||||
impl Default for Visibility {
|
||||
fn default() -> Visibility {
|
||||
Visibility::All
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Visibility {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"all" => Ok(Visibility::All),
|
||||
"active" => Ok(Visibility::Active),
|
||||
"completed" => Ok(Visibility::Completed),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Visibility {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.label().to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
impl Visibility {
|
||||
/// Get a string label for this visibility.
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Visibility::All => "All",
|
||||
Visibility::Active => "Active",
|
||||
Visibility::Completed => "Completed",
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,3 +25,6 @@ quote = "0.6.10"
|
|||
[build-dependencies]
|
||||
lalrpop = "0.16.1"
|
||||
version_check = "0.1.5"
|
||||
|
||||
[features]
|
||||
dodrio = []
|
||||
|
|
|
@ -21,11 +21,11 @@ pub fn global_attrs(span: Span) -> StringyMap<Ident, TokenStream> {
|
|||
|
||||
insert("accesskey", "String");
|
||||
insert("autocapitalize", "String");
|
||||
insert("contenteditable", "bool");
|
||||
insert("contenteditable", "crate::types::Bool");
|
||||
insert("contextmenu", "crate::types::Id");
|
||||
insert("dir", "crate::types::TextDirection");
|
||||
insert("draggable", "bool");
|
||||
insert("hidden", "bool");
|
||||
insert("draggable", "crate::types::Bool");
|
||||
insert("hidden", "crate::types::Bool");
|
||||
insert("is", "String");
|
||||
insert("lang", "crate::types::LanguageTag");
|
||||
insert("style", "String");
|
||||
|
|
|
@ -174,6 +174,10 @@ pub NodeWithType: (Node, Option<Vec<Token>>) = {
|
|||
},
|
||||
};
|
||||
|
||||
pub NodeWithBump: (Ident, Node) = {
|
||||
<Ident> "," <Node>,
|
||||
};
|
||||
|
||||
|
||||
// The declare macro
|
||||
|
||||
|
|
|
@ -60,6 +60,26 @@ impl Node {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dodrio")]
|
||||
pub fn into_dodrio_token_stream(
|
||||
self,
|
||||
bump: &Ident,
|
||||
is_req_child: bool,
|
||||
) -> Result<TokenStream, TokenStream> {
|
||||
match self {
|
||||
Node::Element(el) => el.into_dodrio_token_stream(bump, is_req_child),
|
||||
Node::Text(text) => Ok(dodrio_text_node(text)),
|
||||
Node::Block(group) => {
|
||||
let span = group.span();
|
||||
let error =
|
||||
"you cannot use a block as a top level element or a required child element";
|
||||
Err(quote_spanned! { span=>
|
||||
compile_error! { #error }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -69,6 +89,12 @@ pub struct Element {
|
|||
pub children: Vec<Node>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "dodrio")]
|
||||
fn dodrio_text_node(text: Literal) -> TokenStream {
|
||||
let text = TokenTree::Literal(text);
|
||||
quote!(dodrio::builder::text(#text))
|
||||
}
|
||||
|
||||
fn extract_data_attrs(attrs: &mut StringyMap<Ident, TokenTree>) -> StringyMap<String, TokenTree> {
|
||||
let mut data = StringyMap::new();
|
||||
let keys: Vec<Ident> = attrs.keys().cloned().collect();
|
||||
|
@ -119,6 +145,16 @@ fn is_string_literal(literal: &Literal) -> bool {
|
|||
literal.to_string().starts_with('"')
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn stringify_ident(ident: &Ident) -> String {
|
||||
let s = ident.to_string();
|
||||
if s.starts_with("r#") {
|
||||
s[2..].to_string()
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
impl Element {
|
||||
fn into_token_stream(mut self, ty: &Option<Vec<Token>>) -> Result<TokenStream, TokenStream> {
|
||||
let name = self.name;
|
||||
|
@ -249,9 +285,233 @@ impl Element {
|
|||
}
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "dodrio")]
|
||||
fn into_dodrio_token_stream(
|
||||
mut self,
|
||||
bump: &Ident,
|
||||
is_req_child: bool,
|
||||
) -> Result<TokenStream, TokenStream> {
|
||||
let name = self.name;
|
||||
let name_str = stringify_ident(&name);
|
||||
let typename: TokenTree = ident::new_raw(&name_str, name.span()).into();
|
||||
let tag_name = TokenTree::from(Literal::string(&name_str));
|
||||
let req_names = required_children(&name_str);
|
||||
if req_names.len() > self.children.len() {
|
||||
let span = name.span();
|
||||
let error = format!(
|
||||
"<{}> requires {} children but there are only {}",
|
||||
name_str,
|
||||
req_names.len(),
|
||||
self.children.len()
|
||||
);
|
||||
return Err(quote_spanned! {span=>
|
||||
compile_error! { #error }
|
||||
});
|
||||
}
|
||||
let events = extract_event_handlers(&mut self.attributes);
|
||||
let data_attrs = extract_data_attrs(&mut self.attributes);
|
||||
let attrs = self.attributes.iter().map(|(key, value)| {
|
||||
(
|
||||
key.to_string(),
|
||||
TokenTree::Ident(ident::new_raw(&key.to_string(), key.span())),
|
||||
value,
|
||||
)
|
||||
});
|
||||
let opt_children = self.children.split_off(req_names.len());
|
||||
let req_children = self
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|node| node.into_dodrio_token_stream(bump, true))
|
||||
.collect::<Result<Vec<TokenStream>, TokenStream>>()?;
|
||||
|
||||
let mut set_attrs = TokenStream::new();
|
||||
|
||||
for (attr_str, key, value) in attrs {
|
||||
match value {
|
||||
TokenTree::Literal(lit) if is_string_literal(lit) => {
|
||||
let mut eprintln_msg = "ERROR: ".to_owned();
|
||||
#[cfg(can_show_location_of_runtime_parse_error)]
|
||||
{
|
||||
let span = lit.span();
|
||||
eprintln_msg += &format!(
|
||||
"{}:{}:{}: ",
|
||||
span.unstable()
|
||||
.source_file()
|
||||
.path()
|
||||
.to_str()
|
||||
.unwrap_or("unknown"),
|
||||
span.unstable().start().line,
|
||||
span.unstable().start().column
|
||||
);
|
||||
}
|
||||
eprintln_msg += &format!(
|
||||
"<{} {}={}> failed to parse attribute value: {{}}",
|
||||
name_str, attr_str, lit,
|
||||
);
|
||||
#[cfg(not(can_show_location_of_runtime_parse_error))]
|
||||
{
|
||||
eprintln_msg += "\nERROR: rebuild with nightly to print source location";
|
||||
}
|
||||
|
||||
set_attrs.extend(quote!(
|
||||
element.attrs.#key = Some(#lit.parse().unwrap_or_else(|err| {
|
||||
eprintln!(#eprintln_msg, err);
|
||||
panic!("failed to parse string literal");
|
||||
}));
|
||||
));
|
||||
}
|
||||
value => {
|
||||
let value = process_value(value);
|
||||
set_attrs.extend(quote!(
|
||||
element.attrs.#key = Some(std::convert::Into::into(#value));
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let attr_max_len = self.attributes.len() + data_attrs.len();
|
||||
let mut builder = quote!(
|
||||
let mut attr_list = dodrio::bumpalo::collections::Vec::with_capacity_in(#attr_max_len, #bump);
|
||||
);
|
||||
|
||||
// Build the attributes.
|
||||
for (key, _) in self.attributes.iter() {
|
||||
let key_str = stringify_ident(key);
|
||||
let key = ident::new_raw(&key_str, key.span());
|
||||
let key_str = TokenTree::from(Literal::string(&key_str));
|
||||
builder.extend(quote!(
|
||||
let attr_value = dodrio::bumpalo::format!(
|
||||
in &#bump, "{}", element.attrs.#key.unwrap());
|
||||
if !attr_value.is_empty() {
|
||||
attr_list.push(dodrio::builder::attr(#key_str, attr_value.into_bump_str()));
|
||||
}
|
||||
));
|
||||
}
|
||||
for (key, value) in data_attrs
|
||||
.iter()
|
||||
.map(|(k, v)| (TokenTree::from(Literal::string(&k)), v.clone()))
|
||||
{
|
||||
builder.extend(quote!(
|
||||
attr_list.push(dodrio::builder::attr(
|
||||
#key,
|
||||
dodrio::bumpalo::format!(
|
||||
in &#bump, "{}", #value
|
||||
).into_bump_str()
|
||||
));
|
||||
));
|
||||
}
|
||||
|
||||
builder.extend(quote!(
|
||||
let mut node = dodrio::builder::ElementBuilder::new(#bump, #tag_name)
|
||||
.attributes(attr_list)
|
||||
));
|
||||
|
||||
// Build an array of event listeners.
|
||||
let mut event_array = TokenStream::new();
|
||||
for (key, value) in events.iter() {
|
||||
let key = TokenTree::from(Literal::string(&stringify_ident(key)));
|
||||
let value = process_value(value);
|
||||
event_array.extend(quote!(
|
||||
dodrio::builder::on(&#bump, #key, #value),
|
||||
));
|
||||
}
|
||||
builder.extend(quote!(
|
||||
.listeners([#event_array]);
|
||||
));
|
||||
|
||||
// And finally an array of children, or a stream of builder commands
|
||||
// if we have a group inside the child list.
|
||||
let mut child_array = TokenStream::new();
|
||||
let mut child_builder = TokenStream::new();
|
||||
let mut static_children = true;
|
||||
|
||||
// Walk through required children and build them inline.
|
||||
let mut make_req_children = TokenStream::new();
|
||||
let mut arg_list = Vec::new();
|
||||
for (index, child) in req_children.into_iter().enumerate() {
|
||||
let req_child = TokenTree::from(ident::new_raw(
|
||||
&format!("req_child_{}", index),
|
||||
Span::call_site(),
|
||||
));
|
||||
let child_node = TokenTree::from(ident::new_raw(
|
||||
&format!("child_node_{}", index),
|
||||
Span::call_site(),
|
||||
));
|
||||
make_req_children.extend(quote!(
|
||||
let (#req_child, #child_node) = #child;
|
||||
));
|
||||
child_array.extend(quote!(
|
||||
#child_node,
|
||||
));
|
||||
child_builder.extend(quote!(
|
||||
node = node.child(#child_node);
|
||||
));
|
||||
arg_list.push(req_child);
|
||||
}
|
||||
|
||||
// Build optional children, test if we have groups.
|
||||
for child_node in opt_children {
|
||||
let child = match child_node {
|
||||
Node::Text(text) => dodrio_text_node(text),
|
||||
Node::Element(el) => el.into_dodrio_token_stream(bump, false)?,
|
||||
Node::Block(group) => {
|
||||
static_children = false;
|
||||
let group: TokenTree = group.into();
|
||||
child_builder.extend(quote!(
|
||||
for child in #group.into_iter() {
|
||||
node = node.child(child);
|
||||
}
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
child_array.extend(quote!(
|
||||
#child,
|
||||
));
|
||||
child_builder.extend(quote!(
|
||||
node = node.child(#child);
|
||||
));
|
||||
}
|
||||
|
||||
if static_children {
|
||||
builder.extend(quote!(
|
||||
let node = node.children([#child_array]);
|
||||
));
|
||||
} else {
|
||||
builder.extend(child_builder);
|
||||
}
|
||||
builder.extend(quote!(node.finish()));
|
||||
|
||||
if is_req_child {
|
||||
builder = quote!(
|
||||
(element, {#builder})
|
||||
);
|
||||
}
|
||||
|
||||
let mut args = TokenStream::new();
|
||||
for arg in arg_list {
|
||||
args.extend(quote!( #arg, ));
|
||||
}
|
||||
|
||||
Ok(quote!(
|
||||
{
|
||||
#make_req_children
|
||||
let mut element: typed_html::elements::#typename<typed_html::output::dodrio::Dodrio> =
|
||||
typed_html::elements::#typename::new(#args);
|
||||
#set_attrs
|
||||
#builder
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME report a decent error when the macro contains multiple top level elements
|
||||
pub fn expand_html(input: &[Token]) -> Result<(Node, Option<Vec<Token>>), ParseError> {
|
||||
grammar::NodeWithTypeParser::new().parse(Lexer::new(input))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn expand_dodrio(input: &[Token]) -> Result<(Ident, Node), ParseError> {
|
||||
grammar::NodeWithBumpParser::new().parse(Lexer::new(input))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
#![recursion_limit = "128"]
|
||||
#![cfg_attr(can_show_location_of_runtime_parse_error, feature(proc_macro_span))]
|
||||
|
||||
extern crate ansi_term;
|
||||
extern crate lalrpop_util;
|
||||
extern crate proc_macro;
|
||||
extern crate proc_macro2;
|
||||
extern crate proc_macro_hack;
|
||||
extern crate quote;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro_hack::proc_macro_hack;
|
||||
|
@ -39,6 +34,26 @@ pub fn html(input: TokenStream) -> TokenStream {
|
|||
})
|
||||
}
|
||||
|
||||
/// Construct a Dodrio node.
|
||||
///
|
||||
/// See the crate documentation for [`typed_html`][typed_html].
|
||||
///
|
||||
/// [typed_html]: ../typed_html/index.html
|
||||
#[cfg(feature = "dodrio")]
|
||||
#[proc_macro_hack]
|
||||
pub fn dodrio(input: TokenStream) -> TokenStream {
|
||||
let stream = lexer::unroll_stream(input.into(), false);
|
||||
let result = html::expand_dodrio(&stream);
|
||||
TokenStream::from(match result {
|
||||
Err(err) => error::parse_error(&stream, &err),
|
||||
Ok((bump, node)) => match node.into_dodrio_token_stream(&bump, false) {
|
||||
Err(err) => err,
|
||||
// Ok(success) => {println!("{}", success); panic!()},
|
||||
Ok(success) => success,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// This macro is used by `typed_html` internally to generate types and
|
||||
/// implementations for HTML elements.
|
||||
#[proc_macro]
|
||||
|
|
|
@ -28,6 +28,11 @@ where
|
|||
pub fn keys(&self) -> impl Iterator<Item = &K> {
|
||||
self.0.values().map(|(k, _)| k)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V, OK, OV> From<Vec<(OK, OV)>> for StringyMap<K, V>
|
||||
|
|
|
@ -25,3 +25,8 @@ htmlescape = "0.3.1"
|
|||
proc-macro-hack = "0.5.4"
|
||||
proc-macro-nested = "0.1.3"
|
||||
stdweb = { version = "0.4.14", optional = true }
|
||||
dodrio = { version = "0.1.0", optional = true }
|
||||
web-sys = { version = "0.3.16", optional = true, features = ["Event", "Element"] }
|
||||
|
||||
[features]
|
||||
dodrio_macro = ["web-sys", "dodrio", "typed-html-macros/dodrio"]
|
||||
|
|
|
@ -100,11 +100,11 @@ declare_elements!{
|
|||
article in [FlowContent, SectioningContent] with FlowContent;
|
||||
aside in [FlowContent, SectioningContent] with FlowContent;
|
||||
audio {
|
||||
autoplay: bool,
|
||||
controls: bool,
|
||||
autoplay: Bool,
|
||||
controls: Bool,
|
||||
crossorigin: CrossOrigin,
|
||||
loop: bool,
|
||||
muted: bool,
|
||||
loop: Bool,
|
||||
muted: Bool,
|
||||
preload: Preload,
|
||||
src: Uri,
|
||||
} in [FlowContent, PhrasingContent, EmbeddedContent] with MediaContent;
|
||||
|
@ -116,13 +116,13 @@ declare_elements!{
|
|||
} in [FlowContent] with FlowContent;
|
||||
br in [FlowContent, PhrasingContent];
|
||||
button {
|
||||
autofocus: bool,
|
||||
disabled: bool,
|
||||
autofocus: Bool,
|
||||
disabled: Bool,
|
||||
form: Id,
|
||||
formaction: Uri,
|
||||
formenctype: FormEncodingType,
|
||||
formmethod: FormMethod,
|
||||
formnovalidate: bool,
|
||||
formnovalidate: Bool,
|
||||
formtarget: Target,
|
||||
name: Id,
|
||||
type: ButtonType,
|
||||
|
@ -143,7 +143,7 @@ declare_elements!{
|
|||
datetime: Datetime,
|
||||
} in [FlowContent, PhrasingContent] with FlowContent;
|
||||
details {
|
||||
open: bool,
|
||||
open: Bool,
|
||||
} in [FlowContent, SectioningContent, InteractiveContent] with [summary] FlowContent;
|
||||
dfn in [FlowContent, PhrasingContent] with PhrasingContent;
|
||||
div in [FlowContent] with FlowContent;
|
||||
|
@ -167,7 +167,7 @@ declare_elements!{
|
|||
enctype: FormEncodingType,
|
||||
method: FormMethod,
|
||||
name: Id,
|
||||
novalidate: bool,
|
||||
novalidate: Bool,
|
||||
target: Target,
|
||||
} in [FlowContent] with FlowContent;
|
||||
h1 in [FlowContent, HeadingContent, HGroupContent] with PhrasingContent;
|
||||
|
@ -182,8 +182,8 @@ declare_elements!{
|
|||
i in [FlowContent, PhrasingContent] with PhrasingContent;
|
||||
iframe {
|
||||
allow: FeaturePolicy,
|
||||
allowfullscreen: bool,
|
||||
allowpaymentrequest: bool,
|
||||
allowfullscreen: Bool,
|
||||
allowpaymentrequest: Bool,
|
||||
height: usize,
|
||||
name: Id,
|
||||
referrerpolicy: ReferrerPolicy,
|
||||
|
@ -197,7 +197,7 @@ declare_elements!{
|
|||
crossorigin: CrossOrigin,
|
||||
decoding: ImageDecoding,
|
||||
height: usize,
|
||||
ismap: bool,
|
||||
ismap: Bool,
|
||||
sizes: SpacedList<String>, // FIXME it's not really just a string
|
||||
src: Uri,
|
||||
srcset: String, // FIXME this is much more complicated
|
||||
|
@ -205,16 +205,39 @@ declare_elements!{
|
|||
width: usize,
|
||||
} in [FlowContent, PhrasingContent, EmbeddedContent];
|
||||
input {
|
||||
accept: String,
|
||||
alt: String,
|
||||
autocomplete: String,
|
||||
autofocus: bool,
|
||||
disabled: bool,
|
||||
autofocus: Bool,
|
||||
capture: String,
|
||||
checked: Bool,
|
||||
disabled: Bool,
|
||||
form: Id,
|
||||
formaction: Uri,
|
||||
formenctype: FormEncodingType,
|
||||
formmethod: FormDialogMethod,
|
||||
formnovalidate: Bool,
|
||||
formtarget: Target,
|
||||
height: isize,
|
||||
list: Id,
|
||||
max: String,
|
||||
maxlength: usize,
|
||||
min: String,
|
||||
minlength: usize,
|
||||
multiple: Bool,
|
||||
name: Id,
|
||||
required: bool,
|
||||
pattern: String,
|
||||
placeholder: String,
|
||||
readonly: Bool,
|
||||
required: Bool,
|
||||
size: usize,
|
||||
spellcheck: Bool,
|
||||
src: Uri,
|
||||
step: String,
|
||||
tabindex: usize,
|
||||
type: InputType,
|
||||
value: String,
|
||||
width: isize,
|
||||
} in [FlowContent, FormContent, PhrasingContent];
|
||||
ins {
|
||||
cite: Uri,
|
||||
|
@ -248,12 +271,12 @@ declare_elements!{
|
|||
height: usize,
|
||||
name: Id,
|
||||
type: Mime,
|
||||
typemustmatch: bool,
|
||||
typemustmatch: Bool,
|
||||
usemap: String, // TODO should be a fragment starting with '#'
|
||||
width: usize,
|
||||
} in [FlowContent, PhrasingContent, EmbeddedContent, InteractiveContent, FormContent] with param;
|
||||
ol {
|
||||
reversed: bool,
|
||||
reversed: Bool,
|
||||
start: isize,
|
||||
type: OrderedListType,
|
||||
} in [FlowContent] with li;
|
||||
|
@ -275,11 +298,11 @@ declare_elements!{
|
|||
s in [FlowContent, PhrasingContent] with PhrasingContent;
|
||||
samp in [FlowContent, PhrasingContent] with PhrasingContent;
|
||||
script {
|
||||
async: bool,
|
||||
async: Bool,
|
||||
crossorigin: CrossOrigin,
|
||||
defer: bool,
|
||||
defer: Bool,
|
||||
integrity: Integrity,
|
||||
nomodule: bool,
|
||||
nomodule: Bool,
|
||||
nonce: Nonce,
|
||||
src: Uri,
|
||||
text: String,
|
||||
|
@ -288,12 +311,12 @@ declare_elements!{
|
|||
section in [FlowContent, SectioningContent] with FlowContent;
|
||||
select {
|
||||
autocomplete: String,
|
||||
autofocus: bool,
|
||||
disabled: bool,
|
||||
autofocus: Bool,
|
||||
disabled: Bool,
|
||||
form: Id,
|
||||
multiple: bool,
|
||||
multiple: Bool,
|
||||
name: Id,
|
||||
required: bool,
|
||||
required: Bool,
|
||||
size: usize,
|
||||
} in [FlowContent, PhrasingContent, InteractiveContent, FormContent] with SelectContent;
|
||||
small in [FlowContent, PhrasingContent] with PhrasingContent;
|
||||
|
@ -305,16 +328,16 @@ declare_elements!{
|
|||
template in [MetadataContent, FlowContent, PhrasingContent, TableColumnContent] with Node;
|
||||
textarea {
|
||||
autocomplete: OnOff,
|
||||
autofocus: bool,
|
||||
autofocus: Bool,
|
||||
cols: usize,
|
||||
disabled: bool,
|
||||
disabled: Bool,
|
||||
form: Id,
|
||||
maxlength: usize,
|
||||
minlength: usize,
|
||||
name: Id,
|
||||
placeholder: String,
|
||||
readonly: bool,
|
||||
required: bool,
|
||||
readonly: Bool,
|
||||
required: Bool,
|
||||
rows: usize,
|
||||
spellcheck: BoolOrDefault,
|
||||
wrap: Wrap,
|
||||
|
@ -331,7 +354,7 @@ declare_elements!{
|
|||
area {
|
||||
alt: String,
|
||||
coords: String, // TODO could perhaps be validated
|
||||
download: bool,
|
||||
download: Bool,
|
||||
href: Uri,
|
||||
hreflang: LanguageTag,
|
||||
ping: SpacedList<Uri>,
|
||||
|
@ -354,13 +377,13 @@ declare_elements!{
|
|||
value: isize,
|
||||
} with FlowContent;
|
||||
option {
|
||||
disabled: bool,
|
||||
disabled: Bool,
|
||||
label: String,
|
||||
selected: bool,
|
||||
selected: Bool,
|
||||
value: String,
|
||||
} in [SelectContent] with TextNode;
|
||||
optgroup {
|
||||
disabled: bool,
|
||||
disabled: Bool,
|
||||
label: String,
|
||||
} in [SelectContent] with option;
|
||||
param {
|
||||
|
@ -389,7 +412,7 @@ declare_elements!{
|
|||
thead in [TableContent] with tr;
|
||||
tr in [TableContent] with TableColumnContent;
|
||||
track {
|
||||
default: bool,
|
||||
default: Bool,
|
||||
kind: VideoKind,
|
||||
label: String,
|
||||
src: Uri,
|
||||
|
@ -407,7 +430,7 @@ declare_elements!{
|
|||
loop: isize,
|
||||
scrollamount: usize,
|
||||
scrolldelay: usize,
|
||||
truespeed: bool,
|
||||
truespeed: Bool,
|
||||
vspace: String, // FIXME size
|
||||
width: String, // FIXME size
|
||||
} in [FlowContent, PhrasingContent] with PhrasingContent;
|
||||
|
|
|
@ -199,6 +199,10 @@ use std::fmt::Display;
|
|||
#[proc_macro_hack(support_nested)]
|
||||
pub use typed_html_macros::html;
|
||||
|
||||
#[cfg(feature = "dodrio_macro")]
|
||||
#[proc_macro_hack(support_nested)]
|
||||
pub use typed_html_macros::dodrio;
|
||||
|
||||
pub mod dom;
|
||||
pub mod elements;
|
||||
pub mod events;
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
use std::fmt::{Display, Error, Formatter};
|
||||
|
||||
use crate::OutputType;
|
||||
|
||||
/// DOM output using the Dodrio virtual DOM
|
||||
pub struct Dodrio;
|
||||
impl OutputType for Dodrio {
|
||||
type Events = Events;
|
||||
type EventTarget = ();
|
||||
type EventListenerHandle = ();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Events;
|
||||
|
||||
impl Display for Events {
|
||||
fn fmt(&self, _: &mut Formatter) -> Result<(), Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
|
@ -1,2 +1,4 @@
|
|||
#[cfg(feature = "stdweb")]
|
||||
pub mod stdweb;
|
||||
#[cfg(feature = "dodrio_macro")]
|
||||
pub mod dodrio;
|
||||
|
|
|
@ -69,6 +69,12 @@ impl From<Id> for Class {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Class {
|
||||
fn from(str: &'a str) -> Self {
|
||||
Class::from_str(str).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Class {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
||||
Display::fmt(&self.0, f)
|
||||
|
|
|
@ -58,6 +58,12 @@ impl FromStr for Id {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Id {
|
||||
fn from(str: &'a str) -> Self {
|
||||
Id::from_str(str).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Class> for Id {
|
||||
fn from(c: Class) -> Self {
|
||||
Id(c.to_string())
|
||||
|
|
|
@ -104,6 +104,16 @@ pub enum FormMethod {
|
|||
Get,
|
||||
}
|
||||
|
||||
#[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, AsStaticStr)]
|
||||
pub enum FormDialogMethod {
|
||||
#[strum(to_string = "post")]
|
||||
Post,
|
||||
#[strum(to_string = "get")]
|
||||
Get,
|
||||
#[strum(to_string = "dialog")]
|
||||
Dialog,
|
||||
}
|
||||
|
||||
#[derive(EnumString, Display, PartialEq, Eq, PartialOrd, Ord, AsRefStr, AsStaticStr)]
|
||||
pub enum HTTPEquiv {
|
||||
#[strum(to_string = "content-security-policy")]
|
||||
|
|
|
@ -33,6 +33,11 @@ impl<A: Ord> SpacedSet<A> {
|
|||
pub fn new() -> Self {
|
||||
SpacedSet(BTreeSet::new())
|
||||
}
|
||||
|
||||
/// Add a value to the `SpacedSet`.
|
||||
pub fn add<T: Into<A>>(&mut self, value: T) -> bool {
|
||||
self.0.insert(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Ord> Default for SpacedSet<A> {
|
||||
|
|
Loading…
Reference in New Issue