├── src ├── queue_render │ ├── svg │ │ ├── mod.rs │ │ └── list.rs │ ├── html │ │ ├── mod.rs │ │ ├── element.rs │ │ └── list.rs │ ├── base │ │ ├── mod.rs │ │ └── text.rs │ ├── dom │ │ ├── nodes.rs │ │ ├── list.rs │ │ ├── mod.rs │ │ └── text.rs │ ├── mod.rs │ └── val.rs ├── render │ ├── macros │ │ └── mod.rs │ ├── base │ │ ├── mod.rs │ │ ├── events.rs │ │ ├── nodes_extensions.rs │ │ ├── list.rs │ │ ├── attributes.rs │ │ └── text.rs │ ├── mod.rs │ ├── svg │ │ ├── mod.rs │ │ ├── element.rs │ │ ├── partial_list.rs │ │ ├── attributes_elements_with_ambiguous_names.rs │ │ ├── keyed_list.rs │ │ └── list.rs │ └── html │ │ ├── mod.rs │ │ ├── partial_list.rs │ │ ├── attributes_elements_with_ambiguous_names.rs │ │ ├── keyed_list.rs │ │ └── list.rs ├── element.rs ├── utils.rs ├── application.rs ├── macros.rs ├── dom │ ├── mod.rs │ └── text.rs ├── routing.rs └── lib.rs ├── examples ├── fetch │ ├── README.md │ ├── index.html │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── game_of_life │ ├── assets │ │ └── favicon.ico │ ├── index.html │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ └── cell.rs │ └── styles.css ├── components │ ├── README.md │ ├── index.html │ ├── Cargo.toml │ └── src │ │ ├── child.rs │ │ └── lib.rs ├── counter │ ├── index.html │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── counter_queue_render │ ├── index.html │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── boids │ ├── index.html │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── settings.rs │ │ ├── simulation.rs │ │ ├── slider.rs │ │ └── math.rs │ └── index.scss ├── todomvc │ ├── index.html │ ├── src │ │ └── utils.rs │ └── Cargo.toml └── svg_clock │ ├── index.html │ └── Cargo.toml ├── crates ├── examples │ ├── fetch │ │ ├── README.md │ │ ├── index.html │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── benchmark │ │ ├── Cargo.toml │ │ ├── index.html │ │ └── src │ │ │ ├── table.rs │ │ │ ├── header.rs │ │ │ └── main.rs │ ├── counter │ │ ├── Cargo.toml │ │ ├── index.html │ │ └── src │ │ │ └── main.rs │ ├── benchmark-gen │ │ ├── Cargo.toml │ │ ├── index.html │ │ └── src │ │ │ ├── table.rs │ │ │ ├── main.rs │ │ │ └── header.rs │ ├── counter-gen │ │ ├── Cargo.toml │ │ ├── index.html │ │ └── src │ │ │ └── main.rs │ ├── match_expr │ │ ├── Cargo.toml │ │ ├── index.html │ │ └── src │ │ │ └── main.rs │ ├── components │ │ ├── index.html │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── child.rs │ │ │ └── main.rs │ └── todomvc │ │ ├── Cargo.toml │ │ └── index.html ├── spair-macros │ ├── Cargo.toml │ └── src │ │ ├── component_for.rs │ │ └── lib.rs └── spairc │ ├── test.sh │ ├── src │ ├── events.rs │ ├── element │ │ └── values.rs │ ├── helper.rs │ ├── lib.rs │ ├── test_helper.rs │ └── routing.rs │ └── Cargo.toml ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── test.sh ├── Cargo.toml └── docs └── 01-introduction.md /src/queue_render/svg/mod.rs: -------------------------------------------------------------------------------- 1 | mod list; 2 | 3 | pub use list::*; 4 | -------------------------------------------------------------------------------- /src/queue_render/html/mod.rs: -------------------------------------------------------------------------------- 1 | mod element; 2 | mod list; 3 | 4 | pub use list::*; 5 | -------------------------------------------------------------------------------- /examples/fetch/README.md: -------------------------------------------------------------------------------- 1 | This is an example that uses `gloo-net` to fetch data from an API. 2 | -------------------------------------------------------------------------------- /crates/examples/fetch/README.md: -------------------------------------------------------------------------------- 1 | This is an example that uses `gloo-net` to fetch data from an API. 2 | -------------------------------------------------------------------------------- /src/render/macros/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod elements; 3 | 4 | #[macro_use] 5 | mod attributes; 6 | -------------------------------------------------------------------------------- /examples/game_of_life/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aclueless/spair/HEAD/examples/game_of_life/assets/favicon.ico -------------------------------------------------------------------------------- /src/queue_render/base/mod.rs: -------------------------------------------------------------------------------- 1 | mod attribute; 2 | mod element; 3 | mod list; 4 | mod nodes; 5 | mod text; 6 | 7 | pub use attribute::*; 8 | pub use list::*; 9 | -------------------------------------------------------------------------------- /examples/components/README.md: -------------------------------------------------------------------------------- 1 | Using Spair, you only need child components in big apps or when you need to manage the states separately. 2 | 3 | For regular apps, you just need a single component and implement `spair::Render` for your items that need to be rendered. 4 | -------------------------------------------------------------------------------- /crates/examples/benchmark/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "benchmark" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | wasm-bindgen = "0.2" 8 | spair = { path = "../../spairc", package = "spairc" } 9 | wasm-logger = "0.2" 10 | log = "0.4" 11 | -------------------------------------------------------------------------------- /crates/examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ccounter" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | wasm-bindgen = "0.2" 8 | spair = { path = "../../spairc", package = "spairc" } 9 | # wasm-logger = "0.2" 10 | # log = "0.4" 11 | -------------------------------------------------------------------------------- /crates/examples/benchmark-gen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "benchmark-gen" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | wasm-bindgen = "0.2" 8 | spair = { path = "../../spairc", package = "spairc" } 9 | wasm-logger = "0.2" 10 | log = "0.4" 11 | -------------------------------------------------------------------------------- /crates/examples/counter-gen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ccounter-gen" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | wasm-bindgen = "0.2" 8 | spair = { path = "../../spairc", package = "spairc" } 9 | # wasm-logger = "0.2" 10 | # log = "0.4" 11 | -------------------------------------------------------------------------------- /crates/examples/match_expr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "match_expr" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | wasm-bindgen = "0.2" 8 | spair = { path = "../../spairc", package = "spairc" } 9 | # wasm-logger = "0.2" 10 | # log = "0.4" 11 | -------------------------------------------------------------------------------- /crates/spair-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spair-macros" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | syn = "2.0" 11 | quote = "1.0" 12 | proc-macro2 = { version="1.0", features = ["span-locations"]} 13 | -------------------------------------------------------------------------------- /examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/fetch/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/components/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /crates/examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /crates/examples/fetch/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /crates/examples/match_expr/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/counter_queue_render/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /crates/examples/benchmark/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /crates/examples/components/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /crates/examples/counter-gen/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /crates/examples/benchmark-gen/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/boids/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spair • Boids 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spair • TodoMVC 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /crates/examples/todomvc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ctodomvc" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | wasm-bindgen = "0.2" 8 | spair = { path = "../../spairc", package = "spairc" } 9 | serde = { version = "1.0", features = ["derive"] } 10 | gloo-storage = "0.3" 11 | wasm-logger = "0.2" 12 | log = "0.4" 13 | -------------------------------------------------------------------------------- /crates/examples/todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spair • TodoMVC 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/game_of_life/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spair • Game of Life 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Install 18 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 19 | 20 | - name: Run tests 21 | run: ./test.sh 22 | -------------------------------------------------------------------------------- /examples/components/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "components" 3 | version = "0.1.0" 4 | authors = ["aclueless <61309385+aclueless@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [lib] 10 | crate-type = ["cdylib", "rlib"] 11 | 12 | [dependencies] 13 | wasm-bindgen = "0.2" 14 | spair = { path = "../../" } 15 | wasm-logger = "0.2" -------------------------------------------------------------------------------- /crates/examples/components/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ccomponents" 3 | version = "0.1.0" 4 | authors = ["aclueless <61309385+aclueless@users.noreply.github.com>"] 5 | edition = "2024" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | wasm-bindgen = "0.2" 11 | spair = { path = "../../spairc", package = "spairc" } 12 | log = "0.4" 13 | wasm-logger = "0.2" 14 | -------------------------------------------------------------------------------- /examples/svg_clock/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter" 3 | version = "0.1.0" 4 | authors = ["aclueless <61309385+aclueless@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | # [lib] 10 | # crate-type = ["cdylib", "rlib"] 11 | 12 | [dependencies] 13 | wasm-bindgen = "0.2" 14 | spair = { path = "../../" } 15 | # log = "0.4" 16 | # wasm-logger = "0.2" 17 | -------------------------------------------------------------------------------- /examples/todomvc/src/utils.rs: -------------------------------------------------------------------------------- 1 | use gloo_storage::{LocalStorage, Storage}; 2 | use spair::prelude::*; 3 | 4 | const TODO_DATA_KEY: &str = "todos-data-for-spair"; 5 | 6 | pub(crate) fn write_data_to_storage(data: &super::TodoData) { 7 | LocalStorage::set(TODO_DATA_KEY, data).expect_throw("Unable to set item on local storage") 8 | } 9 | 10 | pub(crate) fn read_data_from_storage() -> super::TodoData { 11 | LocalStorage::get(TODO_DATA_KEY).unwrap_or_default() 12 | } 13 | -------------------------------------------------------------------------------- /src/render/base/mod.rs: -------------------------------------------------------------------------------- 1 | mod attributes; 2 | mod element; 3 | mod events; 4 | mod list; 5 | mod nodes; 6 | mod nodes_extensions; 7 | mod text; 8 | 9 | pub use crate::events::MethodsForEvents; 10 | pub use attributes::*; 11 | pub use element::*; 12 | pub use events::*; 13 | pub use list::*; 14 | pub use nodes::*; 15 | pub use nodes_extensions::*; 16 | pub use text::*; 17 | 18 | #[cfg(feature = "keyed-list")] 19 | mod keyed_list; 20 | #[cfg(feature = "keyed-list")] 21 | pub use keyed_list::*; 22 | -------------------------------------------------------------------------------- /examples/svg_clock/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "svg_clock" 3 | version = "0.1.0" 4 | authors = ["aclueless <61309385+aclueless@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [lib] 10 | crate-type = ["cdylib", "rlib"] 11 | 12 | [dependencies] 13 | spair = { path = "../../", features = ["svg", "keyed-list"] } 14 | js-sys = "0.3" 15 | #wasm-logger = "0.2" 16 | gloo-timers = "0.2" 17 | -------------------------------------------------------------------------------- /src/render/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod macros; 3 | 4 | pub mod base; 5 | pub mod html; 6 | #[cfg(feature = "svg")] 7 | pub mod svg; 8 | 9 | pub struct SeeDeprecationNoteOrMethodDocForInformation; 10 | 11 | #[derive(Copy, Clone)] 12 | pub enum ListElementCreation { 13 | Clone, 14 | New, 15 | } 16 | 17 | impl ListElementCreation { 18 | pub fn use_template(&self) -> bool { 19 | match self { 20 | Self::Clone => true, 21 | Self::New => false, 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/game_of_life/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "game_of_life" 3 | version = "0.1.0" 4 | authors = [ 5 | "Diego Cardoso ", 6 | "Ilya Bogdanov " 8 | ] 9 | edition = "2021" 10 | license = "MIT OR Apache-2.0" 11 | 12 | [dependencies] 13 | getrandom = { version = "0.2", features = ["js"] } 14 | log = "0.4" 15 | rand = "0.8" 16 | wasm-logger = "0.2" 17 | spair = { path = "../../", features=["keyed-list"] } 18 | gloo = "0.8" 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | #Added by cargo 14 | # 15 | #already existing elements were commented out 16 | 17 | /target 18 | #Cargo.lock 19 | 20 | **/pkg 21 | **/pkg* 22 | **/dist* -------------------------------------------------------------------------------- /examples/counter_queue_render/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter-queue-render" 3 | version = "0.1.0" 4 | authors = ["aclueless <61309385+aclueless@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [lib] 10 | crate-type = ["cdylib", "rlib"] 11 | 12 | [dependencies] 13 | wasm-bindgen = "0.2" 14 | spair = { path = "../../", features = ["queue-render"] } 15 | wasm-logger = "0.2" 16 | console_error_panic_hook = "0.1" -------------------------------------------------------------------------------- /crates/examples/fetch/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cfetch" 3 | version = "0.1.0" 4 | authors = ["aclueless <61309385+aclueless@users.noreply.github.com>"] 5 | edition = "2024" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | gloo-net = { version = "0.6", default-features = false, features = ["http", "json"] } 11 | serde = { version = "1.0", features = ["derive"] } 12 | spair = { path = "../../spairc", package = "spairc" } 13 | #wasm-bindgen = "0.2" 14 | -------------------------------------------------------------------------------- /examples/fetch/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fetch" 3 | version = "0.1.0" 4 | authors = ["aclueless <61309385+aclueless@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [lib] 10 | crate-type = ["cdylib", "rlib"] 11 | 12 | [dependencies] 13 | gloo-net = { version = "0.2.4", default-features = false, features = ["http", "json"] } 14 | serde = { version = "1.0", features = ["derive"] } 15 | spair = { path = "../../" } 16 | wasm-bindgen = "0.2" 17 | -------------------------------------------------------------------------------- /examples/boids/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "boids" 3 | version = "0.1.0" 4 | authors = ["motoki saito "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | [dependencies] 10 | anyhow = "1.0" 11 | getrandom = { version = "0.2", features = ["js"] } 12 | rand = "0.8" 13 | serde = { version = "1.0", features = ["derive"] } 14 | spair = { path = "../../", features = ["svg"] } 15 | gloo = "0.8" 16 | 17 | [dependencies.web-sys] 18 | version = "0.3" 19 | features = [ 20 | "HtmlInputElement", 21 | ] 22 | -------------------------------------------------------------------------------- /src/queue_render/dom/nodes.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::Cell, rc::Rc}; 2 | 3 | pub struct QrGroupRepresentative { 4 | end_flag_node: web_sys::Node, 5 | unmounted: Rc>, 6 | } 7 | 8 | impl Drop for QrGroupRepresentative { 9 | fn drop(&mut self) { 10 | self.unmounted.set(true); 11 | } 12 | } 13 | 14 | impl QrGroupRepresentative { 15 | pub fn new(end_flag_node: web_sys::Node, unmounted: Rc>) -> Self { 16 | Self { 17 | end_flag_node, 18 | unmounted, 19 | } 20 | } 21 | 22 | pub fn end_flag_node(&self) -> &web_sys::Node { 23 | &self.end_flag_node 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/spairc/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | TARGET="--target=wasm32-unknown-unknown" 5 | 6 | cargo fmt --all -- --check 7 | cargo clippy --all -- -D warnings 8 | 9 | # --chrome and --firefox on separate lines to easily disable one of them if the driver has problems 10 | # wasm-pack test --headless --chrome -- --all-features 11 | #wasm-pack test --headless --firefox -- --all-features 12 | wasm-pack test --headless --firefox 13 | # wasm-pack test --headless --chrome -- --features=svg,queue-render,keyed-list 14 | 15 | for x in ../examples/*; do 16 | if [ -f $x/Cargo.toml ]; then 17 | cargo build $TARGET --manifest-path=$x/Cargo.toml 18 | fi 19 | done 20 | -------------------------------------------------------------------------------- /src/queue_render/dom/list.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::Cell, rc::Rc}; 2 | 3 | pub struct QrListRepresentative { 4 | end_flag_node: Option, 5 | unmounted: Rc>, 6 | } 7 | 8 | impl Drop for QrListRepresentative { 9 | fn drop(&mut self) { 10 | self.unmounted.set(true); 11 | } 12 | } 13 | 14 | impl QrListRepresentative { 15 | pub fn new(end_flag_node: Option, unmounted: Rc>) -> Self { 16 | Self { 17 | end_flag_node, 18 | unmounted, 19 | } 20 | } 21 | 22 | pub fn end_flag_node(&self) -> Option<&web_sys::Node> { 23 | self.end_flag_node.as_ref() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | TARGET="--target=wasm32-unknown-unknown" 5 | 6 | cargo fmt --all -- --check 7 | cargo clippy --all -- -D warnings 8 | 9 | # --chrome and --firefox on separate lines to easily disable one of them if the driver has problems 10 | # wasm-pack test --headless --chrome -- --all-features 11 | #wasm-pack test --headless --firefox -- --all-features 12 | wasm-pack test --headless --firefox -- --features=svg,queue-render,keyed-list 13 | # wasm-pack test --headless --chrome -- --features=svg,queue-render,keyed-list 14 | 15 | for x in ./examples/*; do 16 | if [ -f $x/Cargo.toml ]; then 17 | cargo build $TARGET --manifest-path=$x/Cargo.toml 18 | fi 19 | done 20 | -------------------------------------------------------------------------------- /crates/spairc/src/events.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Function; 2 | use wasm_bindgen::{JsCast, closure::Closure}; 3 | 4 | pub trait EventListener { 5 | fn js_function(&self) -> &Function; 6 | } 7 | 8 | macro_rules! impl_event_listener_trait { 9 | ($($EventArg:ident)+) => { 10 | $( 11 | impl EventListener for Closure { 12 | fn js_function(&self) -> &Function { 13 | self.as_ref().unchecked_ref() 14 | } 15 | } 16 | )+ 17 | }; 18 | } 19 | 20 | impl_event_listener_trait! { 21 | ClipboardEvent 22 | Event 23 | FocusEvent 24 | InputEvent 25 | KeyboardEvent 26 | MouseEvent 27 | PopStateEvent 28 | WheelEvent 29 | } 30 | -------------------------------------------------------------------------------- /examples/todomvc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todomvc" 3 | version = "0.1.0" 4 | authors = ["aclueless <61309385+aclueless@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | wasm-bindgen = "0.2" 11 | spair = { path = "../../", features = ["keyed-list"] } 12 | serde = { version = "1.0", features = ["derive"] } 13 | serde_json = "1.0" 14 | gloo-storage = "0.2" 15 | 16 | [dependencies.web-sys] 17 | version = "0.3.36" 18 | features = [ 19 | "Node", 20 | "Event", 21 | "KeyboardEvent", 22 | "FocusEvent", 23 | "HtmlInputElement", 24 | "FocusEvent", 25 | "KeyboardEvent", 26 | "Storage", 27 | ] 28 | -------------------------------------------------------------------------------- /examples/game_of_life/README.md: -------------------------------------------------------------------------------- 1 | # Game of Life Example 2 | 3 | This implementation is a port of [Yew's implementation](https://github.com/yewstack/yew/tree/8172b9ceacdcd7d4609e8ba00f758507a8bbc85d/examples/game_of_life). 4 | 5 | This example boasts a complete implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway's_Game_of_Life). 6 | You can manually toggle cells by clicking on them or create a random layout by pressing the "Random" button. 7 | 8 | ## Running 9 | 10 | This example is quite resource intensive; it's recommended that you only use it with the `--release` flag: 11 | 12 | ```bash 13 | trunk serve --release 14 | ``` 15 | 16 | ## Concepts 17 | 18 | - Uses [`gloo_timer`](https://docs.rs/gloo-timers/latest/gloo_timers/) to automatically step the simulation. 19 | - Logs to the console using the [`weblog`](https://crates.io/crates/weblog) crate. 20 | -------------------------------------------------------------------------------- /src/render/base/events.rs: -------------------------------------------------------------------------------- 1 | use super::MethodsForEvents; 2 | 3 | impl<'updater, C: crate::component::Component, T> StateHelperMethods<'updater, C> for T where 4 | T: MethodsForEvents<'updater, C> 5 | { 6 | } 7 | 8 | pub trait StateHelperMethods<'updater, C: crate::component::Component>: 9 | MethodsForEvents<'updater, C> 10 | { 11 | fn on_input_value( 12 | self, 13 | comp: &crate::Comp, 14 | updater: impl Fn(&mut C, String) + 'static, 15 | ) -> Self { 16 | self.on_input( 17 | comp.handler_arg_mut(move |state, event: crate::events::InputEvent| { 18 | if let Some(value) = event 19 | .current_target() 20 | .into_input_element() 21 | .map(|i| i.0.value()) 22 | { 23 | updater(state, value); 24 | } 25 | }), 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/spairc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spairc" 3 | version = "0.0.1" 4 | edition = "2024" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | wasm-bindgen = "0.2" 10 | js-sys = "0.3" 11 | log="0.4" 12 | rustc-hash = "2" 13 | wasm-bindgen-futures = "0.4" 14 | 15 | spair-macros = { path = "../spair-macros" } 16 | 17 | [dependencies.web-sys] 18 | version = "0.3" 19 | features = [ 20 | "Window", 21 | "History", 22 | "Document", 23 | "Element", 24 | "DocumentFragment", 25 | "HtmlTemplateElement", 26 | "HtmlInputElement", 27 | "HtmlTextAreaElement", 28 | "HtmlSelectElement", 29 | "HtmlOptionElement", 30 | "HtmlAnchorElement", 31 | "HtmlAreaElement", 32 | "Event", 33 | "MouseEvent", 34 | "InputEvent", 35 | "FocusEvent", 36 | "KeyboardEvent", 37 | "WheelEvent", 38 | "EventTarget", 39 | "Location", 40 | "PopStateEvent", 41 | "ClipboardEvent", 42 | "DataTransfer", 43 | "DomTokenList", 44 | "Text", 45 | ] 46 | 47 | [dev-dependencies] 48 | wasm-bindgen-test = "0.3" 49 | -------------------------------------------------------------------------------- /src/element.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsCast; 2 | 3 | pub struct EventTarget(pub(crate) Option); 4 | // pub struct InputElement(pub(crate) web_sys::HtmlInputElement); 5 | // pub struct SelectElement(pub(crate) web_sys::HtmlSelectElement); 6 | // pub struct FormElement(pub(crate) web_sys::HtmlFormElement); 7 | 8 | duplicate::duplicate! { 9 | [ 10 | TypeName into_name; 11 | [HtmlInputElement] [into_input_element]; 12 | [HtmlSelectElement] [into_select_element]; 13 | [HtmlFormElement] [into_form_element]; 14 | [HtmlTextAreaElement] [into_text_area_element]; 15 | ] 16 | pub struct TypeName(pub(crate) web_sys::TypeName); 17 | impl TypeName { 18 | pub fn into_inner(self) -> web_sys::TypeName { 19 | self.0 20 | } 21 | } 22 | impl EventTarget { 23 | pub fn into_name(self) -> Option { 24 | self.into_ws_element().map(TypeName) 25 | } 26 | } 27 | } 28 | 29 | impl EventTarget { 30 | pub fn into_ws_element(self) -> Option { 31 | self.0.and_then(|v| v.dyn_into().ok()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/queue_render/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::VecDeque}; 2 | 3 | pub mod base; 4 | pub mod dom; 5 | pub mod html; 6 | #[cfg(feature = "svg")] 7 | pub mod svg; 8 | pub mod val; 9 | pub mod vec; 10 | 11 | type FnMap = Box U>; 12 | type FnMapC = Box U>; 13 | 14 | struct RenderQueue { 15 | queue: RefCell>>, 16 | } 17 | 18 | thread_local! { 19 | static RENDER_QUEUE: RenderQueue = RenderQueue { 20 | queue: RefCell::new(VecDeque::new()) 21 | }; 22 | } 23 | 24 | fn queue_render(fn_render: impl FnOnce() + 'static) { 25 | RENDER_QUEUE.with(|rq| rq.add(Box::new(fn_render))); 26 | } 27 | 28 | impl RenderQueue { 29 | fn add(&self, f: Box) { 30 | self.queue.borrow_mut().push_back(f); 31 | } 32 | 33 | fn take(&self) -> Option> { 34 | self.queue.borrow_mut().pop_front() 35 | } 36 | 37 | fn execute(&self) { 38 | while let Some(f) = self.take() { 39 | f(); 40 | } 41 | } 42 | } 43 | 44 | pub fn execute_render_queue() { 45 | RENDER_QUEUE.with(|uq| uq.execute()); 46 | } 47 | -------------------------------------------------------------------------------- /crates/examples/counter/src/main.rs: -------------------------------------------------------------------------------- 1 | use spair::prelude::*; 2 | use spair::{CallbackArg, web_sys::MouseEvent}; 3 | 4 | struct AppState { 5 | value: i32, 6 | } 7 | 8 | #[new_view] 9 | impl UpdownButton { 10 | fn create(handler: CallbackArg, text: &str) {} 11 | fn update() {} 12 | fn view() { 13 | button(on_click = handler, text(text)) 14 | } 15 | } 16 | 17 | impl AppState { 18 | fn increase(&mut self) { 19 | self.value += 1; 20 | } 21 | 22 | fn decrease(&mut self) { 23 | self.value -= 1; 24 | } 25 | } 26 | #[component_for] 27 | impl AppState { 28 | fn create(ccontext: &Context) {} 29 | fn update(ucontext: &Context) {} 30 | fn view() { 31 | div( 32 | replace_at_element_id = "root", 33 | v.UpdownButton(ccontext.comp.callback_arg(|state, _| state.decrease()), "-"), 34 | text(ucontext.state.value), 35 | v.UpdownButton(ccontext.comp.callback_arg(|state, _| state.increase()), "+"), 36 | ) 37 | } 38 | } 39 | 40 | fn main() { 41 | // wasm_logger::init(wasm_logger::Config::default()); 42 | spair::start_app(|_| AppState { value: 42 }); 43 | } 44 | -------------------------------------------------------------------------------- /examples/game_of_life/src/cell.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, PartialEq, Eq)] 2 | pub enum State { 3 | Alive, 4 | Dead, 5 | } 6 | 7 | #[derive(Clone, Copy)] 8 | pub struct Cellule { 9 | pub state: State, 10 | } 11 | 12 | impl Cellule { 13 | pub fn new_dead() -> Self { 14 | Self { state: State::Dead } 15 | } 16 | 17 | pub fn set_alive(&mut self) { 18 | self.state = State::Alive; 19 | } 20 | 21 | pub fn set_dead(&mut self) { 22 | self.state = State::Dead; 23 | } 24 | 25 | pub fn is_alive(self) -> bool { 26 | self.state == State::Alive 27 | } 28 | 29 | pub fn toggle(&mut self) { 30 | if self.is_alive() { 31 | self.set_dead() 32 | } else { 33 | self.set_alive() 34 | } 35 | } 36 | 37 | pub fn count_alive_neighbors(neighbors: &[Self]) -> usize { 38 | neighbors.iter().filter(|n| n.is_alive()).count() 39 | } 40 | 41 | pub fn alone(neighbors: &[Self]) -> bool { 42 | Self::count_alive_neighbors(neighbors) < 2 43 | } 44 | 45 | pub fn overpopulated(neighbors: &[Self]) -> bool { 46 | Self::count_alive_neighbors(neighbors) > 3 47 | } 48 | 49 | pub fn can_be_revived(neighbors: &[Self]) -> bool { 50 | Self::count_alive_neighbors(neighbors) == 3 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/queue_render/html/element.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | component::Component, 3 | dom::WsElement, 4 | queue_render::val::{QrVal, QrValMap, QrValMapWithState}, 5 | render::{base::ElementUpdaterMut, html::HtmlElementUpdater}, 6 | }; 7 | 8 | // These methods don't have to be implemented on HtmlElementUpdater because 9 | // they are for queue-render. But their equivalent methods (for incremental 10 | // render) need to be on HtmlElementUpdater, so these methods need to be on 11 | // HtmlElementUpdater, too. 12 | impl<'a, C: Component> HtmlElementUpdater<'a, C> { 13 | pub fn qr_property( 14 | &self, 15 | fn_update: impl Fn(&WsElement, &T) + 'static, 16 | value: &QrVal, 17 | ) { 18 | self.element_updater().qr_property(fn_update, value) 19 | } 20 | 21 | pub fn qrm_property( 22 | &self, 23 | fn_update: impl Fn(&WsElement, &U) + 'static, 24 | value: QrValMap, 25 | ) { 26 | self.element_updater().qrm_property(fn_update, value) 27 | } 28 | 29 | pub fn qrmws_property( 30 | &self, 31 | fn_update: impl Fn(&WsElement, &U) + 'static, 32 | value: QrValMapWithState, 33 | ) { 34 | self.element_updater().qrmws_property(fn_update, value) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/render/svg/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::dom::{ElementTag, ElementTagExt}; 2 | 3 | mod attributes; 4 | mod attributes_elements_with_ambiguous_names; 5 | mod element; 6 | #[cfg(feature = "keyed-list")] 7 | mod keyed_list; 8 | mod list; 9 | mod nodes; 10 | mod partial_list; 11 | 12 | pub use attributes::*; 13 | pub use attributes_elements_with_ambiguous_names::*; 14 | pub use element::*; 15 | #[cfg(feature = "keyed-list")] 16 | pub use keyed_list::*; 17 | pub use list::*; 18 | pub use nodes::*; 19 | pub use partial_list::*; 20 | 21 | #[derive(Copy, Clone)] 22 | pub struct SvgTag(pub &'static str); 23 | 24 | impl From<&'static str> for SvgTag { 25 | fn from(value: &'static str) -> Self { 26 | Self(value) 27 | } 28 | } 29 | 30 | impl ElementTag for SvgTag { 31 | const NAMESPACE: &'static str = "http://www.w3.org/2000/svg"; 32 | fn tag_name(&self) -> &str { 33 | self.0 34 | } 35 | } 36 | 37 | impl<'a, C: crate::Component> ElementTagExt<'a, C> for SvgTag { 38 | type Updater = SvgElementUpdater<'a, C>; 39 | fn make_updater(e: super::base::ElementUpdater<'a, C>) -> Self::Updater { 40 | e.into() 41 | } 42 | } 43 | 44 | // This is a struct to make sure that a name that appears in both 45 | // SVG element names and SVG attribute names causes a conflict 46 | // and fail to compile (during test). 47 | #[cfg(test)] 48 | pub struct TestSvgMethods; 49 | -------------------------------------------------------------------------------- /examples/boids/README.md: -------------------------------------------------------------------------------- 1 | # Boids Example 2 | 3 | This implementation is a port of [Yew's implementation](https://github.com/yewstack/yew/tree/8172b9ceacdcd7d4609e8ba00f758507a8bbc85d/examples/boids). 4 | 5 | A version of [Boids](https://en.wikipedia.org/wiki/Boids) implemented in Yew. 6 | 7 | This example doesn't make use of a [Canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API), 8 | instead, each boid has its own element demonstrating the performance of Yew's virtual DOM. 9 | 10 | ## Running 11 | 12 | You should run this example with the `--release` flag: 13 | 14 | ```bash 15 | trunk serve --release 16 | ``` 17 | 18 | ## Concepts 19 | 20 | The example uses [`gloo::timers`](https://docs.rs/gloo-timers/latest/gloo_timers/) implementation of `setInterval` to drive the Yew game loop. 21 | 22 | ## Improvements 23 | 24 | - Add the possibility to switch the behaviour from flocking to scattering by inverting the cohesion rule so that boids avoid each other. 25 | This should also invert the color adaption to restore some variety. 26 | - Add keyboard shortcuts for the actions. 27 | - Make it possible to hide the settings panel entirely 28 | - Bigger boids should accelerate slower than smaller ones 29 | - Share settings by encoding them into the URL 30 | - Resize the boids when "Spacing" is changed. 31 | The setting should then also be renamed to something like "Size". 32 | -------------------------------------------------------------------------------- /crates/examples/components/src/child.rs: -------------------------------------------------------------------------------- 1 | use spair::prelude::*; 2 | 3 | pub struct Child { 4 | value: i32, 5 | callback_arg: CallbackArg, 6 | } 7 | 8 | impl Child { 9 | pub fn new(callback_arg: CallbackArg) -> Self { 10 | Self { 11 | value: 42, 12 | callback_arg, 13 | } 14 | } 15 | 16 | pub fn set_value(&mut self, value: i32) { 17 | self.value = value; 18 | } 19 | 20 | fn increment(&mut self) { 21 | self.value += 1; 22 | self.call_to_parent() 23 | } 24 | 25 | fn decrement(&mut self) { 26 | self.value -= 1; 27 | self.call_to_parent() 28 | } 29 | 30 | fn call_to_parent(&self) { 31 | if self.value % 5 == 0 { 32 | self.callback_arg.call(self.value); 33 | } 34 | } 35 | } 36 | 37 | #[component_for] 38 | impl Child { 39 | fn create(cc: &Context) {} 40 | fn update(uc: &Context) {} 41 | fn view() { 42 | div( 43 | text("In child component: "), 44 | button( 45 | on_click = cc.comp.callback_arg(|state, _| state.decrement()), 46 | text("-"), 47 | ), 48 | text(uc.state.value), 49 | button( 50 | on_click = cc.comp.callback_arg(|state, _| state.increment()), 51 | text("+"), 52 | ), 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/spairc/src/element/values.rs: -------------------------------------------------------------------------------- 1 | pub enum Value { 2 | None, 3 | Default, 4 | Bool(bool), 5 | Char(char), 6 | Isize(isize), 7 | Usize(usize), 8 | I8(i8), 9 | U8(u8), 10 | I16(i16), 11 | U16(u16), 12 | I32(i32), 13 | U32(u32), 14 | I64(i64), 15 | U64(u64), 16 | I128(i128), 17 | U128(u128), 18 | F32(f32), 19 | F64(f64), 20 | String(String), 21 | } 22 | 23 | pub trait ValueChanged: Copy { 24 | fn check_value_changed(self, value: &mut Value) -> bool; 25 | } 26 | 27 | macro_rules! impl_value_changed { 28 | ($($Variant:ident $Type:ty)+) => { 29 | $( 30 | impl ValueChanged for $Type { 31 | fn check_value_changed(self, value: &mut Value) -> bool { 32 | if let Value::$Variant(old_value) = value { 33 | if *old_value != self { 34 | *old_value = self; 35 | return true; 36 | } 37 | } 38 | else{ 39 | *value = Value::$Variant(self); 40 | return true; 41 | } 42 | false 43 | } 44 | } 45 | )+ 46 | }; 47 | } 48 | 49 | impl_value_changed! { 50 | Bool bool 51 | Char char 52 | Isize isize 53 | Usize usize 54 | I8 i8 55 | U8 u8 56 | I16 i16 57 | U16 u16 58 | I32 i32 59 | U32 u32 60 | I64 i64 61 | U64 u64 62 | I128 i128 63 | U128 u128 64 | F32 f32 65 | F64 f64 66 | } 67 | -------------------------------------------------------------------------------- /examples/counter/src/main.rs: -------------------------------------------------------------------------------- 1 | use spair::prelude::*; 2 | 3 | struct State { 4 | value: i32, 5 | } 6 | 7 | impl State { 8 | fn increment(&mut self) { 9 | self.value += 1; 10 | } 11 | 12 | fn decrement(&mut self) { 13 | self.value -= 1; 14 | } 15 | } 16 | 17 | impl spair::Component for State { 18 | type Routes = (); 19 | fn render(&self, element: spair::Element) { 20 | let comp = element.comp(); 21 | element 22 | .static_nodes() 23 | .p(|p| { 24 | p.static_nodes() 25 | .static_text("The initial value is ") 26 | .static_text(self.value); 27 | }) 28 | .update_nodes() 29 | .rfn(|nodes| render_button("-", comp.handler_mut(State::decrement), nodes)) 30 | .update_text(self.value) 31 | .rfn(|nodes| render_button("+", comp.handler_mut(State::increment), nodes)); 32 | } 33 | } 34 | 35 | fn render_button(label: &str, handler: H, nodes: spair::Nodes) { 36 | nodes.static_nodes().button(|b| { 37 | b.static_attributes() 38 | .on_click(handler) 39 | .static_nodes() 40 | .static_text(label); 41 | }); 42 | } 43 | 44 | impl spair::Application for State { 45 | fn init(_: &spair::Comp) -> Self { 46 | Self { value: 42 } 47 | } 48 | } 49 | 50 | pub fn main() { 51 | // wasm_logger::init(wasm_logger::Config::default()); 52 | State::mount_to_element_id("root"); 53 | } 54 | -------------------------------------------------------------------------------- /crates/spairc/src/helper.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::{JsCast, UnwrapThrowExt}; 2 | use web_sys::{ 3 | Document, Element, Event, EventTarget, HtmlElement, HtmlInputElement, HtmlSelectElement, 4 | InputEvent, Window, 5 | }; 6 | 7 | thread_local!( 8 | pub static WINDOW: Window = web_sys::window().expect_throw("No window found"); 9 | pub static DOCUMENT: Document = 10 | WINDOW.with(|window| window.document().expect_throw("No document found")); 11 | ); 12 | 13 | #[allow(dead_code)] 14 | pub fn get_body() -> HtmlElement { 15 | DOCUMENT.with(|d| d.body()).expect_throw("No body") 16 | } 17 | 18 | pub fn get_element_by_id(element_id: &str) -> Option { 19 | DOCUMENT.with(|document| document.get_element_by_id(element_id)) 20 | } 21 | 22 | pub trait ElementFromCurrentEventTarget { 23 | fn get_current_target(&self) -> EventTarget; 24 | fn current_target_as_select(&self) -> HtmlSelectElement { 25 | self.get_current_target().unchecked_into() 26 | } 27 | } 28 | 29 | impl ElementFromCurrentEventTarget for Event { 30 | fn get_current_target(&self) -> EventTarget { 31 | self.current_target().unwrap_throw() 32 | } 33 | } 34 | 35 | pub trait InputElementFromCurrentInputEvent { 36 | fn get_current_target(&self) -> EventTarget; 37 | fn current_target_as_input(&self) -> HtmlInputElement { 38 | self.get_current_target().unchecked_into() 39 | } 40 | } 41 | 42 | impl InputElementFromCurrentInputEvent for InputEvent { 43 | fn get_current_target(&self) -> EventTarget { 44 | self.current_target().unwrap_throw() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::UnwrapThrowExt; 2 | 3 | pub fn window() -> web_sys::Window { 4 | web_sys::window().expect_throw("Unable to get window") 5 | } 6 | 7 | pub fn document() -> web_sys::Document { 8 | window().document().expect_throw("Unable to get document") 9 | } 10 | 11 | pub fn create_comment_node(content: &str) -> web_sys::Node { 12 | document().create_comment(content).into() 13 | } 14 | 15 | pub fn alert(message: &str) { 16 | window() 17 | .alert_with_message(message) 18 | .expect_throw("Error on displaying alert dialog"); 19 | } 20 | 21 | pub fn confirm(message: &str) -> bool { 22 | window() 23 | .confirm_with_message(message) 24 | .expect_throw("Error on displaying confirm dialog") 25 | } 26 | 27 | pub fn prompt(message: &str, default_value: Option<&str>) -> Option { 28 | match default_value { 29 | Some(default_value) => window() 30 | .prompt_with_message_and_default(message, default_value) 31 | .expect_throw("Error on getting user input with default value from the prompt dialog"), 32 | None => window() 33 | .prompt_with_message(message) 34 | .expect_throw("Error on getting user input from the prompt dialog"), 35 | } 36 | } 37 | 38 | pub(crate) fn register_event_listener_on_window(event: &str, listener: &js_sys::Function) { 39 | let window = crate::utils::window(); 40 | let window: &web_sys::EventTarget = window.as_ref(); 41 | window 42 | .add_event_listener_with_callback(event, listener) 43 | .expect_throw("Unable to register event listener on window"); 44 | } 45 | -------------------------------------------------------------------------------- /src/render/svg/element.rs: -------------------------------------------------------------------------------- 1 | use super::{SvgAttributesOnly, SvgStaticAttributes, SvgStaticAttributesOnly}; 2 | use crate::{ 3 | component::{Comp, Component}, 4 | dom::WsElement, 5 | render::base::{ElementUpdater, ElementUpdaterMut}, 6 | }; 7 | 8 | pub struct SvgElementUpdater<'updater, C: Component>(ElementUpdater<'updater, C>); 9 | 10 | impl<'updater, C: Component> From> for SvgElementUpdater<'updater, C> { 11 | fn from(element_updater: ElementUpdater<'updater, C>) -> Self { 12 | Self(element_updater) 13 | } 14 | } 15 | 16 | impl<'updater, C: Component> ElementUpdaterMut<'updater, C> for SvgElementUpdater<'updater, C> { 17 | fn element_updater(&self) -> &ElementUpdater { 18 | &self.0 19 | } 20 | fn element_updater_mut(&mut self) -> &mut ElementUpdater<'updater, C> { 21 | &mut self.0 22 | } 23 | } 24 | 25 | impl<'updater, C: Component> SvgElementUpdater<'updater, C> { 26 | pub(super) fn into_inner(self) -> ElementUpdater<'updater, C> { 27 | self.0 28 | } 29 | 30 | pub fn state(&self) -> &'updater C { 31 | self.0.state() 32 | } 33 | 34 | pub fn comp(&self) -> Comp { 35 | self.0.comp() 36 | } 37 | 38 | pub fn attributes_only(self) -> SvgAttributesOnly<'updater, C> { 39 | SvgAttributesOnly::new(self.0) 40 | } 41 | 42 | pub fn static_attributes_only(self) -> SvgStaticAttributesOnly<'updater, C> { 43 | SvgStaticAttributesOnly::new(self.0) 44 | } 45 | 46 | pub fn static_attributes(self) -> SvgStaticAttributes<'updater, C> { 47 | SvgStaticAttributes::new(self.0) 48 | } 49 | 50 | pub fn ws_element(&self) -> &WsElement { 51 | self.0.element().ws_element() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/render/html/mod.rs: -------------------------------------------------------------------------------- 1 | // This module has traits that provide methods for HTML elements and HTML 2 | // attributes. But the trait's names are too long, so their names are 3 | // shorten with prefixes as `Hems` and `Hams`. 4 | // `Hems` is short for `HTML element methods` 5 | // `Hams` is short for `HTML attribute methods` 6 | 7 | use crate::dom::{ElementTag, ElementTagExt}; 8 | 9 | mod attributes; 10 | mod attributes_elements_with_ambiguous_names; 11 | mod attributes_with_predefined_values; 12 | mod element; 13 | mod list; 14 | mod nodes; 15 | mod partial_list; 16 | 17 | pub use attributes::*; 18 | pub use attributes_elements_with_ambiguous_names::*; 19 | pub use attributes_with_predefined_values::*; 20 | pub use element::*; 21 | pub use list::*; 22 | pub use nodes::*; 23 | pub use partial_list::*; 24 | 25 | #[cfg(feature = "keyed-list")] 26 | mod keyed_list; 27 | #[cfg(feature = "keyed-list")] 28 | pub use keyed_list::*; 29 | 30 | #[derive(Copy, Clone)] 31 | pub struct HtmlTag(pub &'static str); 32 | 33 | impl From<&'static str> for HtmlTag { 34 | fn from(value: &'static str) -> Self { 35 | Self(value) 36 | } 37 | } 38 | 39 | impl ElementTag for HtmlTag { 40 | const NAMESPACE: &'static str = "http://www.w3.org/1999/xhtml"; 41 | fn tag_name(&self) -> &str { 42 | self.0 43 | } 44 | } 45 | 46 | impl<'a, C: crate::Component> ElementTagExt<'a, C> for HtmlTag { 47 | type Updater = HtmlElementUpdater<'a, C>; 48 | fn make_updater(e: super::base::ElementUpdater<'a, C>) -> Self::Updater { 49 | e.into() 50 | } 51 | } 52 | 53 | // This is a struct to make sure that a name that appears in both 54 | // HTML element names and HTML attribute names causes a conflict 55 | // and fail to compile (during test). 56 | #[cfg(test)] 57 | pub struct TestHtmlMethods; 58 | -------------------------------------------------------------------------------- /crates/spairc/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 3 | 4 | mod component; 5 | mod element; 6 | mod events; 7 | mod helper; 8 | mod keyed_list; 9 | mod list; 10 | mod routing; 11 | 12 | #[cfg(test)] 13 | mod test_helper; 14 | 15 | use std::{cell::RefCell, ops::Deref}; 16 | 17 | pub use wasm_bindgen; 18 | use wasm_bindgen::JsCast; 19 | pub use web_sys; 20 | 21 | pub use crate::helper::WINDOW; 22 | pub use component::{ 23 | Callback, CallbackArg, Comp, Component, Context, start_app, start_app_with_routing, 24 | }; 25 | pub use element::{Element, TemplateElement, Text, WsElement, WsNode, WsText}; 26 | pub use keyed_list::KeyedList; 27 | pub use list::List; 28 | pub use routing::Route; 29 | 30 | pub mod prelude { 31 | pub use crate::component::{ 32 | Callback, CallbackArg, Comp, Context, RcComp, ShouldRender, SpairSpawnLocal, 33 | SpairSpawnLocalWithCallback, 34 | }; 35 | pub use crate::element::RenderOptionWithDefault; 36 | pub use crate::helper::ElementFromCurrentEventTarget; 37 | pub use spair_macros::*; 38 | } 39 | 40 | pub struct WsRef(RefCell>); 41 | impl Default for WsRef { 42 | fn default() -> Self { 43 | Self::none() 44 | } 45 | } 46 | 47 | impl WsRef { 48 | pub fn none() -> Self { 49 | Self(std::cell::RefCell::new(None)) 50 | } 51 | 52 | pub fn get(&self) -> std::cell::Ref> { 53 | self.0.borrow() 54 | } 55 | 56 | pub fn set(&self, element: &WsElement) { 57 | let e = element.deref().clone().unchecked_into::(); 58 | *self.0.borrow_mut() = Some(e); 59 | } 60 | 61 | pub fn execute(&self, f: impl FnOnce(&T) -> O) -> Option { 62 | self.get().as_ref().map(f) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/render/svg/partial_list.rs: -------------------------------------------------------------------------------- 1 | use super::{SvgNodes, SvgNodesOwned, SvgStaticNodes, SvgStaticNodesOwned}; 2 | use crate::{ 3 | component::Component, 4 | render::{ 5 | base::{NodesUpdater, NodesUpdaterMut}, 6 | ListElementCreation, 7 | }, 8 | }; 9 | 10 | pub trait SemsForPartialList<'a, C: Component>: Sized + NodesUpdaterMut<'a, C> { 11 | fn list(mut self, items: II, render: R) -> Self 12 | where 13 | II: Iterator, 14 | R: Fn(I, SvgNodes), 15 | { 16 | render_list(&mut self, ListElementCreation::New, items, render); 17 | self 18 | } 19 | 20 | fn list_clone(mut self, items: II, render: R) -> Self 21 | where 22 | II: Iterator, 23 | R: Fn(I, SvgNodes), 24 | { 25 | render_list(&mut self, ListElementCreation::Clone, items, render); 26 | self 27 | } 28 | } 29 | 30 | fn render_list<'a, C, T, I, II, R>(updater: &mut T, mode: ListElementCreation, items: II, render: R) 31 | where 32 | II: Iterator, 33 | R: Fn(I, SvgNodes), 34 | C: Component, 35 | T: NodesUpdaterMut<'a, C>, 36 | { 37 | let (comp, state, mut r) = updater 38 | .nodes_updater_mut() 39 | .get_list_updater(mode.use_template()); 40 | let _do_we_have_to_care_about_this_returned_value_ = 41 | r.render(comp, state, items, |item: I, mut nodes: NodesUpdater| { 42 | render(item, SvgNodes::new(&mut nodes)) 43 | }); 44 | } 45 | 46 | impl<'a, C: Component> SemsForPartialList<'a, C> for SvgNodesOwned<'a, C> {} 47 | impl<'a, C: Component> SemsForPartialList<'a, C> for SvgStaticNodesOwned<'a, C> {} 48 | impl<'h, 'n: 'h, C: Component> SemsForPartialList<'n, C> for SvgNodes<'h, 'n, C> {} 49 | impl<'h, 'n: 'h, C: Component> SemsForPartialList<'n, C> for SvgStaticNodes<'h, 'n, C> {} 50 | -------------------------------------------------------------------------------- /src/render/html/partial_list.rs: -------------------------------------------------------------------------------- 1 | use super::{Nodes, NodesOwned, StaticNodes, StaticNodesOwned}; 2 | use crate::{ 3 | component::Component, 4 | render::{ 5 | base::{NodesUpdater, NodesUpdaterMut}, 6 | html::HtmlNodesUpdater, 7 | ListElementCreation, 8 | }, 9 | }; 10 | 11 | pub trait HemsForPartialList<'a, C: Component>: Sized + NodesUpdaterMut<'a, C> { 12 | fn list(mut self, items: II, render: R) -> Self 13 | where 14 | II: Iterator, 15 | R: Fn(I, crate::Nodes), 16 | { 17 | render_list(&mut self, ListElementCreation::New, items, render); 18 | self 19 | } 20 | 21 | fn list_clone(mut self, items: II, render: R) -> Self 22 | where 23 | II: Iterator, 24 | R: Fn(I, crate::Nodes), 25 | { 26 | render_list(&mut self, ListElementCreation::Clone, items, render); 27 | self 28 | } 29 | } 30 | 31 | fn render_list<'a, C, T, I, II, R>(updater: &mut T, mode: ListElementCreation, items: II, render: R) 32 | where 33 | II: Iterator, 34 | R: Fn(I, crate::Nodes), 35 | C: Component, 36 | T: NodesUpdaterMut<'a, C>, 37 | { 38 | let (comp, state, mut list_updater) = updater 39 | .nodes_updater_mut() 40 | .get_list_updater(mode.use_template()); 41 | let _do_we_have_to_care_about_this_returned_value_ = 42 | list_updater.render(comp, state, items, |item: I, nodes: NodesUpdater| { 43 | let mut nodes = HtmlNodesUpdater::new(nodes); 44 | render(item, crate::Nodes::new(&mut nodes)) 45 | }); 46 | } 47 | 48 | impl<'a, C: Component> HemsForPartialList<'a, C> for NodesOwned<'a, C> {} 49 | impl<'a, C: Component> HemsForPartialList<'a, C> for StaticNodesOwned<'a, C> {} 50 | impl<'h, 'n: 'h, C: Component> HemsForPartialList<'n, C> for Nodes<'h, 'n, C> {} 51 | impl<'h, 'n: 'h, C: Component> HemsForPartialList<'n, C> for StaticNodes<'h, 'n, C> {} 52 | -------------------------------------------------------------------------------- /src/queue_render/dom/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::dom::{AChildNode, Element, MaybeAChildNode}; 2 | use wasm_bindgen::UnwrapThrowExt; 3 | 4 | mod list; 5 | mod nodes; 6 | mod text; 7 | 8 | pub use list::*; 9 | pub use nodes::*; 10 | pub use text::*; 11 | 12 | pub enum QrNode { 13 | ClonedWsNode(Option), 14 | Text(QrTextNode), 15 | List(QrListRepresentative), 16 | Group(QrGroupRepresentative), 17 | } 18 | 19 | impl MaybeAChildNode for QrNode { 20 | fn ws_node(&self) -> Option<&web_sys::Node> { 21 | match self { 22 | Self::ClonedWsNode(ws) => ws.as_ref(), 23 | Self::Text(tn) => Some(tn.ws_node()), 24 | Self::List(r) => r.end_flag_node(), 25 | Self::Group(g) => Some(g.end_flag_node()), 26 | } 27 | } 28 | } 29 | 30 | impl QrNode { 31 | pub fn get_first_element(&self) -> Option<&Element> { 32 | match self { 33 | Self::ClonedWsNode(_) => None, 34 | Self::Text(_) => None, 35 | Self::List(_) => None, 36 | Self::Group(_) => None, 37 | } 38 | } 39 | 40 | pub fn get_last_element(&self) -> Option<&Element> { 41 | match self { 42 | Self::ClonedWsNode(_) => None, 43 | Self::Text(_) => None, 44 | Self::List(_) => None, 45 | Self::Group(_) => None, 46 | } 47 | } 48 | } 49 | 50 | impl Clone for QrNode { 51 | fn clone(&self) -> Self { 52 | match self { 53 | Self::ClonedWsNode(wsn) => Self::ClonedWsNode(wsn.as_ref().map(|wsn| { 54 | wsn.clone_node_with_deep(false) 55 | .expect_throw("dom::queue_render::text::Clone for QrNode::clone") 56 | })), 57 | Self::Text(tn) => Self::ClonedWsNode(Some(tn.clone_ws_node())), 58 | Self::List(l) => Self::ClonedWsNode(l.end_flag_node().cloned()), 59 | Self::Group(l) => Self::ClonedWsNode(Some(l.end_flag_node().clone())), 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/render/svg/attributes_elements_with_ambiguous_names.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | SvgAttributesOnly, SvgElementUpdater, SvgNodes, SvgNodesOwned, SvgStaticAttributes, 3 | SvgStaticAttributesOnly, SvgStaticNodes, SvgStaticNodesOwned, UpdateSvgElement, 4 | }; 5 | use crate::{ 6 | component::Component, 7 | render::base::{NodesUpdaterMut, StringAttributeValue}, 8 | }; 9 | 10 | #[cfg(test)] 11 | use crate::render::svg::TestSvgMethods; 12 | 13 | make_trait_for_same_name_attribute_and_element_methods! { 14 | TestStructs: (TestSvgMethods) 15 | DeprecatedTraitName: SemsSamsAmbiguous 16 | for_elements { 17 | TraitName: SemsForAmbiguousNames 18 | UpdateElementTraitName: UpdateSvgElement 19 | ElementUpdaterType: SvgElementUpdater 20 | } 21 | for_attributes { 22 | TraitName: SamsForAmbiguousNames 23 | } 24 | ambiguous_attributes: 25 | // The names are also used to make methods for SVG elements 26 | // type name 27 | str _clip_path :a:"clip-path" :e:"clipPath" 28 | str _mask 29 | str _path 30 | } 31 | 32 | impl<'updater, C: Component> SemsSamsAmbiguous for SvgElementUpdater<'updater, C> {} 33 | impl<'updater, C: Component> SemsSamsAmbiguous for SvgStaticAttributes<'updater, C> {} 34 | 35 | impl<'updater, C: Component> SamsForAmbiguousNames<'updater, C> for SvgAttributesOnly<'updater, C> {} 36 | impl<'updater, C: Component> SamsForAmbiguousNames<'updater, C> 37 | for SvgStaticAttributesOnly<'updater, C> 38 | { 39 | } 40 | 41 | impl<'n, C: Component> SemsForAmbiguousNames<'n, C> for SvgStaticNodesOwned<'n, C> { 42 | type Output = Self; 43 | } 44 | impl<'n, C: Component> SemsForAmbiguousNames<'n, C> for SvgNodesOwned<'n, C> { 45 | type Output = Self; 46 | } 47 | impl<'h, 'n: 'h, C: Component> SemsForAmbiguousNames<'n, C> for SvgStaticNodes<'h, 'n, C> { 48 | type Output = Self; 49 | } 50 | impl<'h, 'n: 'h, C: Component> SemsForAmbiguousNames<'n, C> for SvgNodes<'h, 'n, C> { 51 | type Output = Self; 52 | } 53 | -------------------------------------------------------------------------------- /src/render/html/attributes_elements_with_ambiguous_names.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | AttributesOnly, HtmlElementUpdater, Nodes, NodesOwned, StaticAttributes, StaticAttributesOnly, 3 | StaticNodes, StaticNodesOwned, UpdateHtmlElement, 4 | }; 5 | use crate::{ 6 | component::Component, 7 | render::base::{NodesUpdaterMut, StringAttributeValue, U32AttributeValue}, 8 | }; 9 | 10 | #[cfg(test)] 11 | use crate::render::html::TestHtmlMethods; 12 | 13 | make_trait_for_same_name_attribute_and_element_methods! { 14 | TestStructs: (TestHtmlMethods) 15 | DeprecatedTraitName: HemsHamsAmbiguous 16 | for_elements { 17 | TraitName: HemsForAmbiguousNames 18 | UpdateElementTraitName: UpdateHtmlElement 19 | ElementUpdaterType: HtmlElementUpdater 20 | } 21 | for_attributes { 22 | TraitName: HamsForAmbiguousNames 23 | } 24 | ambiguous_attributes: 25 | // The names are also used to make methods for HTML elements 26 | // type name 27 | str abbr 28 | str cite 29 | str data 30 | str form 31 | str label 32 | u32 span 33 | } 34 | 35 | impl<'updater, C: Component> HemsHamsAmbiguous for HtmlElementUpdater<'updater, C> {} 36 | impl<'updater, C: Component> HemsHamsAmbiguous for StaticAttributes<'updater, C> {} 37 | 38 | impl<'updater, C: Component> HamsForAmbiguousNames<'updater, C> for AttributesOnly<'updater, C> {} 39 | impl<'updater, C: Component> HamsForAmbiguousNames<'updater, C> 40 | for StaticAttributesOnly<'updater, C> 41 | { 42 | } 43 | 44 | impl<'n, C: Component> HemsForAmbiguousNames<'n, C> for StaticNodesOwned<'n, C> { 45 | type Output = Self; 46 | } 47 | impl<'n, C: Component> HemsForAmbiguousNames<'n, C> for NodesOwned<'n, C> { 48 | type Output = Self; 49 | } 50 | impl<'h, 'n: 'h, C: Component> HemsForAmbiguousNames<'n, C> for StaticNodes<'h, 'n, C> { 51 | type Output = Self; 52 | } 53 | impl<'h, 'n: 'h, C: Component> HemsForAmbiguousNames<'n, C> for Nodes<'h, 'n, C> { 54 | type Output = Self; 55 | } 56 | -------------------------------------------------------------------------------- /examples/boids/src/settings.rs: -------------------------------------------------------------------------------- 1 | use gloo::storage::{LocalStorage, Storage}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 5 | pub struct Settings { 6 | /// amount of boids 7 | pub boids: usize, 8 | // time between each simulation tick 9 | pub tick_interval_ms: u64, 10 | /// view distance of a boid 11 | pub visible_range: f64, 12 | /// distance boids try to keep between each other 13 | pub min_distance: f64, 14 | /// max speed 15 | pub max_speed: f64, 16 | /// force multiplier for pulling boids together 17 | pub cohesion_factor: f64, 18 | /// force multiplier for separating boids 19 | pub separation_factor: f64, 20 | /// force multiplier for matching velocity of other boids 21 | pub alignment_factor: f64, 22 | /// controls turn speed to avoid leaving boundary 23 | pub turn_speed_ratio: f64, 24 | /// percentage of the size to the boundary at which a boid starts turning away 25 | pub border_margin: f64, 26 | /// factor for adapting the average color of the swarm 27 | pub color_adapt_factor: f64, 28 | } 29 | impl Settings { 30 | const KEY: &'static str = "spair.boids.settings"; 31 | 32 | pub fn load() -> Self { 33 | LocalStorage::get(Self::KEY).unwrap_or_default() 34 | } 35 | 36 | pub fn remove() { 37 | LocalStorage::delete(Self::KEY); 38 | } 39 | 40 | pub fn store(&self) { 41 | let _ = LocalStorage::set(Self::KEY, self); 42 | } 43 | } 44 | impl Default for Settings { 45 | fn default() -> Self { 46 | Self { 47 | boids: 300, 48 | tick_interval_ms: 50, 49 | visible_range: 80.0, 50 | min_distance: 15.0, 51 | max_speed: 20.0, 52 | alignment_factor: 0.15, 53 | cohesion_factor: 0.05, 54 | separation_factor: 0.6, 55 | turn_speed_ratio: 0.25, 56 | border_margin: 0.1, 57 | color_adapt_factor: 0.05, 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/queue_render/svg/list.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::UnwrapThrowExt; 2 | 3 | use crate::queue_render::vec::QrVec; 4 | 5 | use crate::{ 6 | component::Component, 7 | render::{ 8 | base::{NodesUpdater, NodesUpdaterMut}, 9 | svg::{ 10 | SvgAttributesOnly, SvgElementUpdater, SvgNodesOwned, SvgStaticAttributes, 11 | SvgStaticAttributesOnly, 12 | }, 13 | ListElementCreation, 14 | }, 15 | }; 16 | 17 | pub trait SemsForQrList<'a, C: Component>: Sized + Into> { 18 | fn qr_list(self, list: &QrVec, mode: ListElementCreation, render: R) 19 | where 20 | I: 'static + Clone, 21 | R: 'static + Fn(I, crate::SvgNodes), 22 | { 23 | let mut nodes_updater: SvgNodesOwned = self.into(); 24 | let qr_list_render = match nodes_updater.nodes_updater_mut().create_qr_list_render( 25 | true, 26 | mode, 27 | move |entry: I, mut nodes: NodesUpdater| { 28 | render(entry, crate::SvgNodes::new(&mut nodes)) 29 | }, 30 | ) { 31 | None => return, 32 | Some(render) => render, 33 | }; 34 | list.content() 35 | .try_borrow_mut() 36 | .expect_throw("queue_render::html::list::HemsForQrList::qr_list content borrow mut") 37 | .add_render(Box::new(qr_list_render)); 38 | list.check_and_queue_a_render(); 39 | } 40 | 41 | fn qr_list_clone(self, list: &QrVec, render: R) 42 | where 43 | I: 'static + Clone, 44 | R: 'static + Fn(I, crate::SvgNodes), 45 | { 46 | self.qr_list(list, ListElementCreation::Clone, render) 47 | } 48 | } 49 | 50 | impl<'a, C: Component> SemsForQrList<'a, C> for SvgElementUpdater<'a, C> {} 51 | impl<'a, C: Component> SemsForQrList<'a, C> for SvgAttributesOnly<'a, C> {} 52 | impl<'a, C: Component> SemsForQrList<'a, C> for SvgStaticAttributes<'a, C> {} 53 | impl<'a, C: Component> SemsForQrList<'a, C> for SvgStaticAttributesOnly<'a, C> {} 54 | -------------------------------------------------------------------------------- /crates/examples/benchmark/src/table.rs: -------------------------------------------------------------------------------- 1 | use spair::prelude::*; 2 | 3 | use crate::AppState; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct RowItem { 7 | pub id: usize, 8 | pub label: String, 9 | } 10 | 11 | #[new_view] 12 | impl Table { 13 | fn create() {} 14 | fn update(app_state: &AppState, context: &Context) {} 15 | fn view() { 16 | table( 17 | class = "table table-hover table-striped test-data", 18 | tbody( 19 | inlined.list( 20 | context, 21 | app_state.rows.iter(), 22 | |item| -> &usize { &item.id }, 23 | |citem, ccontext| { 24 | let id = citem.id; 25 | }, 26 | |uitem, ucontext| {}, 27 | tr( 28 | class_if = (Some(uitem.id) == ucontext.state.selected_id, "danger"), 29 | td(class = "col-md-1", text(citem.id)), 30 | td( 31 | class = "col-md-4", 32 | a( 33 | on_click = ccontext 34 | .comp 35 | .callback_arg(move |state, _| state.set_selected_id(id)), 36 | text(uitem.label.as_str()), 37 | ), 38 | ), 39 | td( 40 | class = "col-md-1", 41 | a( 42 | on_click = ccontext 43 | .comp 44 | .callback_arg(move |state, _| state.remove_by_id(id)), 45 | span(class = "glyphicon glyphicon-remove", aria_hidden = true), 46 | ), 47 | ), 48 | td(class = "col-md-6"), 49 | ), 50 | ), 51 | ), 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/examples/match_expr/src/main.rs: -------------------------------------------------------------------------------- 1 | use spair::prelude::*; 2 | use spair::{CallbackArg, web_sys::MouseEvent}; 3 | 4 | struct AppState { 5 | mode: Mode, 6 | } 7 | 8 | enum Mode { 9 | None, 10 | HtmlElement, 11 | View, 12 | } 13 | 14 | #[new_view] 15 | impl Button { 16 | fn create(handler: CallbackArg, text: &str) {} 17 | fn update() {} 18 | fn view() { 19 | button(on_click = handler, text(text)) 20 | } 21 | } 22 | #[component_for] 23 | impl AppState { 24 | fn create(ccontext: &Context) {} 25 | fn update(ucontext: &Context) {} 26 | fn view() { 27 | div( 28 | replace_at_element_id = "root", 29 | div( 30 | v.Button( 31 | ccontext 32 | .comp 33 | .callback_arg(|state, _| state.mode = Mode::None), 34 | "None", 35 | ), 36 | v.Button( 37 | ccontext 38 | .comp 39 | .callback_arg(|state, _| state.mode = Mode::HtmlElement), 40 | "HTML element", 41 | ), 42 | v.Button( 43 | ccontext 44 | .comp 45 | .callback_arg(|state, _| state.mode = Mode::View), 46 | "View", 47 | ), 48 | ), 49 | match &ucontext.state.mode { 50 | Mode::None => {} 51 | Mode::HtmlElement => span(text("You are in Mode::HtmlElement.")), 52 | Mode::View => v.Button( 53 | ucontext 54 | .comp 55 | .callback_arg(|state, _| state.mode = Mode::None), 56 | "You're in Button view. Click to go back to Mode::None", 57 | ), 58 | }, 59 | ) 60 | } 61 | } 62 | 63 | fn main() { 64 | // wasm_logger::init(wasm_logger::Config::default()); 65 | spair::start_app(|_| AppState { mode: Mode::None }); 66 | } 67 | -------------------------------------------------------------------------------- /examples/game_of_life/styles.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | text-align: center; 6 | } 7 | 8 | body { 9 | font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | line-height: 1.4em; 11 | color: #4d4d4d; 12 | min-width: 230px; 13 | max-width: 1000px; 14 | margin: 0 auto; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-font-smoothing: antialiased; 17 | font-smoothing: antialiased; 18 | font-weight: 300; 19 | background-color: #000000; 20 | } 21 | 22 | .app-logo { 23 | animation: app-logo-scale infinite 4s linear; 24 | height: 80px; 25 | width: 80px; 26 | } 27 | 28 | .app-header { 29 | background-color: #000; 30 | height: 130px; 31 | color: aliceblue; 32 | } 33 | 34 | .app-title { 35 | font-size: 24px; 36 | width: 100%; 37 | font-weight: 100; 38 | text-align: center; 39 | -webkit-text-rendering: optimizeLegibility; 40 | -moz-text-rendering: optimizeLegibility; 41 | text-rendering: optimizeLegibility; 42 | } 43 | 44 | .app-footer { 45 | background-color: #000000; 46 | height: 30px; 47 | padding: 10px; 48 | } 49 | 50 | .footer-text { 51 | color: aliceblue; 52 | padding-left: 20px; 53 | font-size: 14px; 54 | } 55 | 56 | .game-area { 57 | width: 94%; 58 | margin: 20px auto; 59 | } 60 | 61 | .game-container { 62 | background: #000000; 63 | margin: 20px 0 0px 0; 64 | } 65 | 66 | .game-of-life { 67 | display: inline-block; 68 | background-color: aliceblue; 69 | width: max-content; 70 | overflow: hidden; 71 | } 72 | 73 | .game-row { 74 | line-height: 0; 75 | } 76 | 77 | .game-cellule { 78 | display: inline-block; 79 | width: 8px; 80 | height: 8px; 81 | border: 1px solid #ccc; 82 | } 83 | 84 | .cellule-dead { 85 | background-color: white; 86 | } 87 | 88 | .cellule-live { 89 | background-color: black; 90 | } 91 | 92 | .game-buttons { 93 | width: 100%; 94 | margin-top: 20px; 95 | } 96 | 97 | @keyframes app-logo-scale { 98 | from { 99 | transform: scale(0.8); 100 | } 101 | to { 102 | transform: scale(1.2); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spair" 3 | version = "0.0.9" 4 | authors = ["aclueless <61309385+aclueless@users.noreply.github.com>"] 5 | edition = "2021" 6 | description = "A framework for single-page application in Rust" 7 | categories = ["wasm", "web-programming"] 8 | keywords = ["SPA", "wasm", "framework"] 9 | repository = "https://github.com/aclueless/spair" 10 | license = "MPL-2.0" 11 | readme = "README.md" 12 | 13 | [lib] 14 | doctest = false 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [features] 19 | keyed-list = ["uuid"] 20 | svg = [] 21 | queue-render = [] 22 | nightly-text-render = [] 23 | 24 | [workspace] 25 | members = [ 26 | "examples/*", 27 | "crates/spairc", 28 | "crates/examples/*", "crates/spair-macros", 29 | ] 30 | 31 | [dev-dependencies] 32 | wasm-bindgen-test = "0.3" 33 | wasm-logger = "0.2" 34 | 35 | [dependencies] 36 | log = "0.4" 37 | thiserror = "1.0" 38 | js-sys = "0.3" 39 | wasm-bindgen-futures = "0.4" 40 | uuid = { version = "1", optional = true } 41 | duplicate = "1.0.0" 42 | 43 | [dependencies.wasm-bindgen] 44 | version = "0.2" 45 | 46 | [dependencies.web-sys] 47 | version = "0.3" 48 | features = [ 49 | "Document", 50 | "Element", 51 | "Event", 52 | "EventTarget", 53 | "HtmlElement", 54 | "HtmlInputElement", 55 | "HtmlSelectElement", 56 | "HtmlOptionElement", 57 | "HtmlTextAreaElement", 58 | "HtmlFormElement", 59 | "Node", 60 | "Text", 61 | "Window", 62 | "Comment", 63 | "DomTokenList", 64 | "Location", 65 | "History", 66 | "Storage", 67 | # Events 68 | "Event", 69 | "MouseEvent", 70 | "InputEvent", 71 | "FocusEvent", 72 | "KeyboardEvent", 73 | "UiEvent", 74 | "WheelEvent", 75 | "EventTarget", 76 | "PopStateEvent", 77 | "HashChangeEvent", 78 | "ClipboardEvent", 79 | # Scroll into view 80 | "ScrollBehavior", 81 | "ScrollLogicalPosition", 82 | "ScrollIntoViewOptions", 83 | ] 84 | resolver = "2" 85 | 86 | [profile.dev] 87 | opt-level = 3 88 | 89 | [profile.release] 90 | lto = "fat" 91 | codegen-units = 1 92 | strip = "symbols" 93 | panic = "abort" 94 | 95 | [profile.bench] 96 | lto = "fat" 97 | codegen-units = 1 98 | -------------------------------------------------------------------------------- /examples/components/src/child.rs: -------------------------------------------------------------------------------- 1 | use spair::prelude::*; 2 | 3 | pub struct ChildState { 4 | props: ChildProps, 5 | value: i32, 6 | } 7 | 8 | pub struct ChildProps { 9 | pub title: &'static str, 10 | pub description: &'static str, 11 | pub callback_arg: spair::CallbackArg, 12 | } 13 | 14 | impl ChildState { 15 | pub fn new(props: ChildProps) -> Self { 16 | Self { props, value: 42 } 17 | } 18 | 19 | pub fn set_value(&mut self, value: i32) { 20 | self.value = value; 21 | } 22 | 23 | fn increment(&mut self) { 24 | self.value += 1; 25 | self.update_parent_component() 26 | } 27 | 28 | fn decrement(&mut self) { 29 | self.value -= 1; 30 | self.update_parent_component() 31 | } 32 | 33 | fn update_parent_component(&self) { 34 | if self.value % 5 == 0 { 35 | self.props.callback_arg.call_or_queue(self.value); 36 | } 37 | } 38 | } 39 | 40 | impl spair::Component for ChildState { 41 | type Routes = (); 42 | 43 | fn debug(&self) -> &str { 44 | self.props.title 45 | } 46 | 47 | fn render(&self, element: spair::Element) { 48 | let comp = element.comp(); 49 | element 50 | .static_nodes() 51 | .div(|d| d.static_text(self.props.title).done()) 52 | .line_break() 53 | .static_text( 54 | "This counter is in a child-component, \ 55 | the parent component will be notified every \ 56 | time the value is divisible by five.", 57 | ) 58 | .line_break() 59 | .update_nodes() 60 | .rfn(|nodes| super::render_button("-", comp.handler_mut(ChildState::decrement), nodes)) 61 | .update_text(self.value) 62 | .rfn(|nodes| super::render_button("+", comp.handler_mut(ChildState::increment), nodes)) 63 | .line_break() 64 | .line_break() 65 | .static_text(self.props.description); 66 | } 67 | } 68 | 69 | impl spair::AsChildComp for ChildState { 70 | const ROOT_ELEMENT_TAG: spair::TagName = spair::TagName::Html(spair::HtmlTag("div")); 71 | type Properties = ChildProps; 72 | fn init(_comp: &spair::Comp, props: Self::Properties) -> Self { 73 | Self::new(props) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/queue_render/html/list.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::UnwrapThrowExt; 2 | 3 | use crate::queue_render::vec::QrVec; 4 | 5 | use crate::{ 6 | component::Component, 7 | render::{ 8 | base::{NodesUpdater, NodesUpdaterMut}, 9 | html::{ 10 | AttributesOnly, HtmlElementUpdater, HtmlNodesUpdater, Nodes, NodesOwned, 11 | StaticAttributes, StaticAttributesOnly, 12 | }, 13 | ListElementCreation, 14 | }, 15 | }; 16 | 17 | pub trait HemsForQrList<'a, C: Component>: Sized + Into> { 18 | fn qr_list(self, list: &QrVec, render: R) 19 | where 20 | I: 'static + Clone, 21 | R: 'static + Fn(I, crate::Nodes), 22 | { 23 | qr_list(self, list, ListElementCreation::New, render) 24 | } 25 | 26 | fn qr_list_clone(self, list: &QrVec, render: R) 27 | where 28 | I: 'static + Clone, 29 | R: 'static + Fn(I, crate::Nodes), 30 | { 31 | qr_list(self, list, ListElementCreation::Clone, render) 32 | } 33 | } 34 | 35 | fn qr_list<'a, C, T, I, R>(updater: T, list: &QrVec, mode: ListElementCreation, render: R) 36 | where 37 | I: 'static + Clone, 38 | R: 'static + Fn(I, crate::Nodes), 39 | C: Component, 40 | T: Sized + Into>, 41 | { 42 | let mut nodes_updater: NodesOwned = updater.into(); 43 | let fn_render = move |entry: I, nodes: NodesUpdater| { 44 | let mut nodes = HtmlNodesUpdater::new(nodes); 45 | render(entry, Nodes::new(&mut nodes)); 46 | }; 47 | let qr_list_render = match nodes_updater 48 | .nodes_updater_mut() 49 | .create_qr_list_render(true, mode, fn_render) 50 | { 51 | None => return, 52 | Some(render) => render, 53 | }; 54 | list.content() 55 | .try_borrow_mut() 56 | .expect_throw("queue_render::html::list::HemsForQrList::qr_list content borrow mut") 57 | .add_render(Box::new(qr_list_render)); 58 | list.check_and_queue_a_render(); 59 | } 60 | 61 | impl<'a, C: Component> HemsForQrList<'a, C> for HtmlElementUpdater<'a, C> {} 62 | impl<'a, C: Component> HemsForQrList<'a, C> for AttributesOnly<'a, C> {} 63 | impl<'a, C: Component> HemsForQrList<'a, C> for StaticAttributes<'a, C> {} 64 | impl<'a, C: Component> HemsForQrList<'a, C> for StaticAttributesOnly<'a, C> {} 65 | -------------------------------------------------------------------------------- /crates/examples/components/src/main.rs: -------------------------------------------------------------------------------- 1 | use spair::prelude::*; 2 | 3 | mod child; 4 | use child::Child; 5 | 6 | pub struct State { 7 | value: i32, 8 | value_from_child: Option, 9 | child_comp: RcComp, 10 | } 11 | 12 | impl State { 13 | fn increment(&mut self) { 14 | self.value += 1; 15 | } 16 | 17 | fn decrement(&mut self) { 18 | self.value -= 1; 19 | } 20 | 21 | pub fn receive_value_from_child(&mut self, value: i32) { 22 | self.value_from_child = Some(value); 23 | } 24 | 25 | fn send_value_to_child(&mut self) { 26 | let value = self.value; 27 | self.child_comp 28 | .comp() 29 | .callback_arg(Child::set_value) 30 | .call(value); 31 | } 32 | } 33 | 34 | #[component_for] 35 | impl State { 36 | fn create(cc: &Context) {} 37 | fn update(uc: &Context) {} 38 | fn view() { 39 | div( 40 | replace_at_element_id = "root", 41 | text("Interaction between components"), 42 | hr(), 43 | div( 44 | text("In root component: "), 45 | button( 46 | on_click = cc.comp.callback_arg(|state, _| state.decrement()), 47 | text("-"), 48 | ), 49 | text(uc.state.value), 50 | button( 51 | on_click = cc.comp.callback_arg(|state, _| state.increment()), 52 | text("+"), 53 | ), 54 | button( 55 | on_click = cc.comp.callback_arg(|state, _| state.send_value_to_child()), 56 | text("Send value to child component"), 57 | ), 58 | ), 59 | div(text( 60 | "Value received from child component: ", 61 | uc.state.value_from_child.or_default("not het"), 62 | )), 63 | hr(), 64 | ws.element(cc.state.child_comp.root_element()), 65 | ) 66 | } 67 | } 68 | 69 | pub fn main() { 70 | wasm_logger::init(wasm_logger::Config::default()); 71 | spair::start_app(|app_comp| State { 72 | value: 42, 73 | value_from_child: None, 74 | child_comp: RcComp::new(|_child_comp| { 75 | Child::new(app_comp.callback_arg(State::receive_value_from_child)) 76 | }), 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /src/render/base/nodes_extensions.rs: -------------------------------------------------------------------------------- 1 | use crate::dom::Nodes; 2 | 3 | pub trait MakeNodesExtensions<'a> { 4 | fn make_nodes_extensions(self) -> NodesExtensions<'a>; 5 | } 6 | 7 | pub struct NodesExtensions<'a>(&'a Nodes); 8 | 9 | impl<'a> NodesExtensions<'a> { 10 | pub(crate) fn new(nodes: &'a Nodes) -> Self { 11 | Self(nodes) 12 | } 13 | 14 | pub fn done(self) {} 15 | 16 | pub fn scroll_to_last_element_if( 17 | self, 18 | need_to_scroll: bool, 19 | options: &web_sys::ScrollIntoViewOptions, 20 | ) -> Self { 21 | if need_to_scroll { 22 | if let Some(e) = self.0.get_last_element() { 23 | e.ws_element().scroll_to_view_with_options(options); 24 | } 25 | } 26 | self 27 | } 28 | 29 | pub fn scroll_to_top_of_last_element_if(self, need_to_scroll: bool) -> Self { 30 | if need_to_scroll { 31 | if let Some(e) = self.0.get_last_element() { 32 | e.ws_element().scroll_to_view_with_bool(true); 33 | } 34 | } 35 | self 36 | } 37 | 38 | pub fn scroll_to_bottom_of_last_element_if(self, need_to_scroll: bool) -> Self { 39 | if need_to_scroll { 40 | if let Some(e) = self.0.get_last_element() { 41 | e.ws_element().scroll_to_view_with_bool(false); 42 | } 43 | } 44 | self 45 | } 46 | 47 | pub fn scroll_to_first_element_if( 48 | self, 49 | need_to_scroll: bool, 50 | options: &web_sys::ScrollIntoViewOptions, 51 | ) -> Self { 52 | if need_to_scroll { 53 | if let Some(e) = self.0.get_first_element() { 54 | e.ws_element().scroll_to_view_with_options(options); 55 | } 56 | } 57 | self 58 | } 59 | 60 | pub fn scroll_to_top_of_first_element_if(self, need_to_scroll: bool) -> Self { 61 | if need_to_scroll { 62 | if let Some(e) = self.0.get_first_element() { 63 | e.ws_element().scroll_to_view_with_bool(true); 64 | } 65 | } 66 | self 67 | } 68 | 69 | pub fn scroll_to_bottom_of_first_element_if(self, need_to_scroll: bool) -> Self { 70 | if need_to_scroll { 71 | if let Some(e) = self.0.get_first_element() { 72 | e.ws_element().scroll_to_view_with_bool(false); 73 | } 74 | } 75 | self 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/application.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | component::{Comp, Component, RcComp}, 3 | routing::{self, Routes}, 4 | }; 5 | use wasm_bindgen::UnwrapThrowExt; 6 | 7 | pub trait Application: Component { 8 | fn init(comp: &Comp) -> Self; 9 | 10 | /// If your Component::Routes is not `()`, you must override this method 11 | /// to provide the actual router instance 12 | fn init_router(_comp: &Comp) -> Option<<::Routes as Routes>::Router> { 13 | None 14 | } 15 | 16 | fn mount_to_element(root: web_sys::Element) { 17 | let rc_comp = mount_to_element::(root); 18 | // It is the root component of the app, hence it is reasonable to just forget it. 19 | std::mem::forget(rc_comp); 20 | } 21 | 22 | fn mount_to_element_id(id: &str) { 23 | let root = crate::utils::document() 24 | .get_element_by_id(id) 25 | .expect_throw("No element associated with the specified id (to use as a root element)"); 26 | Self::mount_to_element(root); 27 | } 28 | 29 | fn mount_to_body() { 30 | let root = crate::utils::document() 31 | .body() 32 | .expect("document body") 33 | .into(); 34 | Self::mount_to_element(root); 35 | } 36 | } 37 | 38 | pub fn mount_to_element(root: web_sys::Element) -> RcComp { 39 | root.set_text_content(None); 40 | let rc_comp = RcComp::with_ws_root(root); 41 | let comp = rc_comp.comp(); 42 | 43 | // Must set the router before initing the state. 44 | match A::init_router(&comp) { 45 | Some(router) => routing::set_router(router), 46 | None if std::any::TypeId::of::<()>() 47 | != std::any::TypeId::of::<<::Routes as Routes>::Router>() => 48 | { 49 | log::warn!( 50 | "You may want to implement `Application::init_router()` to return Some(router)" 51 | ); 52 | } 53 | _ => {} 54 | } 55 | 56 | // In case that the root-component (A) have a child-component (C) that being construct by A 57 | // in A's Application::init(). Currently, C, will immediately register its callback to the 58 | // router, hence, the router must be already set before initing A's state. 59 | let state = Application::init(&comp); 60 | rc_comp.set_state(state); 61 | 62 | routing::execute_routing::<<::Routes as Routes>::Router>(); 63 | 64 | rc_comp.first_render(); 65 | rc_comp 66 | } 67 | -------------------------------------------------------------------------------- /crates/examples/benchmark/src/header.rs: -------------------------------------------------------------------------------- 1 | use spair::{prelude::*, web_sys::MouseEvent}; 2 | 3 | use crate::AppState; 4 | 5 | #[new_view] 6 | impl Button { 7 | fn create(id: &str, text: &str, callback: CallbackArg) {} 8 | fn update() {} 9 | fn view() { 10 | div( 11 | class = "col-sm-6 smallpad", 12 | button( 13 | id = id, 14 | class = "btn btn-primary btn-block", 15 | r#type = "button", 16 | on_click = callback, 17 | text(text), 18 | ), 19 | ) 20 | } 21 | } 22 | 23 | #[new_view] 24 | impl Header { 25 | fn create(comp: &Comp) {} 26 | fn update() {} 27 | fn view() { 28 | div( 29 | class = "jumbotron", 30 | div( 31 | class = "row", 32 | div(class = "col-md-6", h1(text("Spair Keyed"))), 33 | div( 34 | class = "col-md-6", 35 | div( 36 | class = "row", 37 | v.Button( 38 | "run", 39 | "Create 1,000 rows", 40 | comp.callback_arg(|state, _| state.create(1000)), 41 | ), 42 | v.Button( 43 | "runlots", 44 | "Create 10,000 rows", 45 | comp.callback_arg(|state, _| state.create(10000)), 46 | ), 47 | v.Button( 48 | "add", 49 | "Append 1,000 rows", 50 | comp.callback_arg(|state, _| state.append(1000)), 51 | ), 52 | v.Button( 53 | "update", 54 | "Update every 10th row", 55 | comp.callback_arg(|state, _| state.update()), 56 | ), 57 | v.Button( 58 | "clear", 59 | "Clear", 60 | comp.callback_arg(|state, _| state.clear()), 61 | ), 62 | v.Button( 63 | "swaprows", 64 | "Swap Rows", 65 | comp.callback_arg(|state, _| state.swap()), 66 | ), 67 | ), 68 | ), 69 | ), 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! set_arm { 3 | ( $match_if:ident $(,)? ) => { 4 | $match_if.render_on_arm_index({ 5 | struct Index; 6 | ::core::any::TypeId::of::() 7 | }) 8 | }; 9 | } 10 | 11 | #[cfg(test)] 12 | macro_rules! make_a_test_component { 13 | ( 14 | type: $Type:ty; 15 | init: $init_expr:expr; 16 | render_fn: $($render_fn:tt)+ 17 | ) => { 18 | use $crate::prelude::*; 19 | 20 | struct TestComponent($Type); 21 | 22 | impl TestComponent { 23 | fn update(&mut self, value: $Type) { 24 | self.0 = value; 25 | } 26 | } 27 | 28 | impl $crate::Component for TestComponent { 29 | type Routes = (); 30 | $($render_fn)+ 31 | } 32 | 33 | impl $crate::Application for TestComponent { 34 | fn init(_comp: &$crate::Comp) -> Self { 35 | TestComponent($init_expr) 36 | } 37 | } 38 | 39 | struct Test { 40 | root: web_sys::Node, 41 | rc_comp: $crate::component::RcComp, 42 | callback: $crate::CallbackArg<$Type>, 43 | } 44 | 45 | impl Test { 46 | fn set_up() -> Test { 47 | let root = crate::dom::Element::new_ns($crate::HtmlTag("div")); 48 | let rc_comp = 49 | $crate::application::mount_to_element(root.ws_element().clone().into_inner()); 50 | let callback = rc_comp.comp().callback_arg_mut(TestComponent::update); 51 | Self { 52 | root: root.ws_element().ws_node().clone(), 53 | rc_comp, 54 | callback, 55 | } 56 | } 57 | 58 | #[allow(dead_code)] 59 | fn update(&self, value: $Type) { 60 | self.callback.call(value); 61 | } 62 | 63 | #[allow(dead_code)] 64 | fn update_with(&self, updater: impl Fn(&mut $Type) + 'static) { 65 | self.rc_comp.comp().callback_mut(move |state| updater(&mut state.0)).call(); 66 | } 67 | 68 | #[allow(dead_code)] 69 | fn execute_on_nodes(&self, func: impl Fn(&[$crate::dom::Node]) -> T) -> T { 70 | let comp_instance = self.rc_comp.comp_instance(); 71 | let nodes_vec = comp_instance.root_element().nodes().nodes_vec(); 72 | func(nodes_vec) 73 | } 74 | 75 | #[allow(dead_code)] 76 | fn text_content(&self) -> Option { 77 | self.root.text_content() 78 | } 79 | } 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /src/render/base/list.rs: -------------------------------------------------------------------------------- 1 | use super::NodesUpdater; 2 | use crate::{ 3 | component::{Comp, Component}, 4 | dom::{ElementStatus, Nodes}, 5 | }; 6 | 7 | #[must_use = "Caller should set selected option for