Support boolean attrs, child blocks, and port TodoMVC example.

This commit is contained in:
Bodil Stokke 2019-03-16 20:59:56 +00:00
parent f628e63e63
commit a39ccf737f
20 changed files with 980 additions and 46 deletions

View File

@ -4,6 +4,7 @@ members = [
"macros",
"examples/stdweb",
"examples/rocket",
"examples/dodrio",
"examples/dodrio/counter",
"examples/dodrio/todomvc",
"ui",
]

View File

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

View File

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

View File

@ -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"]

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
}
}
}

View File

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

View File

@ -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 &bump; 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>
)
}
}

View File

@ -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 &bump; text(incomplete_count.into_bump_str())]
}</strong>
{ bumpalo::vec![in &bump; text(items_left)] }
</span>
<ul class="filters">
{ bumpalo::vec![in &bump;
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 &bump; 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 &bump; 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 &bump;
self.header(bump), self.todos_list(bump), self.footer(bump)
] }</div>
)
}
}

View File

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

View File

@ -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",
}
}
}

View File

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

View File

@ -69,15 +69,14 @@ impl Node {
) -> Result<TokenStream, TokenStream> {
match self {
Node::Element(el) => el.into_dodrio_token_stream(bump, is_req_child),
Node::Text(text) => {
let text = TokenTree::Literal(text);
Ok(quote!(dodrio::builder::text(#text)))
}
Node::Text(text) => Ok(dodrio_text_node(text)),
Node::Block(group) => {
let group: TokenTree = group.into();
Ok(quote!(
#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 }
})
}
}
}
@ -90,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();
@ -140,6 +145,7 @@ 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#") {
@ -288,7 +294,7 @@ impl Element {
) -> Result<TokenStream, TokenStream> {
let name = self.name;
let name_str = stringify_ident(&name);
let typename: TokenTree = Ident::new(&name_str, name.span()).into();
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() {
@ -312,12 +318,7 @@ impl Element {
value,
)
});
let opt_children = self
.children
.split_off(req_names.len())
.into_iter()
.map(|node| node.into_dodrio_token_stream(bump, false))
.collect::<Result<Vec<TokenStream>, TokenStream>>()?;
let opt_children = self.children.split_off(req_names.len());
let req_children = self
.children
.into_iter()
@ -369,38 +370,41 @@ impl Element {
}
}
let attr_max_len = self.attributes.len() + data_attrs.len();
let mut builder = quote!(
dodrio::builder::ElementBuilder::new(#bump, #tag_name)
let mut attr_list = dodrio::bumpalo::collections::Vec::with_capacity_in(#attr_max_len, #bump);
);
// Build an array of attributes.
let mut attr_array = TokenStream::new();
// Build the attributes.
for (key, _) in self.attributes.iter() {
let key_str = TokenTree::from(Literal::string(&stringify_ident(key)));
attr_array.extend(quote!(
dodrio::builder::attr(
#key_str,
dodrio::bumpalo::format!(
in &#bump, "{}", element.attrs.#key.unwrap()
).into_bump_str()
),
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()))
{
attr_array.extend(quote!(
dodrio::builder::attr(
builder.extend(quote!(
attr_list.push(dodrio::builder::attr(
#key,
dodrio::bumpalo::format!(
in &#bump, "{}", #value
).into_bump_str()
)
));
));
}
builder.extend(quote!(
.attributes([#attr_array])
let mut node = dodrio::builder::ElementBuilder::new(#bump, #tag_name)
.attributes(attr_list)
));
// Build an array of event listeners.
@ -413,21 +417,24 @@ impl Element {
));
}
builder.extend(quote!(
.listeners([#event_array])
.listeners([#event_array]);
));
// And finally an array of children.
// 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(
let req_child = TokenTree::from(ident::new_raw(
&format!("req_child_{}", index),
Span::call_site(),
));
let child_node = TokenTree::from(Ident::new(
let child_node = TokenTree::from(ident::new_raw(
&format!("child_node_{}", index),
Span::call_site(),
));
@ -437,23 +444,48 @@ impl Element {
child_array.extend(quote!(
#child_node,
));
child_builder.extend(quote!(
node = node.child(#child_node);
));
arg_list.push(req_child);
}
for child in opt_children {
// 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);
));
}
builder.extend(quote!(
.children([#child_array])
.finish()
));
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)
(element, {#builder})
);
}
@ -479,6 +511,7 @@ pub fn expand_html(input: &[Token]) -> Result<(Node, Option<Vec<Token>>), ParseE
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))
}

View File

@ -48,6 +48,7 @@ pub fn dodrio(input: TokenStream) -> TokenStream {
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,
},
})

View File

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