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: Clone> {
pub model: RefCell<Mdl>,
main_el_vdom: RefCell<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: Clone + '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 + Clone> {
pub cfg: Rc<AppCfg<Ms, Mdl>>,
pub data: Rc<AppData<Ms, Mdl>>,
}
impl<Ms: Clone + 'static, Mdl: Clone + '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 + Clone> {
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: Clone> 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: Clone> 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(model),
main_el_vdom: RefCell::new(El::empty(dom_types::Tag::Div)),
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;
match self.cfg.window_events {
Some(_window_events) => {
topel_vdom = (self.cfg.view)(self.clone(), &self.data.model.borrow());
setup_window_listeners(
&util::window(),
&mut Vec::new(),
&mut Vec::new(),
&self.mailbox(),
);
}
None => {
topel_vdom = (self.cfg.view)(self.clone(), &self.data.model.borrow());
}
}
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(topel_vdom);
if let Some(routes) = self.data.routes.borrow().clone() {
routing::setup_popstate_listener(
&routing::initial(self.clone(), routes),
routes
);
}
panic::set_hook(Box::new(console_error_panic_hook::hook));
self
}
pub fn update(&self, message: Ms) {
for l in self.data.msg_listeners.borrow().iter() {
(l)(&message)
}
let model_to_update = self.data.model.borrow().clone();
let updated_model_wrapped = (self.cfg.update)(message, model_to_update);
let mut should_render = true;
let mut effect_msg = None;
let updated_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
}
};
if let Some(window_events) = self.cfg.window_events {
let mut new_listeners = (window_events)(&updated_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(), &updated_model);
setup_els(&self.cfg.document, &mut topel_new_vdom, 0, 0);
detach_listeners(&mut self.data.main_el_vdom.borrow_mut());
patch(
&self.cfg.document,
&mut self.data.main_el_vdom.borrow_mut(),
&mut topel_new_vdom,
&self.cfg.mount_point,
&self.mailbox(),
);
self.data.main_el_vdom.replace(topel_new_vdom);
}
self.data.model.replace(updated_model);
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 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> 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,
old: &mut 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(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 mut old_children_patched = Vec::new();
for (i_new, child_new) in new.children.iter_mut().enumerate() {
match old.children.get(i_new) {
Some(child_old) => {
patch(document, &mut child_old.clone(), child_new, &old_el_ws, &mailbox);
old_children_patched.push(child_old.id.expect("Can't find child's id"));
},
None => {
websys_bridge::attach_el_and_children(child_new, &old_el_ws);
let mut child_new = child_new;
attach_listeners(&mut child_new, &mailbox);
}
}
}
for child in old.children.iter_mut()
.filter(|c| !old_children_patched
.contains(&c.id.expect("Can't find child's id")) ) {
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)");}
}
child.el_ws.replace(child_el_ws);
}
new.el_ws = Some(old_el_ws);
}
fn _match_score<Ms: Clone>(old: &El<Ms>, old_posit: usize, new: &El<Ms>, new_posit: usize) -> f32 {
let child_score_significance: f32 = 0.6;
let mut score = 0.;
if old.tag == new.tag {
score += 0.3
} else {
score -= 0.3
};
if old_posit == new_posit {
score += 0.1
}
if old.attrs == new.attrs {
score += 0.15
} else {
score -= 0.15
};
if old.style == new.style {
score += 0.05
} else {
score -= 0.05
};
if old.text == new.text {
score += 0.05
} else {
score -= 0.05
};
if old.get_text() == new.get_text() {
score += 0.15
} else {
score -= 0.15
};
if old.children.len() == new.children.len() {
score += 0.1
} else {
score -= 0.1
}
if old.id.expect("Missing id") == new.id.expect("Missing id") {
score += 0.15
} else {
score -= 0.15
};
for posit in 0..old.children.len() {
if let Some(child_old) = &old.children.get(posit) {
if let Some(child_new) = &new.children.get(posit) {
score += child_score_significance.powi(posit as i32) *
_match_score(child_old, posit, child_new, posit);
}
}
}
score
}
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;
wasm_bindgen_test_configure!(run_in_browser);
use super::*;
use wasm_bindgen_test::*;
use crate as seed;
use crate::{div, li, prelude::*};
#[derive(Clone)]
enum Msg {}
#[ignore]
#[test]
fn el_added() {
let mut old_vdom: El<Msg> = div!["text", vec![li!["child1"],]];
let mut new_vdom: El<Msg> = div!["text", vec![li!["child1"], li!["child2"]]];
let doc = util::document();
let old_ws = doc.create_element("div").unwrap();
let new_ws = doc.create_element("div").unwrap();
let child1 = doc.create_element("li").unwrap();
let child2 = doc.create_element("li").unwrap();
old_ws.set_text_content(Some("text"));
child1.set_text_content(Some("child1"));
child2.set_text_content(Some("child2"));
old_ws.append_child(&child1).unwrap();
new_ws.append_child(&child1).unwrap();
new_ws.append_child(&child2).unwrap();
let mailbox = Mailbox::new(|msg: Msg| {});
let parent = doc.create_element("div").unwrap();
patch(&doc, &mut old_vdom, &mut new_vdom, &parent, &mailbox);
unimplemented!()
}
#[ignore]
#[test]
fn el_removed() {
unimplemented!()
}
#[ignore]
#[test]
fn el_changed() {
unimplemented!()
}
}