Adding in TODO MVC example using web-sys

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

View File

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

View File

@@ -0,0 +1,196 @@
use crate::exit;
use crate::store::*;
use crate::view::ViewMessage;
use crate::{Message, Scheduler};
use js_sys::Date;
use std::cell::RefCell;
use std::rc::Weak;
/// The controller of the application turns page state into functionality
pub struct Controller {
store: Store,
sched: RefCell<Option<Weak<Scheduler>>>,
active_route: String,
last_active_route: String,
}
/// Messages that represent the methods to be called on the Controller
pub enum ControllerMessage {
AddItem(String),
SetPage(String),
EditItemSave(String, String),
ToggleItem(String, bool),
EditItemCancel(String),
RemoveCompleted(),
RemoveItem(String),
ToggleAll(bool),
}
impl Controller {
pub fn new(store: Store, sched: Weak<Scheduler>) -> Controller {
Controller {
store,
sched: RefCell::new(Some(sched)),
active_route: "".into(),
last_active_route: "none".into(),
}
}
/// Used by `Scheduler` to convert a `ControllerMessage` into a function call on a `Controller`
pub fn call(&mut self, method_name: ControllerMessage) {
use self::ControllerMessage::*;
match method_name {
AddItem(title) => self.add_item(title),
SetPage(hash) => self.set_page(hash),
EditItemSave(id, value) => self.edit_item_save(id, value),
EditItemCancel(id) => self.edit_item_cancel(id),
RemoveCompleted() => self.remove_completed_items(),
RemoveItem(id) => self.remove_item(&id),
ToggleAll(completed) => self.toggle_all(completed),
ToggleItem(id, completed) => self.toggle_item(id, completed),
}
}
fn toggle_item(&mut self, id: String, completed: bool) {
self.toggle_completed(id, completed);
self._filter(completed);
}
fn add_message(&self, view_message: ViewMessage) {
if let Ok(sched) = self.sched.try_borrow_mut() {
if let Some(ref sched) = *sched {
if let Some(sched) = sched.upgrade() {
sched.add_message(Message::View(view_message));
}
}
}
}
pub fn set_page(&mut self, raw: String) {
let route = raw.trim_left_matches("#/");
self.active_route = route.to_string();
self._filter(false);
self.add_message(ViewMessage::UpdateFilterButtons(route.to_string()));
}
/// Add an Item to the Store and display it in the list.
fn add_item(&mut self, title: String) {
self.store.insert(Item {
id: Date::now().to_string(),
title,
completed: false,
});
self.add_message(ViewMessage::ClearNewTodo());
self._filter(true);
}
/// Save an Item in edit.
fn edit_item_save(&mut self, id: String, title: String) {
if !title.is_empty() {
self.store.update(ItemUpdate::Title {
id: id.clone(),
title: title.clone(),
});
self.add_message(ViewMessage::EditItemDone(id.to_string(), title.to_string()));
} else {
self.remove_item(&id);
}
}
/// Cancel the item editing mode.
fn edit_item_cancel(&mut self, id: String) {
let mut message = None;
if let Some(data) = self.store.find(ItemQuery::Id { id: id.clone() }) {
if let Some(todo) = data.get(0) {
let title = todo.title.to_string();
let citem = id.to_string();
message = Some(ViewMessage::EditItemDone(citem, title));
}
}
if let Some(message) = message {
self.add_message(message);
}
}
/// Remove the data and elements related to an Item.
fn remove_item(&mut self, id: &String) {
self.store.remove(ItemQuery::Id { id: id.clone() });
self._filter(false);
let ritem = id.to_string();
self.add_message(ViewMessage::RemoveItem(ritem));
}
/// Remove all completed items.
fn remove_completed_items(&mut self) {
self.store.remove(ItemQuery::Completed { completed: true });
self._filter(true);
}
/// Update an Item in storage based on the state of completed.
fn toggle_completed(&mut self, id: String, completed: bool) {
self.store.update(ItemUpdate::Completed {
id: id.clone(),
completed,
});
let tid = id.to_string();
self.add_message(ViewMessage::SetItemComplete(tid, completed));
}
/// Set all items to complete or active.
fn toggle_all(&mut self, completed: bool) {
let mut vals = Vec::new();
self.store
.find(
ItemQuery::EmptyItemQuery,
).map(|data| {
for item in data.iter() {
vals.push(item.id.clone());
}
});
for id in vals.iter() {
self.toggle_completed(id.to_string(), completed);
}
self._filter(false);
}
/// Refresh the list based on the current route.
fn _filter(&mut self, force: bool) {
let route = &self.active_route;
if force || self.last_active_route != "" || &self.last_active_route != route {
let query = match route.as_str() {
"completed" => ItemQuery::Completed { completed: true },
"active" => ItemQuery::Completed { completed: false },
_ => ItemQuery::EmptyItemQuery,
};
let mut v = None;
{
let store = &mut self.store;
if let Some(res) = store.find(query) {
v = Some(res.into());
}
}
if let Some(res) = v {
self.add_message(ViewMessage::ShowItems(res));
}
}
if let Some((total, active, completed)) = self.store.count() {
self.add_message(ViewMessage::SetItemsLeft(active));
self.add_message(ViewMessage::SetClearCompletedButtonVisibility(
completed > 0,
));
self.add_message(ViewMessage::SetCompleteAllCheckbox(completed == total));
self.add_message(ViewMessage::SetMainVisibility(total > 0));
}
self.last_active_route = route.to_string();
}
}
impl Drop for Controller {
fn drop(&mut self) {
exit("calling drop on Controller");
}
}

View File

@@ -0,0 +1,208 @@
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
/// Wrapper for `web_sys::Element` to simplify calling different interfaces
pub struct Element {
el: Option<web_sys::Element>,
}
impl From<Element> for Option<web_sys::Node> {
fn from(obj: Element) -> Option<web_sys::Node> {
if let Some(el) = obj.el {
Some(el.into())
} else {
None
}
}
}
impl From<Element> for Option<web_sys::EventTarget> {
fn from(obj: Element) -> Option<web_sys::EventTarget> {
if let Some(el) = obj.el {
Some(el.into())
} else {
None
}
}
}
impl Element {
pub fn qs(selector: &str) -> Option<Element> {
let body: web_sys::Element = web_sys::window()?.document()?.body()?.into();
let el = body.query_selector(selector).ok()?;
Some(Element { el })
}
/// Add event listener to this node
pub fn add_event_listener<T>(&mut self, event_name: &str, handler: T)
where
T: 'static + FnMut(web_sys::Event),
{
let cb = Closure::wrap(Box::new(handler) as Box<FnMut(_)>);
if let Some(el) = self.el.take() {
let el_et: web_sys::EventTarget = el.into();
el_et.add_event_listener_with_callback(event_name, cb.as_ref().unchecked_ref());
cb.forget();
if let Ok(el) = el_et.dyn_into::<web_sys::Element>() {
self.el = Some(el);
}
}
}
/// Delegate an event to a selector
pub fn delegate<T>(
&mut self,
selector: &'static str,
event: &str,
mut handler: T,
use_capture: bool,
) where
T: 'static + FnMut(web_sys::Event) -> (),
{
let el = match self.el.take() {
Some(e) => e,
None => return,
};
if let Some(dyn_el) = &el.dyn_ref::<web_sys::EventTarget>()
{
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
// TODO document selector to the target element
let tg_el = document;
let cb = Closure::wrap(Box::new(move |event: web_sys::Event| {
if let Some(target_element) = event.target() {
let dyn_target_el: Option<
&web_sys::Node,
> = wasm_bindgen::JsCast::dyn_ref(&target_element);
if let Some(target_element) = dyn_target_el {
if let Ok(potential_elements) =
tg_el.query_selector_all(selector)
{
let mut has_match = false;
for i in 0..potential_elements.length() {
if let Some(el) = potential_elements.get(i) {
if target_element.is_equal_node(Some(&el)) {
has_match = true;
}
break;
}
}
if has_match {
handler(event);
}
}
}
}
}) as Box<FnMut(_)>);
dyn_el.add_event_listener_with_callback_and_bool(
event,
cb.as_ref().unchecked_ref(),
use_capture,
);
cb.forget(); // TODO cycle collect
}
}
}
self.el = Some(el);
}
/// Find child `Element`s from this node
pub fn qs_from(&mut self, selector: &str) -> Option<Element> {
let mut found_el = None;
if let Some(el) = self.el.as_ref() {
found_el = Some(Element {
el: el.query_selector(selector).ok()?,
});
}
found_el
}
/// Sets the inner HTML of the `self.el` element
pub fn set_inner_html(&mut self, value: String) {
if let Some(el) = self.el.take() {
el.set_inner_html(&value);
self.el = Some(el);
}
}
/// Sets the text content of the `self.el` element
pub fn set_text_content(&mut self, value: &str) {
if let Some(el) = self.el.as_ref() {
if let Some(node) = &el.dyn_ref::<web_sys::Node>() {
node.set_text_content(Some(&value));
}
}
}
/// Removes a class list item from the element
///
/// ```
/// e.class_list_remove(String::from("clickable"));
/// // removes the class 'clickable' from e.el
/// ```
pub fn class_list_remove(&mut self, value: &str) {
if let Some(el) = self.el.take() {
el.class_list().remove_1(&value);
self.el = Some(el);
}
}
/// Given another `Element` it will remove that child from the DOM from this element
/// Consumes `child` so it can't be used after it's removal.
pub fn remove_child(&mut self, mut child: Element) {
if let Some(child_el) = child.el.take() {
if let Some(el) = self.el.take() {
if let Some(el_node) = el.dyn_ref::<web_sys::Node>() {
let child_node: web_sys::Node = child_el.into();
el_node.remove_child(&child_node);
}
self.el = Some(el);
}
}
}
/// Sets the whole class value for `self.el`
pub fn set_class_name(&mut self, class_name: &str) {
if let Some(el) = self.el.take() {
el.set_class_name(&class_name);
self.el = Some(el);
}
}
/// Sets the visibility for the element in `self.el`
pub fn set_visibility(&mut self, visible: bool) {
if let Some(el) = self.el.take() {
{
let dyn_el: Option<&web_sys::HtmlElement> = wasm_bindgen::JsCast::dyn_ref(&el);
if let Some(el) = dyn_el {
el.set_hidden(!visible);
}
}
self.el = Some(el);
}
}
/// Sets the visibility for the element in `self.el` (The element must be an input)
pub fn set_value(&mut self, value: &str) {
if let Some(el) = self.el.take() {
if let Some(el) = wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&el) {
el.set_value(&value);
}
self.el = Some(el);
}
}
/// Sets the checked state for the element in `self.el` (The element must be an input)
pub fn set_checked(&mut self, checked: bool) {
if let Some(el) = self.el.take() {
if let Some(el) = wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&el) {
el.set_checked(checked);
}
self.el = Some(el);
}
}
}

View File

@@ -0,0 +1,70 @@
//! # TODO MVC
//!
//! A [TODO MVC](https://todomvc.com/) implementation written using [web-sys](https://rustwasm.github.io/wasm-bindgen/web-sys/overview.html)
#![warn(missing_docs)]
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
extern crate js_sys;
extern crate web_sys;
use std::rc::Rc;
#[macro_use]
extern crate askama;
/// Controller of the program
pub mod controller;
/// Element wrapper to the DOM
pub mod element;
/// Schedule messages to the Controller and View
pub mod scheduler;
/// Stores items into localstorage
pub mod store;
/// Handles constructing template strings from data
pub mod template;
/// Presentation layer
pub mod view;
use crate::controller::{Controller, ControllerMessage};
use crate::scheduler::Scheduler;
use crate::store::Store;
use crate::view::{View, ViewMessage};
/// Message wrapper enum used to pass through the scheduler to the Controller or View
pub enum Message {
/// Message wrapper to send to the controller
Controller(ControllerMessage),
/// Message wrapper to send to the view
View(ViewMessage),
}
/// Used for debugging to the console
pub fn exit(message: &str) {
let v = wasm_bindgen::JsValue::from_str(&message.to_string());
web_sys::console::exception_1(&v);
std::process::abort();
}
fn app(name: &str) {
let sched = Rc::new(Scheduler::new());
let store = match Store::new(name) {
Some(s) => s,
None => return,
};
let controller = Controller::new(store, Rc::downgrade(&sched));
if let Some(mut view) = View::new(sched.clone()) {
let sch: &Rc<Scheduler> = &sched;
view.init();
sch.set_view(view);
sch.set_controller(controller);
sched.add_message(Message::Controller(ControllerMessage::SetPage(
"".to_string(),
)));
}
}
/// Entry point into the program from JavaScript
#[wasm_bindgen]
pub fn run() {
app("todos-wasmbindgen");
}

View File

@@ -0,0 +1,138 @@
use crate::controller::Controller;
use crate::exit;
use crate::view::View;
use crate::Message;
use std::cell::RefCell;
use std::rc::Rc;
/// Creates an event loop that starts each time a message is added
pub struct Scheduler {
controller: Rc<RefCell<Option<Controller>>>,
view: Rc<RefCell<Option<View>>>,
events: RefCell<Vec<Message>>,
running: RefCell<bool>,
}
impl Scheduler {
/// Construct a new `Scheduler`
pub fn new() -> Scheduler {
Scheduler {
controller: Rc::new(RefCell::new(None)),
view: Rc::new(RefCell::new(None)),
events: RefCell::new(Vec::new()),
running: RefCell::new(false),
}
}
pub fn set_controller(&self, controller: Controller) {
if let Ok(mut controller_data) = self.controller.try_borrow_mut() {
controller_data.replace(controller);
} else {
exit("This might be a deadlock");
}
}
pub fn set_view(&self, view: View) {
if let Ok(mut view_data) = self.view.try_borrow_mut() {
view_data.replace(view);
} else {
exit("This might be a deadlock");
}
}
/// Add a new message onto the event stack
///
/// Triggers running the event loop if it's not already running
pub fn add_message(&self, message: Message) {
let running = {
if let Ok(running) = self.running.try_borrow() {
running.clone()
} else {
exit("This might be a deadlock");
false
}
};
{
if let Ok(mut events) = self.events.try_borrow_mut() {
events.push(message);
} else {
exit("This might be a deadlock");
}
}
if !running {
self.run();
}
}
/// Start the event loop, taking messages from the stack to run
fn run(&self) {
let mut events_len = 0;
{
if let Ok(events) = self.events.try_borrow() {
events_len = events.len().clone();
} else {
exit("This might be a deadlock");
}
}
if events_len == 0 {
if let Ok(mut running) = self.running.try_borrow_mut() {
*running = false;
} else {
exit("This might be a deadlock");
}
} else {
{
if let Ok(mut running) = self.running.try_borrow_mut() {
*running = true;
} else {
exit("This might be a deadlock");
}
}
self.next_message();
}
}
fn next_message(&self) {
let event = {
if let Ok(mut events) = self.events.try_borrow_mut() {
Some(events.pop())
} else {
exit("This might be a deadlock");
None
}
};
if let Some(Some(event)) = event {
match event {
Message::Controller(e) => {
if let Ok(mut controller) = self.controller.try_borrow_mut() {
if let Some(ref mut ag) = *controller {
ag.call(e);
}
} else {
exit("This might be a deadlock");
}
}
Message::View(e) => {
if let Ok(mut view) = self.view.try_borrow_mut() {
if let Some(ref mut ag) = *view {
ag.call(e);
}
} else {
exit("This might be a deadlock");
}
}
}
self.run();
} else if let Ok(mut running) = self.running.try_borrow_mut() {
*running = false;
} else {
exit("This might be a deadlock");
}
}
}
impl Drop for Scheduler {
fn drop(&mut self) {
exit("calling drop on Scheduler");
}
}

View File

@@ -0,0 +1,285 @@
use js_sys::JSON;
use wasm_bindgen::prelude::*;
use crate::exit;
/// Stores items into localstorage
pub struct Store {
local_storage: web_sys::Storage,
data: ItemList,
name: String,
}
impl Store {
/// Creates a new store with `name` as the localstorage value name
pub fn new(name: &str) -> Option<Store> {
let window = web_sys::window()?;
if let Ok(Some(local_storage)) = window.local_storage() {
let mut store = Store {
local_storage,
data: ItemList::new(),
name: String::from(name),
};
store.fetch_local_storage();
Some(store)
} else {
None
}
}
/// Read the local ItemList from localStorage.
/// Returns an &Option<ItemList> of the stored database
/// Caches the store into `self.data` to reduce calls to JS
///
/// Uses mut here as the return is something we might want to manipulate
///
fn fetch_local_storage(&mut self) -> Option<()> {
let mut item_list = ItemList::new();
// If we have an existing cached value, return early.
if let Ok(Some(value)) = self.local_storage.get_item(&self.name) {
let data = JSON::parse(&value).ok()?;
let iter = js_sys::try_iter(&data).ok()??;
for item in iter {
let item = item.ok()?;
let item_array: &js_sys::Array = wasm_bindgen::JsCast::dyn_ref(&item)?;
let title = item_array.shift().as_string()?;
let completed = item_array.shift().as_bool()?;
let id = item_array.shift().as_string()?;
let mut temp_item = Item {
title,
completed,
id,
};
item_list.push(temp_item);
}
}
self.data = item_list;
Some(())
}
/// Write the local ItemList to localStorage.
fn sync_local_storage(&mut self) {
let array = js_sys::Array::new();
for item in self.data.iter() {
let mut child = js_sys::Array::new();
let s = item.title.clone();
child.push(&JsValue::from(&s));
child.push(&JsValue::from(item.completed));
child.push(&JsValue::from(item.id.to_string()));
array.push(&JsValue::from(child));
}
if let Ok(storage_string) = JSON::stringify(&JsValue::from(array)) {
let storage_string: String = storage_string.to_string().into();
self.local_storage
.set_item(&self.name, storage_string.as_str());
}
}
/// Find items with properties matching those on query.
/// `ItemQuery` query Query to match
///
/// ```
/// let data = db.find(ItemQuery::Completed {completed: true});
/// // data will contain items whose completed properties are true
/// ```
pub fn find(&mut self, query: ItemQuery) -> Option<ItemListSlice> {
Some(self.data.iter().filter(|todo| query.matches(*todo)).collect())
}
/// Update an item in the Store.
///
/// `ItemUpdate` update Record with an id and a property to update
pub fn update(&mut self, update: ItemUpdate) {
let id = update.id();
self.data.iter_mut().for_each(|todo| {
if id == todo.id {
todo.update(&update);
}
});
self.sync_local_storage();
}
/// Insert an item into the Store.
///
/// `Item` item Item to insert
pub fn insert(&mut self, item: Item) {
self.data.push(item);
self.sync_local_storage();
}
/// Remove items from the Store based on a query.
/// query is an `ItemQuery` query Query matching the items to remove
pub fn remove(&mut self, query: ItemQuery) {
self.data.retain(|todo| !query.matches(todo));
self.sync_local_storage();
}
/// Count total, active, and completed todos.
pub fn count(&mut self) -> Option<(usize, usize, usize)> {
self.find(ItemQuery::EmptyItemQuery).map(|data| {
let total = data.length();
let mut completed = 0;
for item in data.iter() {
if item.completed {
completed += 1;
}
}
(total, total - completed, completed)
})
}
}
/// Represents a todo item
pub struct Item {
pub id: String,
pub title: String,
pub completed: bool,
}
impl Item {
pub fn update(&mut self, update: &ItemUpdate) {
match update {
ItemUpdate::Title { title, .. } => {
self.title = title.to_string();
}
ItemUpdate::Completed { completed, .. } => {
self.completed = *completed;
}
}
}
}
pub trait ItemListTrait<T> {
fn new() -> Self;
fn get(&self, i: usize) -> Option<&T>;
fn length(&self) -> usize;
fn push(&mut self, item: T);
fn iter(&self) -> std::slice::Iter<T>;
}
pub struct ItemList {
list: Vec<Item>,
}
impl ItemList {
fn into_iter(self) -> std::vec::IntoIter<Item> {
self.list.into_iter()
}
fn retain<F>(&mut self, f: F)
where
F: FnMut(&Item) -> bool {
self.list.retain(f);
}
fn iter_mut(&mut self) -> std::slice::IterMut<Item> {
self.list.iter_mut()
}
}
impl ItemListTrait<Item> for ItemList {
fn new() -> ItemList {
ItemList { list: Vec::new() }
}
fn get(&self, i: usize) -> Option<&Item> {
self.list.get(i)
}
fn length(&self) -> usize {
self.list.len()
}
fn push(&mut self, item: Item) {
self.list.push(item)
}
fn iter(&self) -> std::slice::Iter<Item> {
self.list.iter()
}
}
use std::iter::FromIterator;
impl<'a> FromIterator<Item> for ItemList {
fn from_iter<I: IntoIterator<Item = Item>>(iter: I) -> Self {
let mut c = ItemList::new();
for i in iter {
c.push(i);
}
c
}
}
/// A borrowed set of Items filtered from the store
pub struct ItemListSlice<'a> {
list: Vec<&'a Item>,
}
impl<'a> ItemListTrait<&'a Item> for ItemListSlice<'a> {
fn new() -> ItemListSlice<'a> {
ItemListSlice { list: Vec::new() }
}
fn get(&self, i: usize) -> Option<&&'a Item> {
self.list.get(i)
}
fn length(&self) -> usize {
self.list.len()
}
fn push(&mut self, item: &'a Item) {
self.list.push(item)
}
fn iter(&self) -> std::slice::Iter<&'a Item> {
self.list.iter()
}
}
impl<'a> FromIterator<&'a Item> for ItemListSlice<'a> {
fn from_iter<I: IntoIterator<Item = &'a Item>>(iter: I) -> Self {
let mut c = ItemListSlice::new();
for i in iter {
c.push(i);
}
c
}
}
impl<'a> Into<ItemList> for ItemListSlice<'a> {
fn into(self) -> ItemList {
let mut i = ItemList::new();
let items = self.list.into_iter();
for j in items {
// TODO neaten this cloning?
let item = Item {
id: j.id.clone(),
completed: j.completed,
title: j.title.clone(),
};
i.push(item);
}
i
}
}
/// Represents a search into the store
pub enum ItemQuery {
Id { id: String },
Completed { completed: bool },
EmptyItemQuery,
}
impl ItemQuery {
fn matches(&self, item: &Item) -> bool {
match *self {
ItemQuery::EmptyItemQuery => true,
ItemQuery::Id { ref id } => &item.id == id,
ItemQuery::Completed { completed } => item.completed == completed,
}
}
}
pub enum ItemUpdate {
Title { id: String, title: String },
Completed { id: String, completed: bool },
}
impl ItemUpdate {
fn id(&self) -> String {
match self {
ItemUpdate::Title { id, .. } => id.clone(),
ItemUpdate::Completed { id, .. } => id.clone(),
}
}
}

View File

@@ -0,0 +1,58 @@
use store::{ItemList, ItemListTrait, Item};
use askama::Template as AskamaTemplate;
#[derive(AskamaTemplate)]
#[template(path = "row.html")]
struct RowTemplate<'a> {
id: &'a str,
title: &'a str,
completed: bool,
}
#[derive(AskamaTemplate)]
#[template(path = "itemsLeft.html")]
struct ItemsLeftTemplate {
active_todos: usize,
}
pub struct Template {}
impl Template {
/// Format the contents of a todo list.
///
/// items `ItemList` contains keys you want to find in the template to replace.
/// Returns the contents for a todo list
///
pub fn item_list(items: ItemList) -> String {
let mut output = String::from("");
for item in items.iter() {
let row = RowTemplate {
id: &item.id,
completed: item.completed,
title: &item.title,
};
if let Ok(res) = row.render() {
output.push_str(&res);
}
}
output
}
///
/// Format the contents of an "items left" indicator.
///
/// `active_todos` Number of active todos
///
/// Returns the contents for an "items left" indicator
pub fn item_counter(active_todos: usize) -> String {
let items_left = ItemsLeftTemplate {
active_todos
};
if let Ok(res) = items_left.render() {
res
} else {
String::new()
}
}
}

View File

@@ -0,0 +1,478 @@
use crate::controller::ControllerMessage;
use crate::exit;
use crate::element::Element;
use crate::store::ItemList;
use crate::{Message, Scheduler};
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::JsCast;
use crate::template::Template;
const ENTER_KEY: u32 = 13;
const ESCAPE_KEY: u32 = 27;
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
/// Messages that represent the methods to be called on the View
pub enum ViewMessage {
UpdateFilterButtons(String),
ClearNewTodo(),
ShowItems(ItemList),
SetItemsLeft(usize),
SetClearCompletedButtonVisibility(bool),
SetCompleteAllCheckbox(bool),
SetMainVisibility(bool),
RemoveItem(String),
EditItemDone(String, String),
SetItemComplete(String, bool),
}
fn item_id(element: &web_sys::EventTarget) -> Option<String> {
//TODO ugly reformat
let dyn_el: Option<&web_sys::Node> = wasm_bindgen::JsCast::dyn_ref(element);
if let Some(element_node) = dyn_el {
element_node.parent_node().map(|parent| {
let mut res = None;
if let Some(e) = wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&parent) {
if e.dataset().get("id") != "" {
res = Some(e.dataset().get("id"))
}
};
if None == res {
if let Some(ep) = parent.parent_node() {
if let Some(dyn_el) = wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&ep)
{
res = Some(dyn_el.dataset().get("id"));
}
}
}
res.unwrap()
})
} else {
None
}
}
/// Presentation layer
#[wasm_bindgen]
pub struct View {
sched: RefCell<Rc<Scheduler>>,
todo_list: Element,
todo_item_counter: Element,
clear_completed: Element,
main: Element,
toggle_all: Element,
new_todo: Element,
callbacks: Vec<(web_sys::EventTarget, String, Closure<FnMut()>)>,
}
impl View {
/// Construct a new view
pub fn new(sched: Rc<Scheduler>) -> Option<View> {
let todo_list = Element::qs(".todo-list")?;
let todo_item_counter = Element::qs(".todo-count")?;
let clear_completed = Element::qs(".clear-completed")?;
let main = Element::qs(".main")?;
let toggle_all = Element::qs(".toggle-all")?;
let new_todo = Element::qs(".new-todo")?;
Some(View {
sched: RefCell::new(sched),
todo_list,
todo_item_counter,
clear_completed,
main,
toggle_all,
new_todo,
callbacks: Vec::new(),
})
}
pub fn init(&mut self) {
let window = match web_sys::window() {
Some(w) => w,
None => return,
};
let document = match window.document() {
Some(d) => d,
None => return,
};
let sched = self.sched.clone();
let set_page = Closure::wrap(Box::new(move || {
if let Some(location) = document.location() {
if let Ok(hash) = location.hash() {
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(ControllerMessage::SetPage(
hash,
)));
}
}
}
}) as Box<FnMut()>);
let window_et: web_sys::EventTarget = window.into();
window_et.add_event_listener_with_callback(
"hashchange",
set_page.as_ref().unchecked_ref(),
);
set_page.forget(); // Cycle collect this
//self.callbacks.push((window_et, "hashchange".to_string(), set_page));
self.bind_add_item();
self.bind_edit_item_save();
self.bind_edit_item_cancel();
self.bind_remove_item();
self.bind_toggle_item();
self.bind_edit_item();
self.bind_remove_completed();
self.bind_toggle_all();
}
fn bind_edit_item(&mut self) {
self.todo_list.delegate(
"li label",
"dblclick",
|e: web_sys::Event| {
if let Some(target) = e.target() {
if let Ok(el) = wasm_bindgen::JsCast::dyn_into::<web_sys::Element>(target) {
View::edit_item(el);
}
}
},
false,
);
}
/// Put an item into edit mode.
fn edit_item(target: web_sys::Element) {
let target_node: web_sys::Node = target.into();
if let Some(parent_element) = target_node.parent_element() {
let parent_node: web_sys::Node = parent_element.into();
if let Some(list_item) = parent_node.parent_element() {
list_item.class_list().add_1("editing");
if let Some(input) = create_element("input") {
input.set_class_name("edit");
let list_item_node: web_sys::Node = list_item.into();
list_item_node.append_child(&input.into());
}
}
}
if let Some(el) = wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&target_node) {
if let Some(input_el) =
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&target_node)
{
input_el.set_value(&el.inner_text());
}
el.focus();
}
}
/// Used by scheduler to convert a `ViewMessage` into a function call on the `View`
pub fn call(&mut self, method_name: ViewMessage) {
use self::ViewMessage::*;
match method_name {
UpdateFilterButtons(route) => self.update_filter_buttons(&route),
ClearNewTodo() => self.clear_new_todo(),
ShowItems(item_list) => self.show_items(item_list),
SetItemsLeft(count) => self.set_items_left(count),
SetClearCompletedButtonVisibility(visible) => {
self.set_clear_completed_button_visibility(visible)
}
SetCompleteAllCheckbox(complete) => self.set_complete_all_checkbox(complete),
SetMainVisibility(complete) => self.set_main_visibility(complete),
RemoveItem(id) => self.remove_item(&id),
EditItemDone(id, title) => self.edit_item_done(&id, &title),
SetItemComplete(id, completed) => self.set_item_complete(&id, completed),
}
}
/// Populate the todo list with a list of items.
fn show_items(&mut self, items: ItemList) {
self.todo_list.set_inner_html(Template::item_list(items));
}
/// Gets the selector to find a todo item in the DOM
fn get_selector_string(id: &str) -> String {
let mut selector = String::from("[data-id=\"");
selector.push_str(id);
selector.push_str("\"]");
selector
}
/// Remove an item from the view.
fn remove_item(&mut self, id: &str) {
let elem = Element::qs(&View::get_selector_string(id));
if let Some(elem) = elem {
self.todo_list.remove_child(elem);
}
}
/// Set the number in the 'items left' display.
fn set_items_left(&mut self, items_left: usize) {
// TODO what is items left?
self.todo_item_counter
.set_inner_html(Template::item_counter(items_left));
}
/// Set the visibility of the "Clear completed" button.
fn set_clear_completed_button_visibility(&mut self, visible: bool) {
self.clear_completed.set_visibility(visible);
}
/// Set the visibility of the main content and footer.
fn set_main_visibility(&mut self, visible: bool) {
self.main.set_visibility(visible);
}
/// Set the checked state of the Complete All checkbox.
fn set_complete_all_checkbox(&mut self, checked: bool) {
self.toggle_all.set_checked(checked);
}
/// Change the appearance of the filter buttons based on the route.
fn update_filter_buttons(&self, route: &str) {
if let Some(mut el) = Element::qs(".filters .selected") {
el.set_class_name("");
}
let mut selector = String::from(".filters [href=\"");
selector.push_str(route);
selector.push_str("\"]");
if let Some(mut el) = Element::qs(&selector) {
el.set_class_name("selected");
}
}
/// Clear the new todo input
fn clear_new_todo(&mut self) {
self.new_todo.set_value("");
}
/// Render an item as either completed or not.
fn set_item_complete(&self, id: &str, completed: bool) {
if let Some(mut list_item) = Element::qs(&View::get_selector_string(id)) {
let class_name = if completed { "completed" } else { "" };
list_item.set_class_name(class_name);
// In case it was toggled from an event and not by clicking the checkbox
if let Some(mut el) = list_item.qs_from("input") {
el.set_checked(completed);
}
}
}
/// Bring an item out of edit mode.
fn edit_item_done(&self, id: &str, title: &str) {
if let Some(mut list_item) = Element::qs(&View::get_selector_string(id)) {
if let Some(input) = list_item.qs_from("input.edit") {
list_item.class_list_remove("editing");
if let Some(mut list_item_label) = list_item.qs_from("label") {
list_item_label.set_text_content(title);
}
list_item.remove_child(input);
}
}
}
fn bind_add_item(&mut self) {
let sched = self.sched.clone();
let cb = move |event: web_sys::Event| {
if let Some(target) = event.target() {
if let Some(input_el) =
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&target)
{
let v = input_el.value(); // TODO remove with nll
let title = v.trim();
if title != "" {
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(ControllerMessage::AddItem(
String::from(title),
)));
}
}
}
}
};
self.new_todo.add_event_listener("change", cb);
}
fn bind_remove_completed(&mut self) {
let sched = self.sched.clone();
let handler = move |_| {
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(ControllerMessage::RemoveCompleted()));
}
};
self.clear_completed.add_event_listener("click", handler);
}
fn bind_toggle_all(&mut self) {
let sched = self.sched.clone();
self.toggle_all
.add_event_listener("click", move |event: web_sys::Event| {
if let Some(target) = event.target() {
if let Some(input_el) =
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&target)
{
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(ControllerMessage::ToggleAll(
input_el.checked(),
)));
}
}
}
});
}
fn bind_remove_item(&mut self) {
let sched = self.sched.clone();
self.todo_list.delegate(
".destroy",
"click",
move |e: web_sys::Event| {
if let Some(target) = e.target() {
if let Some(item_id) = item_id(&target) {
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(ControllerMessage::RemoveItem(
item_id,
)));
}
}
}
},
false,
);
}
fn bind_toggle_item(&mut self) {
let sched = self.sched.clone();
self.todo_list.delegate(
".toggle",
"click",
move |e: web_sys::Event| {
if let Some(target) = e.target() {
if let Some(input_el) =
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&target)
{
if let Some(item_id) = item_id(&target) {
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(
ControllerMessage::ToggleItem(item_id, input_el.checked()),
));
}
}
}
}
},
false,
);
}
fn bind_edit_item_save(&mut self) {
let sched = self.sched.clone();
self.todo_list.delegate(
"li .edit",
"blur",
move |e: web_sys::Event| {
if let Some(target) = e.target() {
if let Some(target_el) =
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&target)
{
if target_el.dataset().get("iscanceled") != "true" {
if let Some(input_el) =
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlInputElement>(&target)
{
if let Some(item) = item_id(&target) {
// TODO refactor back into fn
// Was: &self.add_message(ControllerMessage::SetPage(hash));
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(
ControllerMessage::EditItemSave(item, input_el.value()),
));
}
// TODO refactor back into fn
}
}
}
}
}
},
true,
);
// Remove the cursor from the input when you hit enter just like if it were a real form
self.todo_list.delegate(
"li .edit",
"keypress",
|e: web_sys::Event| {
if let Some(key_e) = wasm_bindgen::JsCast::dyn_ref::<web_sys::KeyboardEvent>(&e) {
if key_e.key_code() == ENTER_KEY {
if let Some(target) = e.target() {
if let Some(el) =
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&target)
{
el.blur();
}
}
}
}
},
false,
);
}
fn bind_edit_item_cancel(&mut self) {
let sched = self.sched.clone();
self.todo_list.delegate(
"li .edit",
"keyup",
move |e: web_sys::Event| {
if let Some(key_e) = wasm_bindgen::JsCast::dyn_ref::<web_sys::KeyboardEvent>(&e) {
if key_e.key_code() == ESCAPE_KEY {
if let Some(target) = e.target() {
if let Some(el) =
wasm_bindgen::JsCast::dyn_ref::<web_sys::HtmlElement>(&target)
{
el.dataset().set("iscanceled", "true");
el.blur();
}
if let Some(item_id) = item_id(&target) {
if let Ok(sched) = &(sched.try_borrow_mut()) {
sched.add_message(Message::Controller(
ControllerMessage::EditItemCancel(item_id),
));
}
}
}
}
}
},
false,
);
}
}
fn create_element(tag: &str) -> Option<web_sys::Element> {
web_sys::window()?.document()?.create_element(tag).ok()
}
impl Drop for View {
fn drop(&mut self) {
exit("calling drop on view");
let callbacks: Vec<(web_sys::EventTarget, String, Closure<FnMut()>)> =
self.callbacks.drain(..).collect();
for callback in callbacks {
callback.0.remove_event_listener_with_callback(
callback.1.as_str(),
&callback.2.as_ref().unchecked_ref(),
);
}
}
}