use crate::{
dom_types,
dom_types::{El, Namespace},
routing, util, websys_bridge,
};
use std::{cell::RefCell, collections::HashMap, panic, rc::Rc};
use wasm_bindgen::closure::Closure;
use web_sys::{Document, Element, Event, EventTarget, Window};
pub enum Update<Ms, Mdl> {
Render(Mdl),
Skip(Mdl),
RenderThen(Mdl, Ms)
}
impl<Ms, Mdl> Update<Ms, Mdl> {
pub fn model(self) -> Mdl {
use Update::*;
match self {
Render(model) => model,
Skip(model) => model,
RenderThen(model, _) => model,
}
}
}
type UpdateFn<Ms, Mdl> = fn(Ms, Mdl) -> Update<Ms, Mdl>;
type ViewFn<Ms, Mdl> = fn(App<Ms, Mdl>, &Mdl) -> El<Ms>;
type RoutesFn<Ms> = fn(&crate::routing::Url) -> Ms;
type WindowEvents<Ms, Mdl> = fn(&Mdl) -> Vec<dom_types::Listener<Ms>>;
type MsgListeners<Ms> = Vec<Box<Fn(&Ms)>>;
pub struct Mailbox<Message: 'static> {
func: Rc<Fn(Message)>,
}
impl<Ms> Mailbox<Ms> {
pub fn new(func: impl Fn(Ms) + 'static) -> Self {
Mailbox {
func: Rc::new(func),
}
}
pub fn send(&self, message: Ms) {
(self.func)(message)
}
}
impl<Ms> Clone for Mailbox<Ms> {
fn clone(&self) -> Self {
Mailbox {
func: self.func.clone(),
}
}
}
type StoredPopstate = RefCell<Option<Closure<FnMut(Event)>>>;
pub struct AppData<Ms: Clone + 'static, Mdl> {
pub model: RefCell<Option<Mdl>>,
main_el_vdom: RefCell<Option<El<Ms>>>,
pub popstate_closure: StoredPopstate,
pub routes: RefCell<Option<RoutesFn<Ms>>>,
window_listeners: RefCell<Vec<dom_types::Listener<Ms>>>,
msg_listeners: RefCell<MsgListeners<Ms>>,
}
pub struct AppCfg<Ms: Clone + 'static, Mdl: 'static> {
document: web_sys::Document,
mount_point: web_sys::Element,
pub update: UpdateFn<Ms, Mdl>,
view: ViewFn<Ms, Mdl>,
window_events: Option<WindowEvents<Ms, Mdl>>,
}
pub struct App<Ms: Clone + 'static, Mdl: 'static> {
pub cfg: Rc<AppCfg<Ms, Mdl>>,
pub data: Rc<AppData<Ms, Mdl>>,
}
impl<Ms: Clone + 'static, Mdl: 'static> ::std::fmt::Debug for App<Ms, Mdl> {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
write!(f, "App")
}
}
#[derive(Clone)]
pub struct AppBuilder<Ms: Clone + 'static, Mdl: 'static> {
model: Mdl,
update: UpdateFn<Ms, Mdl>,
view: ViewFn<Ms, Mdl>,
parent_div_id: Option<&'static str>,
routes: Option<RoutesFn<Ms>>,
window_events: Option<WindowEvents<Ms, Mdl>>,
}
impl<Ms: Clone, Mdl> AppBuilder<Ms, Mdl> {
pub fn mount(mut self, id: &'static str) -> Self {
self.parent_div_id = Some(id);
self
}
pub fn routes(mut self, routes: RoutesFn<Ms>) -> Self {
self.routes = Some(routes);
self
}
pub fn window_events(mut self, evts: WindowEvents<Ms, Mdl>) -> Self {
self.window_events = Some(evts);
self
}
pub fn finish(self) -> App<Ms, Mdl> {
let parent_div_id = self.parent_div_id.unwrap_or("app");
App::new(
self.model,
self.update,
self.view,
parent_div_id,
self.routes,
self.window_events,
)
}
}
impl<Ms: Clone, Mdl> App<Ms, Mdl> {
pub fn build(
model: Mdl,
update: UpdateFn<Ms, Mdl>,
view: ViewFn<Ms, Mdl>,
) -> AppBuilder<Ms, Mdl> {
AppBuilder {
model,
update,
view,
parent_div_id: None,
routes: None,
window_events: None,
}
}
fn new(
model: Mdl,
update: UpdateFn<Ms, Mdl>,
view: ViewFn<Ms, Mdl>,
parent_div_id: &str,
routes: Option<RoutesFn<Ms>>,
window_events: Option<WindowEvents<Ms, Mdl>>,
) -> Self {
let window = util::window();
let document = window
.document()
.expect("Can't find the window's document");
let mount_point = document
.get_element_by_id(parent_div_id)
.expect("Problem finding parent div");
Self {
cfg: Rc::new(AppCfg {
document,
mount_point,
update,
view,
window_events,
}),
data: Rc::new(AppData {
model: RefCell::new(Some(model)),
main_el_vdom: RefCell::new(None),
popstate_closure: RefCell::new(None),
routes: RefCell::new(routes),
window_listeners: RefCell::new(Vec::new()),
msg_listeners: RefCell::new(Vec::new()),
}),
}
}
pub fn run(self) -> Self {
let window = util::window();
let mut topel_vdom = {
let model = self.data.model.borrow();
let model = model.as_ref().expect("missing model");
(self.cfg.view)(self.clone(), model)
};
if self.cfg.window_events.is_some() {
setup_window_listeners(
&util::window(),
&mut Vec::new(),
&mut Vec::new(),
&self.mailbox(),
);
}
let document = window.document().expect("Problem getting document");
setup_els(&document, &mut topel_vdom, 0, 0);
attach_listeners(&mut topel_vdom, &self.mailbox());
websys_bridge::attach_el_and_children(&mut topel_vdom, &self.cfg.mount_point);
self.data.main_el_vdom.replace(Some(topel_vdom));
if let Some(routes) = self.data.routes.borrow().clone() {
routing::setup_popstate_listener(
&routing::initial(self.clone(), routes),
routes
);
routing::setup_link_listener(&self, routes);
}
panic::set_hook(Box::new(console_error_panic_hook::hook));
self
}
fn call_update(&self, message: Ms) -> (bool, Option<Ms>) {
let model = self.data.model.borrow_mut().take().expect("missing model");
let updated_model_wrapped = (self.cfg.update)(message, model);
let mut should_render = true;
let mut effect_msg = None;
let model = match updated_model_wrapped {
Update::Render(mdl) => mdl,
Update::Skip(mdl) => {
should_render = false;
mdl
},
Update::RenderThen(mdl, msg) => {
effect_msg = Some(msg);
mdl
}
};
self.data.model.borrow_mut().replace(model);
(should_render, effect_msg)
}
pub fn update(&self, message: Ms) {
for l in self.data.msg_listeners.borrow().iter() {
(l)(&message)
}
let (should_render, effect_msg) = self.call_update(message);
let model = self.data.model.borrow();
let model = model.as_ref().expect("missing model");
if let Some(window_events) = self.cfg.window_events {
let mut new_listeners = (window_events)(model);
setup_window_listeners(
&util::window(),
&mut self.data.window_listeners.borrow_mut(),
&mut new_listeners,
&self.mailbox(),
);
self.data.window_listeners.replace(new_listeners);
}
if should_render {
let mut topel_new_vdom = (self.cfg.view)(self.clone(), model);
setup_els(&self.cfg.document, &mut topel_new_vdom, 0, 0);
let mut old_vdom = self.data.main_el_vdom.borrow_mut().take().expect("missing main_el_vdom");
detach_listeners(&mut old_vdom);
patch(
&self.cfg.document,
old_vdom,
&mut topel_new_vdom,
&self.cfg.mount_point,
&self.mailbox(),
);
self.data.main_el_vdom.borrow_mut().replace(topel_new_vdom);
}
if let Some(msg) = effect_msg {
self.update(msg)
}
}
pub fn add_message_listener<F>(&self, listener: F)
where
F: Fn(&Ms) + 'static,
{
self.data
.msg_listeners
.borrow_mut()
.push(Box::new(listener));
}
fn mailbox(&self) -> Mailbox<Ms> {
let cloned = self.clone();
Mailbox::new(move |message| {
cloned.update(message);
})
}
}
pub(crate) fn setup_els<Ms>(document: &Document, el_vdom: &mut El<Ms>, active_level: u32, active_id: u32)
where
Ms: Clone + 'static,
{
let mut id = active_id;
el_vdom.id = Some(id);
id += 1;
el_vdom.nest_level = Some(active_level);
if el_vdom.tag == dom_types::Tag::Input || el_vdom.tag == dom_types::Tag::Select || el_vdom.tag == dom_types::Tag::TextArea {
let listener = if let Some(checked) = el_vdom.attrs.vals.get(&dom_types::At::Checked) {
let checked_bool = match checked.as_ref() {
"true" => true,
"false" => false,
_ => panic!("checked must be true or false.")
};
dom_types::Listener::new_control_check(checked_bool)
} else if let Some(control_val) = el_vdom.attrs.vals.get(&dom_types::At::Value) {
dom_types::Listener::new_control(control_val.to_string())
} else {
dom_types::Listener::new_control("".to_string())
};
el_vdom.listeners.push(listener);
}
let el_ws = websys_bridge::make_websys_el(el_vdom, document);
el_vdom.el_ws = Some(el_ws);
for child in &mut el_vdom.children {
setup_els(document, child, active_level + 1, id);
id += 1;
}
}
impl<Ms: Clone, Mdl> Clone for App<Ms, Mdl> {
fn clone(&self) -> Self {
App {
cfg: Rc::clone(&self.cfg),
data: Rc::clone(&self.data),
}
}
}
fn attach_listeners<Ms: Clone>(el: &mut dom_types::El<Ms>, mailbox: &Mailbox<Ms>) {
let el_ws = el
.el_ws
.take()
.expect("Missing el_ws on attach_all_listeners");
for listener in &mut el.listeners {
if listener.control_val.is_some() || listener.control_checked.is_some() {
listener.attach_control(&el_ws);
} else {
listener.attach(&el_ws, mailbox.clone());
}
}
for child in &mut el.children {
attach_listeners(child, mailbox)
}
el.el_ws.replace(el_ws);
}
fn detach_listeners<Ms: Clone>(el: &mut dom_types::El<Ms>) {
let el_ws = el
.el_ws
.take();
let el_ws2;
match el_ws {
Some(e) => el_ws2 = e,
None => return
}
for listener in &mut el.listeners {
listener.detach(&el_ws2);
}
for child in &mut el.children {
detach_listeners(child)
}
el.el_ws.replace(el_ws2);
}
fn setup_window_listeners<Ms: Clone>(
window: &Window,
old: &mut Vec<dom_types::Listener<Ms>>,
new: &mut Vec<dom_types::Listener<Ms>>,
mailbox: &Mailbox<Ms>,
) {
for listener in old {
listener.detach(window);
}
for listener in new {
listener.attach(window, mailbox.clone());
}
}
fn patch<Ms: Clone>(
document: &Document,
mut old: El<Ms>,
new: &mut El<Ms>,
parent: &web_sys::Node,
mailbox: &Mailbox<Ms>,
) {
let old_el_ws = match old.el_ws.take() {
Some(o) => o,
None => return
};
if old != *new {
if new.empty && !old.empty {
parent.remove_child(&old_el_ws)
.expect("Problem removing old we_el when updating to empty");
if let Some(unmount_actions) = &mut old.hooks.will_unmount {
unmount_actions(&old_el_ws)
}
return
}
else if old.tag != new.tag || old.namespace != new.namespace || old.empty != new.empty {
if let Some(unmount_actions) = &mut old.hooks.will_unmount {
unmount_actions(&old_el_ws)
}
websys_bridge::attach_children(new);
let new_el_ws = new.el_ws.take().expect("Missing websys el");
if old.empty {
parent.append_child(&new_el_ws)
.expect("Problem adding element to previously empty one");
} else {
parent
.replace_child(&new_el_ws, &old_el_ws)
.expect("Problem replacing element");
}
if let Some(mount_actions) = &mut new.hooks.did_mount {
mount_actions(&new_el_ws)
}
new.el_ws.replace(new_el_ws);
let mut new = new;
attach_listeners(&mut new, &mailbox);
return
}
websys_bridge::patch_el_details(&mut old, new, &old_el_ws);
}
for listener in &mut new.listeners {
if listener.control_val.is_some() || listener.control_checked.is_some() {
listener.attach_control(&old_el_ws);
} else {
listener.attach(&old_el_ws, mailbox.clone());
}
}
let num_children_in_both = old.children.len().min(new.children.len());
let mut old_children_iter = old.children.into_iter();
let mut new_children_iter = new.children.iter_mut();
for _i in 0..num_children_in_both {
let child_old = old_children_iter.next().unwrap();
let child_new = new_children_iter.next().unwrap();
patch(document, child_old, child_new, &old_el_ws, &mailbox);
}
while let Some(child_new) = new_children_iter.next() {
websys_bridge::attach_el_and_children(child_new, &old_el_ws);
attach_listeners(child_new, &mailbox);
}
while let Some(mut child) = old_children_iter.next() {
let child_el_ws = child.el_ws.take().expect("Missing child el_ws");
if let Some(unmount_actions) = &mut child.hooks.will_unmount {
unmount_actions(&child_el_ws)
}
match old_el_ws.remove_child(&child_el_ws) {
Ok(_) => {},
Err(_) => {crate::log("Minor error patching html element. (remove)");}
}
}
new.el_ws = Some(old_el_ws);
}
pub trait _Attrs: PartialEq + ToString {
fn vals(self) -> HashMap<String, String>;
}
pub trait _Style: PartialEq + ToString {
fn vals(self) -> HashMap<String, String>;
}
pub trait _Listener<Ms>: Sized {
fn attach<T: AsRef<EventTarget>>(&mut self, el_ws: &T, mailbox: Mailbox<Ms>);
fn detach<T: AsRef<EventTarget>>(&self, el_ws: &T);
}
pub trait _DomEl<Ms>: Sized + PartialEq + DomElLifecycle {
type Tg: PartialEq + ToString;
type At: _Attrs;
type St: _Style;
type Ls: _Listener<Ms>;
type Tx: PartialEq + ToString + Clone + Default;
fn tag(self) -> Self::Tg;
fn attrs(self) -> Self::At;
fn style(self) -> Self::St;
fn listeners(self) -> Vec<Self::Ls>;
fn text(self) -> Option<Self::Tx>;
fn children(self) -> Vec<Self>;
fn websys_el(self) -> Option<web_sys::Element>;
fn id(self) -> Option<u32>;
fn namespace(self) -> Option<Namespace>;
fn empty(self) -> Self;
fn set_id(&mut self, id: Option<u32>);
fn set_websys_el(&mut self, el: Option<Element>);
}
pub trait DomElLifecycle {
fn did_mount(self) -> Option<Box<FnMut(&Element)>>;
fn did_update(self) -> Option<Box<FnMut(&Element)>>;
fn will_unmount(self) -> Option<Box<FnMut(&Element)>>;
}
#[cfg(test)]
pub mod tests {
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use super::*;
use crate as seed;
use crate::{class, div, li, prelude::*, span};
use wasm_bindgen::JsCast;
#[derive(Clone, Debug)]
enum Msg {}
fn make_vdom(doc: &Document, el: El<Msg>) -> El<Msg> {
let mut vdom = el;
setup_els(doc, &mut vdom, 0, 0);
vdom
}
fn setup_and_patch(
doc: &Document,
parent: &Element,
mailbox: &Mailbox<Msg>,
old_vdom: El<Msg>,
new_vdom: El<Msg>,
) -> El<Msg> {
let mut new_vdom = make_vdom(&doc, new_vdom);
patch(&doc, old_vdom, &mut new_vdom, parent, mailbox);
new_vdom
}
#[wasm_bindgen_test]
fn el_added() {
let mailbox = Mailbox::new(|_msg: Msg| {});
let doc = util::document();
let parent = doc.create_element("div").unwrap();
let mut vdom = make_vdom(&doc, El::empty(seed::dom_types::Tag::Div));
let old_ws = vdom.el_ws.as_ref().unwrap().clone();
parent.append_child(&old_ws).unwrap();
assert_eq!(parent.children().length(), 1);
assert_eq!(old_ws.child_nodes().length(), 0);
vdom = setup_and_patch(&doc, &parent, &mailbox, vdom, div!["text"]);
assert_eq!(parent.children().length(), 1);
assert!(old_ws.is_same_node(parent.first_child().as_ref()));
assert_eq!(old_ws.child_nodes().length(), 1);
assert_eq!(old_ws.first_child().unwrap().text_content().unwrap(), "text");
setup_and_patch(
&doc,
&parent,
&mailbox,
vdom,
div!["text", "more text", vec![li!["even more text"]]],
);
assert_eq!(parent.children().length(), 1);
assert!(old_ws.is_same_node(parent.first_child().as_ref()));
assert_eq!(old_ws.child_nodes().length(), 3);
assert_eq!(old_ws.child_nodes().item(0).unwrap().text_content().unwrap(), "text");
assert_eq!(old_ws.child_nodes().item(1).unwrap().text_content().unwrap(), "more text");
let child3 = old_ws.child_nodes().item(2).unwrap();
assert_eq!(child3.node_name(), "LI");
assert_eq!(child3.text_content().unwrap(), "even more text");
}
#[wasm_bindgen_test]
fn el_removed() {
let mailbox = Mailbox::new(|_msg: Msg| {});
let doc = util::document();
let parent = doc.create_element("div").unwrap();
let mut vdom = make_vdom(&doc, El::empty(seed::dom_types::Tag::Div));
let old_ws = vdom.el_ws.as_ref().unwrap().clone();
parent.append_child(&old_ws).unwrap();
vdom = setup_and_patch(
&doc,
&parent,
&mailbox,
vdom,
div!["text", "more text", vec![li!["even more text"]]],
);
assert_eq!(parent.children().length(), 1);
assert_eq!(old_ws.child_nodes().length(), 3);
let old_child1 = old_ws.child_nodes().item(0).unwrap();
setup_and_patch(
&doc,
&parent,
&mailbox,
vdom,
div!["text"],
);
assert_eq!(parent.children().length(), 1);
assert!(old_ws.is_same_node(parent.first_child().as_ref()));
assert_eq!(old_ws.child_nodes().length(), 1);
assert!(old_child1.is_same_node(old_ws.child_nodes().item(0).as_ref()));
}
#[wasm_bindgen_test]
fn el_changed() {
let mailbox = Mailbox::new(|_msg: Msg| {});
let doc = util::document();
let parent = doc.create_element("div").unwrap();
let mut vdom = make_vdom(&doc, El::empty(seed::dom_types::Tag::Div));
let old_ws = vdom.el_ws.as_ref().unwrap().clone();
parent.append_child(&old_ws).unwrap();
vdom = setup_and_patch(
&doc,
&parent,
&mailbox,
vdom,
div![span!["hello"], ", ", span!["world"]],
);
assert_eq!(parent.child_nodes().length(), 1);
assert_eq!(old_ws.child_nodes().length(), 3);
setup_and_patch(
&doc,
&parent,
&mailbox,
vdom,
div![
span![class!["first"], "hello"],
", ",
span![class!["second"], "world"],
],
);
let child1 = old_ws.child_nodes().item(0).unwrap().dyn_into::<Element>().unwrap();
assert_eq!(child1.get_attribute("class"), Some("first".to_string()));
let child3 = old_ws.child_nodes().item(2).unwrap().dyn_into::<Element>().unwrap();
assert_eq!(child3.get_attribute("class"), Some("second".to_string()));
}
}