diff --git a/Cargo.lock b/Cargo.lock index dc67025..116b9e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,7 +195,7 @@ dependencies = [ [[package]] name = "respo" -version = "0.1.5" +version = "0.1.6" dependencies = [ "cirru_parser", "js-sys", diff --git a/demo_respo/src/counter.rs b/demo_respo/src/counter.rs index dc05947..df7fa16 100644 --- a/demo_respo/src/counter.rs +++ b/demo_respo/src/counter.rs @@ -1,6 +1,12 @@ use std::fmt::Debug; -use respo::{button, div, span, ui::ui_button, util, CssColor, DispatchFn, RespoElement, RespoEvent, RespoStyle}; +use respo::{ + button, + css::{CssColor, RespoStyle}, + div, span, + ui::ui_button, + util, DispatchFn, RespoElement, RespoEvent, +}; use respo_state_derive::RespoState; use serde::{Deserialize, Serialize}; diff --git a/demo_respo/src/main.rs b/demo_respo/src/main.rs index ab7480a..9265af1 100644 --- a/demo_respo/src/main.rs +++ b/demo_respo/src/main.rs @@ -14,8 +14,8 @@ use std::rc::Rc; 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, RespoStyle}; use self::counter::comp_counter; pub use self::store::ActionOp; diff --git a/demo_respo/src/plugins.rs b/demo_respo/src/plugins.rs index 9c089be..6b52999 100644 --- a/demo_respo/src/plugins.rs +++ b/demo_respo/src/plugins.rs @@ -1,5 +1,5 @@ use respo::ui::{ui_button_danger, ui_button_primary}; -use respo::{space, ui::ui_row_parted, RespoStyle}; +use respo::{css::RespoStyle, space, ui::ui_row_parted}; use respo::{RespoElement, RespoEvent}; use respo::{button, div, span, ui::ui_button, util, DispatchFn}; diff --git a/demo_respo/src/store.rs b/demo_respo/src/store.rs index bed2d2d..28ca1e0 100644 --- a/demo_respo/src/store.rs +++ b/demo_respo/src/store.rs @@ -35,10 +35,10 @@ impl Hash for Task { pub enum ActionOp { #[default] Noop, - Increment, - Decrement, /// contains State and Value StatesChange(RespoUpdateState), + Increment, + Decrement, AddTask(String, String), RemoveTask(String), UpdateTask(String, String), @@ -54,21 +54,25 @@ impl RespoAction for ActionOp { impl RespoStore for Store { type Action = ActionOp; + fn get_states(&mut self) -> &mut RespoStatesTree { + &mut self.states + } + fn update(&mut self, op: Self::Action) -> Result<(), String> { use ActionOp::*; match op { Noop => { // nothing to to } + StatesChange(a) => { + self.update_states(a); + } Increment => { self.counted += 1; } Decrement => { self.counted -= 1; } - StatesChange(a) => { - self.states.set_in_mut(a); - } AddTask(id, content) => self.tasks.push(Task { id, content, diff --git a/demo_respo/src/task.rs b/demo_respo/src/task.rs index 60cf511..66e9fb9 100644 --- a/demo_respo/src/task.rs +++ b/demo_respo/src/task.rs @@ -4,9 +4,11 @@ use std::fmt::Debug; use memoize::memoize; use respo::{ - button, div, input, space, span, static_styles, + button, + css::{CssColor, CssSize, RespoStyle}, + div, input, space, span, static_styles, ui::{ui_button, ui_center, ui_input, ui_row_middle}, - util, CssColor, CssSize, DispatchFn, RespoComponent, RespoEffect, RespoEvent, RespoNode, RespoStyle, + util, DispatchFn, RespoComponent, RespoEffect, RespoEvent, RespoNode, }; use respo::states_tree::{RespoState, RespoStatesTree}; diff --git a/respo/Cargo.toml b/respo/Cargo.toml index 587fe23..e176807 100644 --- a/respo/Cargo.toml +++ b/respo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "respo" -version = "0.1.5" +version = "0.1.6" edition = "2021" description = "a tiny virtual DOM library migrated from ClojureScript" license = "Apache-2.0" diff --git a/respo/src/app.rs b/respo/src/app.rs index 2405d91..42b7181 100644 --- a/respo/src/app.rs +++ b/respo/src/app.rs @@ -15,7 +15,10 @@ use web_sys::{BeforeUnloadEvent, Node}; use renderer::render_node; -use crate::node::{DispatchFn, RespoAction, RespoNode}; +use crate::{ + node::{DispatchFn, RespoAction, RespoNode}, + states_tree::{RespoStatesTree, RespoUpdateState}, +}; const RESPO_APP_STORE_KEY: &str = "respo_app_respo_store_default"; @@ -132,6 +135,14 @@ pub trait RespoStore { type Action: Debug + Clone + RespoAction; fn update(&mut self, op: Self::Action) -> Result<(), String>; + /// a way to load states tree + fn get_states(&mut self) -> &mut RespoStatesTree; + + /// public API for updating states tree + fn update_states(&mut self, op: RespoUpdateState) { + self.get_states().set_in_mut(op); + } + /// for backup fn to_string(&self) -> String; diff --git a/respo/src/app/patch.rs b/respo/src/app/patch.rs index a25b954..e108c99 100644 --- a/respo/src/app/patch.rs +++ b/respo/src/app/patch.rs @@ -12,7 +12,7 @@ use web_sys::console::warn_1; use crate::node::{RespoComponent, RespoEffectType, RespoEvent, RespoEventMark, RespoEventMarkFn, RespoNode}; -use crate::load_coord_target_tree; +use super::renderer::load_coord_target_tree; use crate::node::dom_change::{ChildDomOp, DomChange, RespoCoord}; use crate::app::renderer::build_dom_tree; diff --git a/respo/src/app/renderer.rs b/respo/src/app/renderer.rs index 7818173..4fc384b 100644 --- a/respo/src/app/renderer.rs +++ b/respo/src/app/renderer.rs @@ -41,7 +41,7 @@ pub(crate) fn mark_need_rerender() { } /// render elements -pub fn render_node( +pub(crate) fn render_node( mount_target: Node, // TODO it copies the whole store, need to optimize get_store: Box U>, @@ -150,7 +150,7 @@ where Ok(()) } -pub fn load_coord_target_tree(tree: &RespoNode, coord: &[RespoCoord]) -> Result, String> +pub(crate) fn load_coord_target_tree(tree: &RespoNode, coord: &[RespoCoord]) -> Result, String> where T: Debug + Clone, { diff --git a/respo/src/app/util.rs b/respo/src/app/util.rs index 0304be1..7c9fc09 100644 --- a/respo/src/app/util.rs +++ b/respo/src/app/util.rs @@ -3,6 +3,7 @@ use wasm_bindgen::prelude::Closure; use wasm_bindgen::JsCast; use web_sys::Node; +/// this one uses `requestAnimationFrame` for calling #[allow(dead_code)] pub fn raf_loop(mut cb: Box Result<(), String>>) { let f_ = Rc::new(RefCell::new(None)); @@ -30,7 +31,8 @@ fn window() -> web_sys::Window { web_sys::window().expect("no global `window` exists") } -/// this API is used for development, prefer `req_loop` for fast response +/// uses `requestAnimationFrame` for calling, but with a interval to reduce cost. +/// prefer `req_loop` if you want to be faster #[allow(dead_code)] pub fn raf_loop_slow(interval: i32, mut cb: Box Result<(), String>>) { let f = Rc::new(RefCell::new(None)); @@ -76,7 +78,9 @@ pub fn query_select_node(pattern: &str) -> Result { } } -/// wraps on top of `web_sys::console.log_1`, use it like: +/// wraps on top of `web_sys::console.log_1`. +/// +/// use it like: /// ```ignore /// util::log!("a is {}", a); /// ``` diff --git a/respo/src/lib.rs b/respo/src/lib.rs index d36c134..531af1c 100644 --- a/respo/src/lib.rs +++ b/respo/src/lib.rs @@ -1,24 +1,45 @@ -//! A tiny framework around a **virtual DOM** library, compiles to WebAssembly, runs in the browser, declarative UI for building interactive web apps. +//! Tiny **virtual DOM** library, compiles to WebAssembly, runs in browser, building interactive web apps with declarative code. //! -//! Original design was [Respo.cljs](http://respo-mvc.org/), which is heavily influenced by React.js and ClojureScript. -//! This module is experimental since WebAssembly lacks of hot reloading. +//! This library is experimental, heavily influenced by React.js and ClojureScript. +//! Previously implementeded in [ClojureScript](https://github.com/Respo/respo.cljs) and [Calcit](https://github.com/Respo/respo.calcit). //! -//! It features: +//! ![Respo Logo](https://cdn.tiye.me/logo/respo.png) //! -//! - virtual DOM(however simplified in list diffing) -//! - components declaration with functions -//! - globals states with Store and Actions dispatching -//! - states tree with nested states(inherited from Respo.cljs , might be strange) -//! - CSS with Rust macros -//! - basic component effects of `Mounted, WillUpdate, Updated, WillUnmount` -//! - okay to [memoize](https://crates.io/crates/memoize) component functions +//! To build UI: //! -//! Meanwhile it does not include features like: +//! - there's Virtual DOM, although simplified, still flexible for declarative UI +//! - CSS with Rust macros, I call it "CSS in Rust" +//! - Effects, flow from data to DOM, for patching DOM manually on data change +//! - `respo::ui` provides basic style. Also try Modal, dialog, drawer components. //! -//! - ❌ macros for JSX syntax. Respo prefer types over tags -//! - ❌ updating component states in lifecycle. Respo enforces "unidirectional data flow" -//! - ❌ React-like hooks. Respo uses plain functions without tricky internal states -//! - ❌ Hot swapping. Respo.rs reload on edit and loads previous states from local storage. +//! To manage states: +//! +//! - Rust enum and pattern matching it really nice for Elm-style action dispatching +//! - global states tree and cursor to maintain states, may not be familiar but still being handy +//! - you may also write shared component like a "plugin" to manage states. +//! +//! To optimize: +//! +//! - components and elements are in functions, available for [memoize](https://crates.io/crates/memoize) +//! - well, it's Rust, you can do more... +//! +//! Meanwhile it does not support React features such as: +//! +//! - ❌ updating data from render. Respo enforces "unidirectional data flow". That's not allowed +//! - ❌ hooks API and context. Respo uses plain functions without tricky internal states +//! - ...does not have many other advanced features from React +//! +//! Rust and WebAssembly lacks tricks for hot reloading, +//! it's suggested to use [trunk](https://github.com/trunk-rs/trunk) to edit and reload the project during development. +//! App states including components states can be saved to local storage and reloaded. +//! +//! To start project, create your structs to implement traits: +//! +//! - `RespoStore` for global states and states tree, and `RespoAction` for updating +//! - `RespoApp` for MVC overview of the app, and more views, bind events +//! +//! say app is called `app`, you start app with `app.render_loop()`. +//! Check [Workflow](https://github.com/Respo/respo-rust-workflow/tree/c7cc0c0/src) for a working example. mod app; pub mod states_tree; @@ -26,10 +47,7 @@ pub mod states_tree; pub(crate) mod node; pub mod ui; -pub use node::css::*; pub use node::element::alias::*; pub use node::*; -pub use app::renderer::*; - pub use app::{util, RespoApp, RespoStore}; diff --git a/respo/src/node.rs b/respo/src/node.rs index 42d300f..b80fd23 100644 --- a/respo/src/node.rs +++ b/respo/src/node.rs @@ -22,7 +22,7 @@ use crate::states_tree::{DynEq, RespoStateBranch, RespoUpdateState}; use css::RespoStyle; -pub use dom_change::RespoCoord; +pub(crate) use dom_change::RespoCoord; pub(crate) use dom_change::{ChildDomOp, DomChange}; pub use component::effect::{RespoEffect, RespoEffectType}; @@ -145,7 +145,7 @@ where pub(crate) type StrDict = HashMap, String>; -pub fn str_dict_to_cirrus_dict(dict: &StrDict) -> Cirru { +pub(crate) fn str_dict_to_cirrus_dict(dict: &StrDict) -> Cirru { let mut xs = vec![]; for (k, v) in dict { xs.push(vec![Cirru::from(k.as_ref()), Cirru::from(v)].into()); diff --git a/respo/src/node/css.rs b/respo/src/node/css.rs index 6acef86..f78c837 100644 --- a/respo/src/node/css.rs +++ b/respo/src/node/css.rs @@ -1,3 +1,23 @@ +//! Define CSS styles in Rust and generate CSS class-name. +//! +//! ```rust +//! use respo::css::*; +//! respo::static_styles!( +//! style_done_button, +//! ( +//! "&", +//! RespoStyle::default() +//! .width(CssSize::Px(24.0)) +//! .height(CssSize::Px(24.0)) +//! .margin(4.) +//! .cursor("pointer".to_owned()) +//! .background_color(CssColor::Hsl(20, 90, 70)), +//! ) +//! ); +//! ``` +//! +//! then `style_done_button()` returns the class name, while CSS is generated and injected into the `