├── 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 | | | |