diff --git a/Cargo.lock b/Cargo.lock index af16292..8d9cfc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,7 +195,7 @@ dependencies = [ [[package]] name = "respo" -version = "0.1.9" +version = "0.1.14" dependencies = [ "cirru_parser", "js-sys", diff --git a/README.md b/README.md index 5a534ed..37cbf19 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Here is some preview of DOM syntax: Ok( div() .class(ui_global()) - .style(RespoStyle::default().padding(12.0)) + .style(respo_style().padding(12.0)) .children([ comp_counter(&states.pick("counter"), store.counted)?, comp_panel(&states.pick("panel"))?, @@ -35,15 +35,15 @@ static_styles!( style_remove_button, ( "&", - RespoStyle::default() - .width(CssSize::Px(16.0)) - .height(CssSize::Px(16.0)) + respo_style() + .width(16.px()) + .height(16.px()) .margin(4.) - .cursor("pointer".to_owned()) + .cursor("pointer") .margin4(0.0, 0.0, 0.0, 16.0) .color(CssColor::Hsl(0, 90, 90)), ), - ("&:hover", RespoStyle::default().color(CssColor::Hsl(0, 90, 80))), + ("&:hover", respo_style().color(CssColor::Hsl(0, 90, 80))), ); ``` @@ -146,7 +146,7 @@ impl RespoApp for App { Ok( div() .class(ui_global()) - .style(RespoStyle::default().padding(12.0)) + .style(respo_style().padding(12.0)) .children([ comp_counter(&states.pick("counter"), store.counted)?, comp_panel(&states.pick("panel"))?, diff --git a/demo_respo/src/counter.rs b/demo_respo/src/counter.rs index 59b9f29..f119285 100644 --- a/demo_respo/src/counter.rs +++ b/demo_respo/src/counter.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use respo::{ br, button, - css::{CssColor, RespoStyle}, + css::{respo_style, CssColor}, div, span, ui::ui_button, util, DispatchFn, RespoElement, RespoEvent, @@ -85,21 +85,21 @@ pub fn comp_counter(states: &RespoStatesTree, global_counted: i32) -> Result Result, String> { + let cursor = states.path(); + + let state = states.cast_branch::(); + + let on_inc = { + let cursor = cursor.to_owned(); + let state = state.to_owned(); + move |e, dispatch: DispatchFn<_>| -> Result<(), String> { + util::log!("click {:?}", e); + if let RespoEvent::Click { original_event, .. } = e { + original_event.prevent_default(); + } + + dispatch.run(ActionOp::Increment)?; + dispatch.run_state( + &cursor, + InnerTextState { + inner_text: !state.inner_text, + }, + )?; + Ok(()) + } + }; + + Ok( + div().elements([ + div().elements([button() + .class(ui_button()) + .inner_text("Switch inner text") + .style(respo_style().margin(4)) + .on_click(on_inc)]), + div().elements([if state.inner_text { + div().inner_text("inner text") + } else { + div().elements([span().inner_text("child 1"), span().inner_text("child 2")]) + }]), + ]), + ) +} diff --git a/demo_respo/src/main.rs b/demo_respo/src/main.rs index 7f3fb87..2c626e2 100644 --- a/demo_respo/src/main.rs +++ b/demo_respo/src/main.rs @@ -1,6 +1,7 @@ extern crate console_error_panic_hook; mod counter; +mod inner_text; mod panel; mod plugins; mod store; @@ -11,12 +12,14 @@ use std::cell::{Ref, RefCell}; use std::panic; use std::rc::Rc; -use respo::RespoAction; +use inner_text::comp_inner_text; +use respo::css::respo_style; +use respo::{space, RespoAction}; use web_sys::Node; use respo::ui::ui_global; -use respo::{css::RespoStyle, util, RespoApp, RespoNode, RespoStore}; use respo::{div, util::query_select_node}; +use respo::{util, RespoApp, RespoNode, RespoStore}; use self::counter::comp_counter; pub use self::store::ActionOp; @@ -62,12 +65,17 @@ impl RespoApp for App { Ok( div() .class(ui_global()) - .style(RespoStyle::default().padding(12.0)) + .style(respo_style().padding(12)) .children([ comp_counter(&states.pick("counter"), store.counted)?.to_node(), + space(None, Some(80)).to_node(), comp_panel(&states.pick("panel"))?, comp_todolist(&states.pick("todolist"), &store.tasks)?.to_node(), + space(None, Some(80)).to_node(), comp_plugins_demo(&states.pick("plugins-demo"))?.to_node(), + space(None, Some(80)).to_node(), + comp_inner_text(&states.pick("inner-text"))?.to_node(), + space(None, Some(80)).to_node(), ]) .to_node(), ) diff --git a/demo_respo/src/panel.rs b/demo_respo/src/panel.rs index 735bf56..343d2c4 100644 --- a/demo_respo/src/panel.rs +++ b/demo_respo/src/panel.rs @@ -65,9 +65,8 @@ pub fn comp_panel(states: &RespoStatesTree) -> Result, Strin "panel", div().elements([ input() + .attrs(&[("placeholder", "some content..."), ("value", state.content.as_str())]) .class(ui_input()) - .attribute("placeholder", "some content...") - .attribute("value", state.content.to_owned()) .on_input(on_input), space(Some(8), None), button().class(ui_button()).inner_text("add").on_click(on_submit), diff --git a/demo_respo/src/plugins.rs b/demo_respo/src/plugins.rs index 6b52999..8d3b04d 100644 --- a/demo_respo/src/plugins.rs +++ b/demo_respo/src/plugins.rs @@ -1,5 +1,6 @@ +use respo::css::respo_style; use respo::ui::{ui_button_danger, ui_button_primary}; -use respo::{css::RespoStyle, space, ui::ui_row_parted}; +use respo::{space, ui::ui_row_parted}; use respo::{RespoElement, RespoEvent}; use respo::{button, div, span, ui::ui_button, util, DispatchFn}; @@ -20,7 +21,7 @@ pub fn comp_plugins_demo(states: &RespoStatesTree) -> Result| { @@ -116,7 +117,7 @@ pub fn comp_plugins_demo(states: &RespoStatesTree) -> Result Result Result Result { - util::log!("error: {:?}", e); + util::error_log!("error: {:?}", e); } }, _ => { diff --git a/respo/src/app/diff.rs b/respo/src/app/diff.rs index 273af4c..1cee155 100644 --- a/respo/src/app/diff.rs +++ b/respo/src/app/diff.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::rc::Rc; @@ -87,14 +88,14 @@ where ( a @ RespoNode::Element(RespoElement { name, - attrs, + attributes: attrs, style, event, children, }), RespoNode::Element(RespoElement { name: old_name, - attrs: old_attrs, + attributes: old_attrs, style: old_style, event: old_event, children: old_children, @@ -109,7 +110,8 @@ where }); collect_effects_outside_in_as(new_tree, coord, dom_path, RespoEffectType::Mounted, changes)?; } else { - diff_attrs(attrs, old_attrs, coord, dom_path, changes); + let reset_inner = RefCell::new(false); + diff_attrs(attrs, old_attrs, coord, dom_path, changes, &reset_inner); diff_style( &HashMap::from_iter(style.0.to_owned()), &HashMap::from_iter(old_style.0.to_owned()), @@ -119,7 +121,12 @@ where ); diff_event(event, old_event, coord, dom_path, changes); - diff_children(children, old_children, coord, dom_path, changes)?; + if *reset_inner.borrow() { + // children is empty after innerHTML or innerText changed + diff_children(children, &[], coord, dom_path, changes)?; + } else { + diff_children(children, old_children, coord, dom_path, changes)?; + } } } (RespoNode::Referenced(new_cell), RespoNode::Referenced(old_cell)) => { @@ -147,6 +154,7 @@ fn diff_attrs( coord: &[RespoCoord], dom_path: &[u32], changes: &mut Vec>, + reset_inner: &RefCell, ) where T: Debug + Clone, { @@ -156,15 +164,24 @@ fn diff_attrs( if old_attrs.contains_key(key) { if &old_attrs[key] != value { added.insert(key.to_owned(), value.to_owned()); + if inner_changed(key) { + *reset_inner.borrow_mut() = true; + } } } else { added.insert(key.to_owned(), value.to_owned()); + if inner_changed(key) { + *reset_inner.borrow_mut() = true; + } } } for key in old_attrs.keys() { if !new_attrs.contains_key(key) { removed.insert(key.to_owned()); + if inner_changed(key) { + *reset_inner.borrow_mut() = true; + } } } @@ -178,6 +195,11 @@ fn diff_attrs( } } +/// changed innerHTML or innerText, which resets children values +fn inner_changed(key: &Rc) -> bool { + key == &"innerHTML".into() || key == &"innerText".into() || key == &"inner-text".into() +} + fn diff_style( new_style: &HashMap, String>, old_style: &HashMap, String>, diff --git a/respo/src/app/patch.rs b/respo/src/app/patch.rs index e108c99..0a389ca 100644 --- a/respo/src/app/patch.rs +++ b/respo/src/app/patch.rs @@ -13,6 +13,7 @@ use web_sys::console::warn_1; use crate::node::{RespoComponent, RespoEffectType, RespoEvent, RespoEventMark, RespoEventMarkFn, RespoNode}; use super::renderer::load_coord_target_tree; +use super::util; use crate::node::dom_change::{ChildDomOp, DomChange, RespoCoord}; use crate::app::renderer::build_dom_tree; @@ -59,7 +60,7 @@ where } } } else { - crate::util::log!("expected component for effects, got: {}", target_tree); + crate::util::warn_log!("expected component for effects, got: {}", target_tree); } } } @@ -198,7 +199,10 @@ where .expect("get node") .children() .item(*idx) - .ok_or_else(|| format!("child to remove not found at {}", &idx))?; + .ok_or_else(|| { + util::warn_log!("child not found at {:?}", coord); + format!("child to remove not found at {}", &idx) + })?; target.remove_child(&child).expect("child removed"); } ChildDomOp::InsertAfter(idx, k, node) => { @@ -243,7 +247,7 @@ where } } } else { - crate::util::log!("expected component for effects, got: {}", target_tree); + crate::util::warn_log!("expected component for effects, got: {}", target_tree); } } } @@ -272,7 +276,7 @@ where } } } else { - crate::util::log!("expected component for effects, got: {}", target_tree); + crate::util::warn_log!("expected component for effects, got: {}", target_tree); } } } @@ -386,11 +390,25 @@ pub fn attach_event(element: &Element, key: &str, coord: &[RespoCoord], handle_e .run(RespoEventMark::new("change", &coord, wrap_event)) .expect("handle change event"); }) as Box); - element - .dyn_ref::() - .expect("convert to html input element") - .set_onchange(Some(handler.as_ref().unchecked_ref())); - handler.forget(); + match element.tag_name().as_str() { + "INPUT" => { + element + .dyn_ref::() + .expect("convert to html input element") + .set_onchange(Some(handler.as_ref().unchecked_ref())); + handler.forget(); + } + "TEXTAREA" => { + element + .dyn_ref::() + .expect("convert to html input element") + .set_onchange(Some(handler.as_ref().unchecked_ref())); + handler.forget(); + } + _ => { + util::warn_log!("not handled change event for element: {}", element.tag_name()); + } + } } "keydown" => { let handler = Closure::wrap(Box::new(move |e: KeyboardEvent| { @@ -409,11 +427,26 @@ pub fn attach_event(element: &Element, key: &str, coord: &[RespoCoord], handle_e .run(RespoEventMark::new("keydown", &coord, wrap_event)) .expect("handle keydown event"); }) as Box); - element - .dyn_ref::() - .expect("convert to html input element") - .set_onkeydown(Some(handler.as_ref().unchecked_ref())); - handler.forget(); + + match element.tag_name().as_str() { + "INPUT" => { + element + .dyn_ref::() + .expect("convert to html input element") + .set_onkeydown(Some(handler.as_ref().unchecked_ref())); + handler.forget(); + } + "TEXTAREA" => { + element + .dyn_ref::() + .expect("convert to html input element") + .set_onkeydown(Some(handler.as_ref().unchecked_ref())); + handler.forget(); + } + _ => { + util::warn_log!("not handled keydown event for element: {}", element.tag_name()); + } + } } "keyup" => { let handler = Closure::wrap(Box::new(move |e: KeyboardEvent| { @@ -431,11 +464,25 @@ pub fn attach_event(element: &Element, key: &str, coord: &[RespoCoord], handle_e .run(RespoEventMark::new("keyup", &coord, wrap_event)) .expect("handle keyup event"); }) as Box); - element - .dyn_ref::() - .expect("convert to html input element") - .set_onkeyup(Some(handler.as_ref().unchecked_ref())); - handler.forget(); + match element.tag_name().as_str() { + "INPUT" => { + element + .dyn_ref::() + .expect("convert to html input element") + .set_onkeyup(Some(handler.as_ref().unchecked_ref())); + handler.forget(); + } + "TEXTAREA" => { + element + .dyn_ref::() + .expect("convert to html input element") + .set_onkeyup(Some(handler.as_ref().unchecked_ref())); + handler.forget(); + } + _ => { + util::warn_log!("not handled keyup event for element: {}", element.tag_name()); + } + } } "keypress" => { let handler = Closure::wrap(Box::new(move |e: KeyboardEvent| { @@ -453,11 +500,25 @@ pub fn attach_event(element: &Element, key: &str, coord: &[RespoCoord], handle_e .run(RespoEventMark::new("keypress", &coord, wrap_event)) .expect("handle keypress event"); }) as Box); - element - .dyn_ref::() - .expect("convert to html input element") - .set_onkeypress(Some(handler.as_ref().unchecked_ref())); - handler.forget(); + match element.tag_name().as_str() { + "INPUT" => { + element + .dyn_ref::() + .expect("convert to html input element") + .set_onkeypress(Some(handler.as_ref().unchecked_ref())); + handler.forget(); + } + "TEXTAREA" => { + element + .dyn_ref::() + .expect("convert to html input element") + .set_onkeypress(Some(handler.as_ref().unchecked_ref())); + handler.forget(); + } + _ => { + util::warn_log!("not handled keypress event for element: {}", element.tag_name()); + } + } } "focus" => { let handler = Closure::wrap(Box::new(move |e: FocusEvent| { diff --git a/respo/src/app/renderer.rs b/respo/src/app/renderer.rs index fd13885..6682346 100644 --- a/respo/src/app/renderer.rs +++ b/respo/src/app/renderer.rs @@ -3,6 +3,7 @@ use crate::node::dom_change::RespoCoord; use crate::node::{ DispatchFn, DomChange, RespoComponent, RespoEffectType, RespoElement, RespoEventMark, RespoEventMarkFn, RespoListenerFn, RespoNode, }; +use crate::warn_log; use std::cell::RefCell; use std::fmt::Debug; use std::rc::Rc; @@ -10,7 +11,7 @@ use std::sync::RwLock; use wasm_bindgen::{JsCast, JsValue}; use web_sys::console::{error_1, warn_1}; -use web_sys::{HtmlElement, HtmlLabelElement, Node}; +use web_sys::{HtmlElement, HtmlInputElement, HtmlLabelElement, HtmlTextAreaElement, Node}; use crate::app::diff::{collect_effects_outside_in_as, diff_tree}; use crate::app::patch::{attach_event, patch_tree}; @@ -25,8 +26,8 @@ fn drain_rerender_status() -> bool { let ret = { *NEED_TO_ERENDER.read().expect("to drain rerender status") }; if ret { - let mut need_to_erender = NEED_TO_ERENDER.write().expect("to drain rerender status"); - *need_to_erender = false; + let mut need_to_rerender = NEED_TO_ERENDER.write().expect("to drain rerender status"); + *need_to_rerender = false; } ret } @@ -108,17 +109,15 @@ where let mut changes: Vec> = vec![]; diff_tree(&new_tree, &to_prev_tree.borrow(), &Vec::new(), &Vec::new(), &mut changes)?; + // use cirru_parser::CirruWriterOptions; // util::log!( // "prev tree: {}", - // cirru_parser::format( - // &[to_prev_tree2.borrow().to_owned().into()], - // cirru_parser::CirruWriterOptions { use_inline: true } - // ) - // .unwrap() + // cirru_parser::format(&[to_prev_tree.borrow().to_owned().into()], CirruWriterOptions { use_inline: true }).unwrap() // ); + // use crate::dom_change::changes_to_cirru; // util::log!( // "changes: {}", - // cirru_parser::format(&[changes_to_cirru(&changes)], cirru_parser::CirruWriterOptions { use_inline: true }).unwrap() + // cirru_parser::format(&[changes_to_cirru(&changes)], CirruWriterOptions { use_inline: true }).unwrap() // ); let handler = handle_event.to_owned(); @@ -232,29 +231,49 @@ where } RespoNode::Element(RespoElement { name, - attrs, + attributes: attrs, style, event, children, }) => { let element = document.create_element(name)?; + let mut inner_set = false; for (key, value) in attrs { let key = key.as_ref(); - if key == "style" { - warn_1(&"style is handled outside attrs".into()); - } else if key == "innerText" { - element.dyn_ref::().expect("into html element").set_inner_text(value); - } else if key == "innerHTML" { - element.set_inner_html(value); - } else if key == "htmlFor" { - element.dyn_ref::().ok_or("to label element")?.set_html_for(value); - } else { - element.set_attribute(key, value)?; + match key { + "style" => warn_1(&"style is handled outside attrs".into()), + "innerText" => { + inner_set = true; + element.dyn_ref::().expect("into html element").set_inner_text(value) + } + "innerHTML" => { + inner_set = true; + element.set_inner_html(value) + } + "htmlFor" => element + .dyn_ref::() + .expect("into label element") + .set_html_for(value), + "value" if &**name == "textarea" => element + .dyn_ref::() + .expect("into textarea element") + .set_value(value), + "value" if &**name == "input" => element.dyn_ref::().expect("into input element").set_value(value), + _ => { + element.set_attribute(key, value)?; + } } } if !style.is_empty() { element.set_attribute("style", &style.to_string())?; } + if inner_set && !children.is_empty() { + warn_log!( + "innerText or innerHTML is set, it's conflicted with children: {} {:?}", + inner_set, + children + ); + } for (k, child) in children { let mut next_coord = coord.to_owned(); next_coord.push(RespoCoord::Key(k.to_owned())); diff --git a/respo/src/app/util.rs b/respo/src/app/util.rs index ab12183..94eab96 100644 --- a/respo/src/app/util.rs +++ b/respo/src/app/util.rs @@ -40,7 +40,10 @@ pub fn raf_loop_slow(interval: i32, mut cb: Box Result<(), String *g.borrow_mut() = Some(Closure::wrap(Box::new(move || { if let Err(e) = cb() { - crate::log!("failed in slow loop: {}", e); + crate::warn_log!( + "Failure in slow loop, program has to stop since inconsistent DOM states. Details: {}", + e + ); } let h = Closure::wrap(Box::new({ @@ -95,7 +98,7 @@ macro_rules! log { /// /// use it like: /// ```ignore -/// util::warn!("a is {}", a); +/// util::warn_log!("a is {}", a); /// ``` #[macro_export] macro_rules! warn_log { @@ -104,6 +107,20 @@ macro_rules! warn_log { }}; } +/// wraps on top of `web_sys::console.error_1`. +/// +/// use it like: +/// ```ignore +/// util::error_log!("a is {}", a); +/// ``` +#[macro_export] +macro_rules! error_log { + ($($t:tt)*) => {{ + web_sys::console::error_1(&format!($($t)*).into()); + }}; +} + +pub use error_log; pub use log; pub use warn_log; diff --git a/respo/src/node.rs b/respo/src/node.rs index ca8712e..450abd7 100644 --- a/respo/src/node.rs +++ b/respo/src/node.rs @@ -20,12 +20,13 @@ pub use element::RespoElement; use crate::states_tree::{DynEq, RespoStateBranch, RespoUpdateState}; -use css::RespoStyle; +use css::respo_style; pub(crate) use dom_change::RespoCoord; pub(crate) use dom_change::{ChildDomOp, DomChange}; pub use component::effect::{RespoEffect, RespoEffectType}; +pub use css::ConvertRespoCssSize; /// an `Element` or a `Component` #[derive(Debug, Clone, PartialEq, Eq)] @@ -123,9 +124,9 @@ where pub fn new_tag(name: &str) -> Self { Self::Element(RespoElement { name: name.into(), - attrs: HashMap::new(), + attributes: HashMap::new(), event: HashMap::new(), - style: RespoStyle::default(), + style: respo_style(), children: Vec::new(), }) } diff --git a/respo/src/node/css.rs b/respo/src/node/css.rs index f78c837..33183da 100644 --- a/respo/src/node/css.rs +++ b/respo/src/node/css.rs @@ -6,11 +6,11 @@ //! style_done_button, //! ( //! "&", -//! RespoStyle::default() -//! .width(CssSize::Px(24.0)) -//! .height(CssSize::Px(24.0)) +//! respo_style() +//! .width(24.px()) +//! .height(24.px()) //! .margin(4.) -//! .cursor("pointer".to_owned()) +//! .cursor("pointer") //! .background_color(CssColor::Hsl(20, 90, 70)), //! ) //! ); @@ -18,6 +18,8 @@ //! //! then `style_done_button()` returns the class name, while CSS is generated and injected into the `