├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── everything.yml │ └── website.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile.toml ├── README.md ├── book-src ├── README.md ├── SUMMARY.md ├── core-api.md ├── helmet-api.md ├── query-api.md └── tutorial.md ├── book.toml ├── crates ├── bounce-macros │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── atom.rs │ │ ├── future_notion.rs │ │ ├── lib.rs │ │ └── slice.rs └── bounce │ ├── Cargo.toml │ ├── src │ ├── any_state.rs │ ├── helmet │ │ ├── bridge.rs │ │ ├── comp.rs │ │ ├── mod.rs │ │ ├── ssr.rs │ │ └── state.rs │ ├── lib.rs │ ├── provider.rs │ ├── query │ │ ├── mod.rs │ │ ├── mutation_states.rs │ │ ├── query_states.rs │ │ ├── traits.rs │ │ ├── use_mutation.rs │ │ ├── use_prepared_query.rs │ │ ├── use_query.rs │ │ └── use_query_value.rs │ ├── root_state.rs │ ├── states │ │ ├── artifact.rs │ │ ├── atom.rs │ │ ├── future_notion.rs │ │ ├── input_selector.rs │ │ ├── mod.rs │ │ ├── notion.rs │ │ ├── observer.rs │ │ ├── selector.rs │ │ └── slice.rs │ └── utils.rs │ └── tests │ ├── init_states.rs │ ├── notion.rs │ └── query.rs └── examples ├── divisibility-input ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── divisibility ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── helmet-ssr ├── Cargo.toml ├── index.html └── src │ ├── bin │ ├── helmet-ssr-client.rs │ └── helmet-ssr-server.rs │ └── lib.rs ├── helmet-title ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── notion ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── partial-render ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── persist ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── queries-mutations ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── queries-ssr ├── Cargo.toml ├── index.html └── src │ ├── bin │ ├── queries-ssr-client.rs │ └── queries-ssr-server.rs │ └── lib.rs ├── random-uuid ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── simple ├── Cargo.toml ├── index.html └── src │ └── main.rs └── title ├── Cargo.toml ├── index.html └── src └── main.rs /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | 5 | This pull request consists of the following changes: 6 | 7 | 8 | 9 | ### Checklist 10 | 11 | - [ ] I have self-reviewed and tested this pull request to my best ability. 12 | - [ ] I have added tests for my changes. 13 | - [ ] I have updated docs to reflect any new features / changes in this pull request. 14 | - [ ] I have added my changes to `CHANGELOG.md`. 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | - package-ecosystem: "cargo" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.github/workflows/everything.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run Tests & Publishing 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | lint: 8 | name: Lint Codebase 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout Project 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Setup Rust 17 | uses: dtolnay/rust-toolchain@stable 18 | with: 19 | targets: wasm32-unknown-unknown 20 | components: rustfmt, clippy 21 | 22 | - name: Restore Rust Cache 23 | uses: Swatinem/rust-cache@v2 24 | 25 | - name: Install cargo-make 26 | uses: davidB/rust-cargo-make@v1 27 | 28 | - name: Run Lints 29 | run: cargo make lints 30 | 31 | build: 32 | name: Build Examples 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout Project 36 | uses: actions/checkout@v4 37 | with: 38 | fetch-depth: 0 39 | 40 | - name: Setup Rust 41 | uses: dtolnay/rust-toolchain@stable 42 | with: 43 | targets: wasm32-unknown-unknown 44 | components: rustfmt, clippy 45 | 46 | - name: Restore Rust Cache 47 | uses: Swatinem/rust-cache@v2 48 | 49 | - name: Setup trunk 50 | uses: jetli/trunk-action@v0.4.0 51 | with: 52 | version: "latest" 53 | 54 | - name: Build Examples 55 | run: | 56 | for d in "examples/"* 57 | do 58 | trunk build --release $d/index.html 59 | done 60 | 61 | test: 62 | name: Run Tests 63 | runs-on: ubuntu-latest 64 | services: 65 | httpbin: 66 | image: kennethreitz/httpbin 67 | ports: 68 | - 8080:80 69 | steps: 70 | - name: Checkout Project 71 | uses: actions/checkout@v4 72 | with: 73 | fetch-depth: 0 74 | 75 | - name: Setup Rust 76 | uses: dtolnay/rust-toolchain@stable 77 | with: 78 | targets: wasm32-unknown-unknown 79 | components: rustfmt, clippy 80 | 81 | - name: Restore Rust Cache 82 | uses: Swatinem/rust-cache@v2 83 | 84 | - name: Install wasm-pack 85 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 86 | 87 | - name: Install cargo-make 88 | uses: davidB/rust-cargo-make@v1 89 | 90 | - name: Run Tests 91 | run: cargo make tests 92 | 93 | publish: 94 | name: Publish to crates.io 95 | runs-on: ubuntu-latest 96 | needs: 97 | - lint 98 | - build 99 | - test 100 | if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')) 101 | steps: 102 | - name: Checkout Project 103 | uses: actions/checkout@v4 104 | with: 105 | fetch-depth: 0 106 | 107 | - name: Setup Rust 108 | uses: dtolnay/rust-toolchain@stable 109 | with: 110 | targets: wasm32-unknown-unknown 111 | components: rustfmt, clippy 112 | 113 | - name: Restore Rust Cache 114 | uses: Swatinem/rust-cache@v2 115 | 116 | - name: Run cargo publish --dry-run for bounce-macros 117 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 118 | run: cargo publish --dry-run --manifest-path crates/bounce-macros/Cargo.toml 119 | env: 120 | RUSTFLAGS: "--cfg releasing" 121 | 122 | - name: Run cargo publish for bounce-macros 123 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 124 | run: cargo publish --token ${{ secrets.CRATES_IO_TOKEN }} --manifest-path crates/bounce-macros/Cargo.toml 125 | env: 126 | RUSTFLAGS: "--cfg releasing" 127 | 128 | # Not possible if bounce-macros does not have a compatible version on crates.io 129 | # See: https://github.com/rust-lang/cargo/issues/1169 130 | # - name: Run cargo publish --dry-run for bounce 131 | # if: github.event_name == 'push' && github.ref == 'refs/heads/master' 132 | # run: cargo publish --dry-run --manifest-path crates/bounce/Cargo.toml 133 | # env: 134 | # RUSTFLAGS: "--cfg releasing" 135 | 136 | - name: Run cargo publish for bounce 137 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 138 | run: cargo publish --token ${{ secrets.CRATES_IO_TOKEN }} --manifest-path crates/bounce/Cargo.toml 139 | env: 140 | RUSTFLAGS: "--cfg releasing" 141 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish Website 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | publish-website: 11 | name: Publish Website 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Project 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Rust 18 | uses: dtolnay/rust-toolchain@stable 19 | 20 | - name: Restore Rust Cache 21 | uses: Swatinem/rust-cache@v2 22 | 23 | - name: Install mdbook 24 | run: cargo install --locked mdbook 25 | 26 | - name: Build Website 27 | run: mdbook build 28 | 29 | - name: Deploy Website 30 | uses: JamesIves/github-pages-deploy-action@v4.4.3 31 | with: 32 | branch: main 33 | folder: book-build 34 | 35 | ssh-key: "${{ secrets.WEBSITE_DEPLOY_KEY }}" 36 | repository-name: "${{ secrets.WEBSITE_DEPLOY_REPO }}" 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | crates/*/target 3 | 4 | examples/*/target 5 | examples/*/dist 6 | 7 | book-build 8 | 9 | Cargo.lock 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Release 0.9.0 2 | 3 | ### Breaking Changes 4 | 5 | - Compatible with Yew 0.21. Due to the breaking API changes in Yew 0.21 not backwards compatible with Yew 0.20. 6 | 7 | ### Other Changes 8 | 9 | - Updated the Gloo dependency to 0.10 10 | 11 | ## Release 0.8.0 12 | 13 | ### Breaking Changes 14 | 15 | - Raise MSRV to 1.64. 16 | - Do not suspend refresh queries. 17 | - Expose a State type for each Query / Mutation that also references to the value. 18 | 19 | ### Other Changes 20 | 21 | - Fix Mutation Loading and Refreshing. 22 | - Allow Future Notion name to be omitted. 23 | - Break loop by returning the inner value. 24 | 25 | ## Release 0.7.0 26 | 27 | ### Other Changes 28 | 29 | - Added `get_init_states` to `BounceRoot` that can be used to provide initial state values other than using `Default`. 30 | 31 | ## Release 0.6.1 32 | 33 | ### Other Changes 34 | 35 | - Added 'property' to the deduplication logic for `` tags 36 | 37 | ## Release 0.6.0 38 | 39 | ### Breaking Changes 40 | 41 | - `use_mutation_value` is renamed to `use_mutation`. 42 | - `.result()` on now returns `Option<&...>`. 43 | 44 | ### Other Changes 45 | 46 | - Fixed query hooks wrongly fallback when refreshing. 47 | - Fixed query hooks panicking when already fetching. 48 | 49 | ## Release 0.5.0 50 | 51 | ### Other Changes 52 | 53 | - Helmet API now supports SSR. 54 | - Added `use_query` which fetches data via Suspense. 55 | - Added `use_prepared_query`, which works like `use_query` but preserves the value created in SSR. 56 | 57 | ## Release 0.4.0 58 | 59 | ### Breaking Changes 60 | 61 | - Bounce now targets Yew 0.20. 62 | 63 | ### Other Changes 64 | 65 | - Fixed a reference cycle that presents in the codebase. 66 | 67 | ## Release 0.3.0 68 | 69 | ### Breaking Changes 70 | 71 | - `with_notion` now needs to be wrapped inside `bounce`. 72 | 73 | ### Other Changes 74 | 75 | - Added Artifact API 76 | - Added Helmet API 77 | - Added Observer API 78 | - Notion is now registered and does not iterate over all states. 79 | - Fixed a bug where the query will not requery itself if any state it 80 | selected has updated. 81 | 82 | ## Release 0.2.0 83 | 84 | ### Breaking Changes 85 | 86 | - Slice is now a derive macro and slices now are required to implement the Reducible trait 87 | - Atom no longer directly implements Slice 88 | 89 | ### Other Changes 90 | 91 | - Added Notion API 92 | - Added FutureNotion API 93 | - Added Selector API 94 | - Added InputSelector API 95 | - Added Query API 96 | - Update Dependencies in Cargo.toml 97 | 98 | ## Release 0.1.3 99 | 100 | ### Other Changes 101 | 102 | - Update Dependencies in Cargo.toml 103 | 104 | ## Release 0.1.2 105 | 106 | ### Other Changes 107 | 108 | - Add License to Cargo.toml 109 | 110 | ## Release 0.1.1 111 | 112 | ### Other Changes: 113 | 114 | - Update Readme & Cargo.toml 115 | 116 | ## Release 0.1.0 117 | 118 | - Initial Release. 119 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/bounce", 4 | "crates/bounce-macros", 5 | 6 | "examples/notion", 7 | "examples/simple", 8 | "examples/partial-render", 9 | "examples/random-uuid", 10 | "examples/divisibility", 11 | "examples/divisibility-input", 12 | "examples/queries-mutations", 13 | "examples/queries-ssr", 14 | "examples/title", 15 | "examples/helmet-title", 16 | "examples/helmet-ssr", 17 | "examples/persist", 18 | ] 19 | resolver = "2" 20 | 21 | [profile.release] 22 | lto = true 23 | codegen-units = 1 24 | panic = "abort" 25 | opt-level = "z" 26 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kaede Hoshikawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true 3 | 4 | ### Lints ### 5 | 6 | # cargo clippy --all-targets --release -- --deny=warnings 7 | # Lints --release profile 8 | [tasks.release-lint] 9 | private = true 10 | command = "cargo" 11 | args = ["clippy", "--all-targets", "--release", "--", "--deny=warnings"] 12 | 13 | # env RUSTFLAG="--cfg releasing" cargo clippy --all-targets -- --deny=warnings 14 | # Lints --cfg releasing flag 15 | [tasks.releasing-lint] 16 | private = true 17 | command = "cargo" 18 | args = ["clippy", "--all-targets", "--", "--deny=warnings"] 19 | 20 | [tasks.releasing-lint.env] 21 | RUSTFLAG = "--cfg releasing" 22 | 23 | # cargo fmt --all -- --check 24 | [tasks.fmt-lint] 25 | private = true 26 | command = "cargo" 27 | args = ["fmt", "--all", "--", "--check"] 28 | 29 | # Each workspace member needs to be checked separately so that 30 | # feature flags will not be merged. 31 | [tasks.lints.run_task] 32 | name = ["fmt-lint", "release-lint", "releasing-lint"] 33 | fork = true 34 | 35 | ### Tests ### 36 | 37 | # wasm-pack test --headless --firefox 38 | [tasks.wasm-test] 39 | private = true 40 | workspace = true 41 | command = "wasm-pack" 42 | args = ["test", "--headless", "--firefox"] 43 | 44 | # wasm-pack test --headless --firefox --all-features 45 | [tasks.wasm-test-all-features] 46 | private = true 47 | workspace = true 48 | command = "wasm-pack" 49 | args = ["test", "--headless", "--firefox", "--all-features"] 50 | 51 | # cargo test 52 | [tasks.native-test] 53 | private = true 54 | workspace = true 55 | command = "cargo" 56 | args = ["test"] 57 | 58 | # cargo test --all-features 59 | [tasks.native-test-all-features] 60 | private = true 61 | workspace = true 62 | command = "cargo" 63 | args = ["test", "--all-features"] 64 | 65 | # cargo test --doc --all-features 66 | [tasks.doc-test] 67 | private = true 68 | workspace = false 69 | command = "cargo" 70 | args = ["test", "--doc", "--all-features", "--workspace"] 71 | 72 | # Each workspace member needs to be tested separately so that 73 | # feature flags will not be merged. 74 | [tasks.tests] 75 | workspace = false 76 | 77 | [tasks.tests.run_task] 78 | name = ["wasm-test", "wasm-test-all-features", "native-test", "native-test-all-features", "doc-test"] 79 | fork = true 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bounce 2 | 3 | [![Run Tests & Publishing](https://github.com/bounce-rs/bounce/actions/workflows/everything.yml/badge.svg)](https://github.com/bounce-rs/bounce/actions/workflows/everything.yml) 4 | [![crates.io](https://img.shields.io/crates/v/bounce)](https://crates.io/crates/bounce) 5 | [![docs.rs](https://docs.rs/bounce/badge.svg)](https://docs.rs/bounce/) 6 | 7 | The uncomplicated state management library for Yew. 8 | 9 | Bounce is inspired by [Redux](https://github.com/reduxjs/redux) and 10 | [Recoil](https://github.com/facebookexperimental/Recoil). 11 | 12 | ## Rationale 13 | 14 | Yew state management solutions that are currently available all have 15 | some (or all) of the following limitations: 16 | 17 | - Too much boilerplate. 18 | 19 | Users either have to manually control whether to notify 20 | subscribers or have to manually define contexts. 21 | 22 | - State change notifies all. 23 | 24 | State changes will notify all subscribers. 25 | 26 | - Needless clones. 27 | 28 | A clone of the state will be produced for all subscribers whenever 29 | there's a change. 30 | 31 | Bounce wants to be a state management library that: 32 | 33 | - Has minimal boilerplate. 34 | 35 | Changes are automatically detected via `PartialEq`. 36 | 37 | - Only notifies relevant subscribers. 38 | 39 | When a state changes, only hooks that subscribe to that state will 40 | be notified. 41 | 42 | - Reduces Cloning. 43 | 44 | States are `Rc`'ed. 45 | 46 | ## Example 47 | 48 | For bounce states to function, a `` must be registered. 49 | 50 | ```rust 51 | #[function_component(App)] 52 | fn app() -> Html { 53 | html! { 54 | 55 | {children} 56 | 57 | } 58 | } 59 | ``` 60 | 61 | A simple state is called an `Atom`. 62 | 63 | You can derive `Atom` for any struct that implements `PartialEq` and `Default`. 64 | 65 | ```rust 66 | #[derive(PartialEq, Atom)] 67 | struct Username { 68 | inner: String, 69 | } 70 | 71 | impl Default for Username { 72 | fn default() -> Self { 73 | Self { 74 | inner: "Jane Doe".into(), 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | You can then use it with the `use_atom` hook. 81 | 82 | When an `Atom` is first used, it will be initialised with its `Default` 83 | value. 84 | 85 | ```rust 86 | #[function_component(Setter)] 87 | fn setter() -> Html { 88 | let username = use_atom::(); 89 | 90 | let on_text_input = { 91 | let username = username.clone(); 92 | 93 | Callback::from(move |e: InputEvent| { 94 | let input: HtmlInputElement = e.target_unchecked_into(); 95 | 96 | username.set(Username { inner: input.value().into() }); 97 | }) 98 | }; 99 | 100 | html! { 101 |
102 | 103 |
104 | } 105 | } 106 | ``` 107 | 108 | If you wish to create a read-only (or set-only) handle, you can use 109 | `use_atom_value` (or `use_atom_setter`). 110 | 111 | ```rust 112 | #[function_component(Reader)] 113 | fn reader() -> Html { 114 | let username = use_atom_value::(); 115 | 116 | html! {
{"Hello, "}{&username.inner}
} 117 | } 118 | ``` 119 | 120 | You can find the full example [here](https://github.com/futursolo/bounce/blob/master/examples/simple/src/main.rs). 121 | 122 | ## License 123 | 124 | Bounce is dual licensed under the MIT license and the Apache License (Version 2.0). 125 | -------------------------------------------------------------------------------- /book-src/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 |

Bounce

4 | 5 |

6 | 7 | crates.io 8 | 9 | | 10 | 11 | docs.rs 12 | 13 | | 14 | 15 | GitHub 16 | 17 |

18 | 19 | Bounce is a state-management library focusing on simplicity and 20 | performance. 21 | 22 | Bounce is inspired by [Redux](https://github.com/reduxjs/redux) and 23 | [Recoil](https://github.com/facebookexperimental/Recoil). 24 | 25 | ## Rationale 26 | 27 | Yew state management solutions that are currently available all have 28 | some (or all) of the following limitations: 29 | 30 | - Too much boilerplate. 31 | 32 | Users either have to manually control whether to notify 33 | subscribers or have to manually define contexts. 34 | 35 | - State change notifies all. 36 | 37 | State changes will notify all subscribers. 38 | 39 | - Needless clones. 40 | 41 | A clone of the state will be produced for all subscribers whenever 42 | there's a change. 43 | 44 | Bounce wants to be a state management library that: 45 | 46 | - Has minimal boilerplate. 47 | 48 | Changes are automatically detected via `PartialEq`. 49 | 50 | - Only notifies relevant subscribers. 51 | 52 | When a state changes, only hooks that subscribe to that state will 53 | be notified. 54 | 55 | - Reduces Cloning. 56 | 57 | States are `Rc`'ed. 58 | 59 | ## Installation 60 | 61 | You can add it to your project with the following command: 62 | 63 | ```shell 64 | cargo add bounce 65 | ``` 66 | 67 | You can also add it to the `Cargo.toml` of your project manually: 68 | 69 | ```toml 70 | bounce = "0.9" 71 | ``` 72 | 73 | ## Getting Started 74 | 75 | If you want to learn more about Bounce, you can check out the 76 | [tutorial](./tutorial.md) and the [API documentation](https://docs.rs/bounce/). 77 | 78 | ## Licence 79 | 80 | Bounce is dual licensed under the MIT license and the Apache License (Version 2.0). 81 | -------------------------------------------------------------------------------- /book-src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # SUMMARY 2 | 3 | # Getting Started 4 | 5 | - [Introduction](./README.md) 6 | - [Tutorial](./tutorial.md) 7 | 8 | # Reference 9 | 10 | - [Core API](./core-api.md) 11 | - [Helmet API](./helmet-api.md) 12 | - [Query API](./query-api.md) 13 | -------------------------------------------------------------------------------- /book-src/helmet-api.md: -------------------------------------------------------------------------------- 1 | # Helmet API 2 | 3 | The Helmet API is an API to manipulate elements resided in the `` element. 4 | 5 | Elements can be applied with the `Helmet` component. 6 | 7 | ```rust 8 | html! { 9 | 10 | // The title of current page. 11 | {"page a title"} 12 | 13 | } 14 | ``` 15 | 16 | The Helmet API supports the following elements: 17 | 18 | - title 19 | - style 20 | - script 21 | - base 22 | - link 23 | - meta 24 | 25 | The Helmet API supports setting attributes of the following elements: 26 | 27 | - html 28 | - body 29 | 30 | ### Helmet Bridge 31 | 32 | The `` component is used to customise the behaviour and 33 | responsible of reconciling the elements to the `` element. 34 | 35 | ```rust 36 | html! { 37 | 38 | 39 | // other components. 40 | 41 | } 42 | ``` 43 | 44 | The Helmet Bridge component accepts two properties, 45 | a `default_title` which will be applied when no other title elements 46 | are registered and a `format_title` function which is used to format 47 | the title before it is passed to the document. 48 | 49 | ### API Reference: 50 | 51 | - [`Helmet API`](https://docs.rs/bounce/latest/bounce/helmet/index.html) 52 | -------------------------------------------------------------------------------- /book-src/query-api.md: -------------------------------------------------------------------------------- 1 | # Query API 2 | 3 | The Query API provides hook-based access to APIs with automatic caching 4 | and request deduplication backed by Bounce’s state management mechanism. 5 | 6 | #### Note 7 | 8 | Bounce does not provide an implementation of HTTP Client. 9 | 10 | You can use reqwest or gloo-net if your backend is using Restful API. 11 | 12 | For GraphQL servers, you can use graphql-client in conjunction with reqwest. 13 | 14 | ### Query 15 | 16 | A query is a state cached by an Input and queried automatically upon initialisation of the state and re-queried when the input changes. 17 | 18 | Queries are usually tied to idempotent methods like GET, which means that they should be side-effect free and can be cached. 19 | 20 | If your endpoint modifies data, you need to use a [mutation](#mutation). 21 | 22 | API Reference: 23 | 24 | - [`use_query_value`](https://docs.rs/bounce/latest/bounce/query/fn.use_query_value.html) 25 | 26 | ### Mutation 27 | 28 | A hook to run a mutation and subscribes to its result. 29 | 30 | A mutation is a state that is not started until the run method is invoked. 31 | Mutations are usually used to modify data on the server. 32 | 33 | API Reference: 34 | 35 | - [`use_mutation`](https://docs.rs/bounce/latest/bounce/query/fn.use_mutation.html) 36 | -------------------------------------------------------------------------------- /book-src/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | This tutorial will guide you to create a simple application that 4 | greets the user with the entered name. 5 | 6 | ## 0. Prerequisites 7 | 8 | This tutorial assumes the reader is familiar with the basics of Yew and 9 | Rust. If you are new to Yew or Rust, you may find the following content 10 | helpful: 11 | 12 | 1. [The Rust Programming Language](https://doc.rust-lang.org/book/) 13 | 2. [The `wasm-bindgen` Guide](https://rustwasm.github.io/wasm-bindgen/introduction.html) 14 | 3. [The Yew Docs](https://yew.rs/docs/getting-started/introduction) 15 | 16 | You need the following tools: 17 | 18 | 1. [Rust](https://rustup.rs/) with `wasm32-unknown-unknown` target toolchain 19 | 20 | If you installed Rust with rustup, You can obtain the 21 | `wasm32-unknown-unknown` target toolchain with the following command: 22 | 23 | ```shell 24 | rustup target add wasm32-unknown-unknown 25 | ``` 26 | 27 | 2. The [Trunk](https://trunkrs.dev/#getting-started) Bundler 28 | 29 | 3. [`cargo-edit`](https://github.com/killercup/cargo-edit) 30 | 31 | ## 1. Prepare Dependencies 32 | 33 | This tutorial uses the `yew-trunk-minimal-template`. 34 | This template repository has a minimal setup of Yew with Trunk. 35 | You can create a new repository using the template with the following commands: 36 | 37 | ```shell 38 | mkdir my-first-bounce-app 39 | cd my-first-bounce-app 40 | git init 41 | git fetch --depth=1 -n https://github.com/yewstack/yew-trunk-minimal-template.git 42 | git reset --hard $(git commit-tree FETCH_HEAD^{tree} -m "initial commit") 43 | ``` 44 | 45 | To add bounce to the dependencies, run the following command: 46 | 47 | ```shell 48 | cargo add bounce 49 | ``` 50 | 51 | You can now view the app with Trunk: 52 | 53 | ```shell 54 | trunk serve --open 55 | ``` 56 | 57 | ## 2. Register Bounce Root 58 | 59 | BounceRoot is a context provider that provides state management and 60 | synchronisation mechanism to its child components. 61 | 62 | It should be registered as a parent of all components that interact with 63 | the bounce states. You only need 1 BounceRoot per application. 64 | 65 | In this example, we can add it to the `App` component. 66 | 67 | ```rust 68 | use bounce::BounceRoot; 69 | use yew::prelude::*; 70 | 71 | #[function_component(App)] 72 | pub fn app() -> Html { 73 | html! { 74 | // Register BounceRoot so that states can be provided to its 75 | // children. 76 | 77 |
78 | 79 |

{ "Hello World!" }

80 | { "from Yew with " } 81 |
82 |
83 | } 84 | } 85 | ``` 86 | 87 | ## 3. Create an Atom 88 | 89 | An atom is a simple state that is similar to a state created by 90 | the `use_state` hook in Yew. 91 | 92 | Atoms are created by deriving the Atom macro on a type. `Atom` can be derived 93 | on any type that implements `Default` and `PartialEq`. 94 | 95 | ```rust 96 | #[derive(Atom, PartialEq)] 97 | pub struct Username { 98 | value: String, 99 | } 100 | 101 | impl Default for Username { 102 | fn default() -> Self { 103 | Username { 104 | value: "Jane Doe".into(), 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | ## 4. Use an Atom 111 | 112 | An Atom can be used in any component under a `BounceRoot` with the 113 | following hooks: 114 | 115 | - `use_atom`: Returns a `UseAtomHandle`, 116 | which can be dereferenced to `T` and includes a `.set` method that can be 117 | used to update the value of this atom and triggers a re-render when 118 | the value changes. 119 | 120 | - `use_atom_value`: Returns `Rc` and triggers a re-render when 121 | the value changes. 122 | 123 | - `use_atom_setter`: Returns a setter of type `Rc` that can be used 124 | to update the value of `T`. This type will not trigger a re-render 125 | when the value changes. 126 | 127 | ## 5. Create a component that displays the username 128 | 129 | To create a component that reads the value of an Atom, you can use the 130 | `use_atom_value` hook mentioned in the previous chapter. 131 | 132 | When the first time a state is used with a hook, 133 | its value will be initialised with the value returned by 134 | `Default::default()`. 135 | 136 | ```rust 137 | #[function_component(Reader)] 138 | fn reader() -> Html { 139 | let username = use_atom_value::(); 140 | 141 | html! {
{"Hello, "}{&username.value}
} 142 | } 143 | ``` 144 | 145 | ## 6. Create a component to update the username 146 | 147 | The `use_atom` hook can be used to establish a bi-directional connection 148 | between a component and a bounce state. 149 | 150 | ```rust 151 | #[function_component(Setter)] 152 | fn setter() -> Html { 153 | let username = use_atom::(); 154 | 155 | let on_text_input = { 156 | let username = username.clone(); 157 | 158 | Callback::from(move |e: InputEvent| { 159 | let input: HtmlInputElement = e.target_unchecked_into(); 160 | 161 | username.set(Username { 162 | value: input.value(), 163 | }); 164 | }) 165 | }; 166 | 167 | html! { 168 |
169 | 170 |
171 | } 172 | } 173 | ``` 174 | 175 | ## 7. Update app 176 | 177 | To make sure that our `` and `` component can 178 | communicate with ``, we need to put them under the bounce 179 | root. 180 | 181 | ```rust 182 | use bounce::BounceRoot; 183 | use yew::prelude::*; 184 | 185 | #[function_component(App)] 186 | pub fn app() -> Html { 187 | html! { 188 | 189 | 190 | 191 | 192 | } 193 | } 194 | ``` 195 | 196 | You may refer to the [simple](https://github.com/futursolo/bounce/tree/master/examples/simple) 197 | example for a complete example. 198 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | language = "en" 3 | multilingual = false 4 | src = "book-src" 5 | title = "The Bounce Book" 6 | 7 | [rust] 8 | edition = "2021" 9 | 10 | [build] 11 | build-dir = "book-build" 12 | 13 | [output.html] 14 | preferred-dark-theme = "ayu" 15 | -------------------------------------------------------------------------------- /crates/bounce-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bounce-macros" 3 | version = "0.9.0" 4 | edition = "2021" 5 | repository = "https://github.com/bounce-rs/bounce" 6 | authors = ["Kaede Hoshiakwa "] 7 | description = "The uncomplicated state management library for Yew." 8 | keywords = ["web", "wasm", "yew", "state-management"] 9 | categories = ["wasm", "web-programming"] 10 | readme = "README.md" 11 | homepage = "https://github.com/bounce-rs/bounce" 12 | license = "MIT OR Apache-2.0" 13 | rust-version = "1.64" 14 | 15 | [lib] 16 | proc-macro = true 17 | 18 | [dependencies] 19 | proc-macro-error = "1.0.4" 20 | proc-macro2 = "1.0.63" 21 | quote = "1.0.29" 22 | syn = { version = "2.0.22", features = ["full", "extra-traits"] } 23 | -------------------------------------------------------------------------------- /crates/bounce-macros/README.md: -------------------------------------------------------------------------------- 1 | # Bounce Macros 2 | 3 | This crate contains procedural macros for 4 | [Bounce](https://crates.io/crates/bounce). 5 | -------------------------------------------------------------------------------- /crates/bounce-macros/src/atom.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::quote; 3 | use syn::{DeriveInput, Ident}; 4 | 5 | use super::slice::BounceAttrs; 6 | 7 | pub(crate) fn macro_fn(input: DeriveInput) -> TokenStream { 8 | let bounce_attrs = match BounceAttrs::parse(&input.attrs) { 9 | Ok(m) => m, 10 | Err(e) => return e.into_compile_error(), 11 | }; 12 | 13 | let notion_ident = Ident::new("notion", Span::mixed_site()); 14 | let notion_apply_impls = bounce_attrs.create_notion_apply_impls(¬ion_ident); 15 | let notion_ids_impls = bounce_attrs.create_notion_id_impls(); 16 | 17 | let ident = input.ident; 18 | 19 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 20 | 21 | let impl_observed = bounce_attrs.observed.is_some().then(|| { 22 | quote! { 23 | fn changed(self: ::std::rc::Rc) { 24 | ::bounce::Observed::changed(self); 25 | } 26 | } 27 | }); 28 | 29 | quote! { 30 | #[automatically_derived] 31 | impl #impl_generics ::bounce::Atom for #ident #ty_generics #where_clause { 32 | fn apply(self: ::std::rc::Rc, #notion_ident: ::std::rc::Rc) -> ::std::rc::Rc { 33 | #(#notion_apply_impls)* 34 | 35 | self 36 | } 37 | 38 | fn notion_ids(&self) -> ::std::vec::Vec<::std::any::TypeId> { 39 | ::std::vec![#(#notion_ids_impls,)*] 40 | } 41 | 42 | #impl_observed 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/bounce-macros/src/future_notion.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::quote; 3 | use syn::parse::{Parse, ParseStream}; 4 | use syn::punctuated::Punctuated; 5 | use syn::token::Comma; 6 | use syn::{parse_quote, FnArg, Generics, Ident, ItemFn, ReturnType, Type, Visibility}; 7 | 8 | #[derive(Debug)] 9 | pub struct FutureNotionAttr { 10 | name: Option, 11 | } 12 | 13 | impl Parse for FutureNotionAttr { 14 | fn parse(input: ParseStream) -> syn::Result { 15 | Ok(Self { 16 | name: input.parse()?, 17 | }) 18 | } 19 | } 20 | 21 | pub struct AsyncFnProps { 22 | input: Type, 23 | output: Type, 24 | with_state: bool, 25 | vis: Visibility, 26 | name: Ident, 27 | generics: Generics, 28 | } 29 | 30 | impl AsyncFnProps { 31 | fn extract(item: &ItemFn) -> syn::Result { 32 | let vis = item.vis.clone(); 33 | let name = item.sig.ident.clone(); 34 | let generics = item.sig.generics.clone(); 35 | 36 | if item.sig.asyncness.is_none() { 37 | return Err(syn::Error::new_spanned( 38 | name, 39 | "future notions must be async functions", 40 | )); 41 | } 42 | 43 | let output = match item.sig.output { 44 | ReturnType::Default => { 45 | // Unit Type is Output. 46 | parse_quote! { () } 47 | } 48 | ReturnType::Type(_, ref ty) => *ty.clone(), 49 | }; 50 | 51 | let mut fn_args = item.sig.inputs.iter(); 52 | 53 | let (input_arg, with_state) = match (fn_args.next(), fn_args.next()) { 54 | (Some(_), Some(n)) => (n.clone(), true), 55 | (Some(m), None) => (m.clone(), false), 56 | _ => { 57 | return Err(syn::Error::new_spanned( 58 | item.sig.inputs.clone(), 59 | "future notions must accept at least 1 argument", 60 | )) 61 | } 62 | }; 63 | 64 | let input_type = match input_arg { 65 | FnArg::Receiver(_) => { 66 | return Err(syn::Error::new_spanned( 67 | item.sig.inputs.clone(), 68 | "future notions do not accept self argument", 69 | )) 70 | } 71 | FnArg::Typed(m) => m, 72 | } 73 | .ty; 74 | 75 | let input = match *input_type { 76 | Type::Reference(m) => *m.elem, 77 | arg => return Err(syn::Error::new_spanned(arg, "input must be a reference")), 78 | }; 79 | Ok(Self { 80 | input, 81 | output, 82 | with_state, 83 | vis, 84 | name, 85 | generics, 86 | }) 87 | } 88 | } 89 | 90 | pub(crate) fn macro_fn(attr: FutureNotionAttr, mut item: ItemFn) -> TokenStream { 91 | let async_fn_props = match AsyncFnProps::extract(&item) { 92 | Ok(m) => m, 93 | Err(e) => return e.into_compile_error(), 94 | }; 95 | 96 | let AsyncFnProps { 97 | input, 98 | output, 99 | with_state, 100 | vis, 101 | name: fn_name, 102 | generics, 103 | } = async_fn_props; 104 | 105 | let (notion_name, fn_name) = match attr.name { 106 | Some(m) => (m, fn_name), 107 | None => (fn_name, Ident::new("inner", Span::mixed_site())), 108 | }; 109 | 110 | if notion_name == fn_name { 111 | return syn::Error::new_spanned( 112 | item.sig.ident, 113 | "notions must not have the same name as the function", 114 | ) 115 | .into_compile_error(); 116 | } 117 | 118 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 119 | let fn_generics = ty_generics.as_turbofish(); 120 | 121 | let fn_call = if with_state { 122 | quote! { 123 | #fn_name #fn_generics(states, input) 124 | } 125 | } else { 126 | quote! { 127 | #fn_name #fn_generics(input) 128 | } 129 | }; 130 | 131 | item.sig.ident = fn_name; 132 | 133 | let phantom_generics = generics 134 | .type_params() 135 | .map(|ty_param| ty_param.ident.clone()) 136 | .collect::>(); 137 | 138 | quote! { 139 | 140 | #vis struct #notion_name #generics { 141 | _marker: ::std::marker::PhantomData<(#phantom_generics)> 142 | } 143 | 144 | #[automatically_derived] 145 | impl #impl_generics ::bounce::FutureNotion for #notion_name #ty_generics #where_clause { 146 | type Input = #input; 147 | type Output = #output; 148 | 149 | fn run<'a>( 150 | states: &'a ::bounce::BounceStates, 151 | input: &'a #input, 152 | ) -> ::bounce::__vendored::futures::future::LocalBoxFuture<'a, #output> { 153 | #item 154 | 155 | ::std::boxed::Box::pin(#fn_call) 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /crates/bounce-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use proc_macro_error::proc_macro_error; 3 | use syn::{parse_macro_input, DeriveInput, ItemFn}; 4 | 5 | mod atom; 6 | mod future_notion; 7 | mod slice; 8 | 9 | #[proc_macro_derive(Atom, attributes(bounce))] 10 | #[proc_macro_error] 11 | pub fn atom(input: TokenStream) -> TokenStream { 12 | let input = parse_macro_input!(input as DeriveInput); 13 | atom::macro_fn(input).into() 14 | } 15 | 16 | #[proc_macro_derive(Slice, attributes(bounce))] 17 | #[proc_macro_error] 18 | pub fn slice(input: TokenStream) -> TokenStream { 19 | let input = parse_macro_input!(input as DeriveInput); 20 | slice::macro_fn(input).into() 21 | } 22 | 23 | #[proc_macro_attribute] 24 | pub fn future_notion(attr: TokenStream, item: TokenStream) -> TokenStream { 25 | let item = parse_macro_input!(item as ItemFn); 26 | let attr = parse_macro_input!(attr as future_notion::FutureNotionAttr); 27 | 28 | future_notion::macro_fn(attr, item).into() 29 | } 30 | -------------------------------------------------------------------------------- /crates/bounce-macros/src/slice.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::quote; 3 | use syn::parse::discouraged::Speculative; 4 | use syn::parse::{Parse, ParseBuffer, ParseStream}; 5 | use syn::punctuated::Punctuated; 6 | use syn::token::Comma; 7 | use syn::{parenthesized, Attribute, DeriveInput, Ident, Type}; 8 | 9 | pub(crate) struct WithNotionAttr { 10 | notion_idents: Vec, 11 | } 12 | 13 | impl WithNotionAttr { 14 | fn parse_parens_content(input: ParseStream<'_>) -> syn::Result> { 15 | let content; 16 | 17 | parenthesized!(content in input); 18 | 19 | Ok(content) 20 | } 21 | 22 | fn try_parse(input: ParseStream<'_>) -> syn::Result> { 23 | let ident = input.parse::()?; 24 | 25 | if ident != "with_notion" { 26 | return Ok(None); 27 | } 28 | 29 | let content = Self::parse_parens_content(input)?; 30 | 31 | let idents = Punctuated::::parse_terminated(&content)?; 32 | 33 | Ok(Some(Self { 34 | notion_idents: idents.into_iter().collect(), 35 | })) 36 | } 37 | } 38 | 39 | pub(crate) struct ObservedAttr { 40 | ident: Ident, 41 | } 42 | 43 | impl ObservedAttr { 44 | fn try_parse(input: ParseStream<'_>) -> syn::Result> { 45 | let ident = input.parse::()?; 46 | 47 | if ident != "observed" { 48 | return Ok(None); 49 | } 50 | 51 | Ok(Some(Self { ident })) 52 | } 53 | } 54 | 55 | pub(crate) enum BounceAttr { 56 | WithNotion(WithNotionAttr), 57 | Observed(ObservedAttr), 58 | } 59 | 60 | impl Parse for BounceAttr { 61 | fn parse(input: ParseStream<'_>) -> syn::Result { 62 | let forked_input = input.fork(); 63 | if let Some(m) = ObservedAttr::try_parse(&forked_input)? { 64 | input.advance_to(&forked_input); 65 | return Ok(Self::Observed(m)); 66 | } 67 | 68 | let forked_input = input.fork(); 69 | if let Some(m) = WithNotionAttr::try_parse(&forked_input)? { 70 | input.advance_to(&forked_input); 71 | return Ok(Self::WithNotion(m)); 72 | } 73 | 74 | Err(input.error("unknown attribute: expected either with_notion or observed")) 75 | } 76 | } 77 | 78 | #[derive(Default)] 79 | pub(crate) struct BounceAttrs { 80 | pub notions: Vec, 81 | pub observed: Option, 82 | } 83 | 84 | impl Parse for BounceAttrs { 85 | fn parse(input: ParseStream<'_>) -> syn::Result { 86 | let attrs = Punctuated::::parse_terminated(input)?; 87 | 88 | let mut this = Self::default(); 89 | 90 | for attr in attrs { 91 | match attr { 92 | BounceAttr::WithNotion(m) => { 93 | this.notions.push(m); 94 | } 95 | BounceAttr::Observed(m) => { 96 | if this.observed.is_some() { 97 | return Err(syn::Error::new_spanned( 98 | m.ident, 99 | "you can only have 1 observed attribute", 100 | )); 101 | } 102 | 103 | this.observed = Some(m); 104 | } 105 | } 106 | } 107 | 108 | Ok(this) 109 | } 110 | } 111 | 112 | impl BounceAttrs { 113 | pub fn parse_one(&mut self, attr: &Attribute) -> syn::Result<()> { 114 | if !attr.path().is_ident("bounce") { 115 | return Ok(()); 116 | } 117 | 118 | let other = attr.parse_args::()?; 119 | 120 | if let Some(m) = other.observed { 121 | if self.observed.is_some() { 122 | return Err(syn::Error::new_spanned( 123 | m.ident, 124 | "you can only have 1 observed attribute", 125 | )); 126 | } 127 | 128 | self.observed = Some(m); 129 | } 130 | 131 | self.notions.extend(other.notions); 132 | 133 | Ok(()) 134 | } 135 | 136 | pub fn parse(attrs: &[Attribute]) -> syn::Result { 137 | let mut this = Self::default(); 138 | 139 | for attr in attrs { 140 | this.parse_one(attr)?; 141 | } 142 | 143 | Ok(this) 144 | } 145 | 146 | pub fn notion_idents(&self) -> Vec { 147 | self.notions 148 | .iter() 149 | .flat_map(|m| m.notion_idents.clone()) 150 | .collect() 151 | } 152 | 153 | pub fn create_notion_apply_impls(&self, notion_ident: &Ident) -> Vec { 154 | let idents = self.notion_idents(); 155 | let mut notion_apply_impls = Vec::new(); 156 | 157 | for ident in idents { 158 | let notion_apply_impl = quote! { 159 | let #notion_ident = match <::std::rc::Rc::>::downcast::<#ident>(#notion_ident) { 160 | ::std::result::Result::Ok(m) => return ::bounce::WithNotion::<#ident>::apply(::std::clone::Clone::clone(&self), m), 161 | ::std::result::Result::Err(e) => e, 162 | }; 163 | }; 164 | 165 | notion_apply_impls.push(notion_apply_impl); 166 | } 167 | 168 | notion_apply_impls 169 | } 170 | 171 | pub fn create_notion_id_impls(&self) -> Vec { 172 | self.notion_idents() 173 | .iter() 174 | .map(|m| { 175 | quote! { ::std::any::TypeId::of::<#m>() } 176 | }) 177 | .collect() 178 | } 179 | } 180 | 181 | pub(crate) fn macro_fn(input: DeriveInput) -> TokenStream { 182 | let bounce_attrs = match BounceAttrs::parse(&input.attrs) { 183 | Ok(m) => m, 184 | Err(e) => return e.into_compile_error(), 185 | }; 186 | 187 | let notion_ident = Ident::new("notion", Span::mixed_site()); 188 | let notion_apply_impls = bounce_attrs.create_notion_apply_impls(¬ion_ident); 189 | let notion_ids_impls = bounce_attrs.create_notion_id_impls(); 190 | 191 | let type_ident = input.ident; 192 | 193 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 194 | 195 | let impl_observed = bounce_attrs.observed.is_some().then(|| { 196 | quote! { 197 | fn changed(self: ::std::rc::Rc) { 198 | ::bounce::Observed::changed(self); 199 | } 200 | } 201 | }); 202 | 203 | quote! { 204 | #[automatically_derived] 205 | impl #impl_generics ::bounce::Slice for #type_ident #ty_generics #where_clause { 206 | type Action = ::Action; 207 | 208 | fn reduce(self: ::std::rc::Rc, action: Self::Action) -> ::std::rc::Rc { 209 | ::bounce::__vendored::yew::functional::Reducible::reduce(self, action) 210 | } 211 | 212 | fn apply(self: ::std::rc::Rc, #notion_ident: ::std::rc::Rc) -> ::std::rc::Rc { 213 | #(#notion_apply_impls)* 214 | 215 | self 216 | } 217 | 218 | fn notion_ids(&self) -> ::std::vec::Vec<::std::any::TypeId> { 219 | ::std::vec![#(#notion_ids_impls,)*] 220 | } 221 | 222 | #impl_observed 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /crates/bounce/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bounce" 3 | version = "0.9.0" 4 | edition = "2021" 5 | repository = "https://github.com/bounce-rs/bounce" 6 | authors = ["Kaede Hoshiakwa "] 7 | description = "The uncomplicated state management library for Yew." 8 | keywords = ["web", "wasm", "yew", "state-management"] 9 | categories = ["wasm", "web-programming"] 10 | readme = "../../README.md" 11 | homepage = "https://github.com/bounce-rs/bounce" 12 | license = "MIT OR Apache-2.0" 13 | rust-version = "1.64.0" 14 | 15 | [dependencies] 16 | anymap2 = "0.13.0" 17 | once_cell = "1.18.0" 18 | wasm-bindgen = "0.2.87" 19 | yew = "0.21" 20 | bounce-macros = { path = "../bounce-macros", version = "0.9.0" } 21 | futures = "0.3.28" 22 | 23 | async-trait = { version = "0.1.68", optional = true } 24 | gloo = { version = "0.10.0", features = ["futures"], optional = true } 25 | html-escape = { version = "0.2.13", optional = true } 26 | serde = { version = "1.0.164", features = ["derive"] } 27 | tracing = "0.1" 28 | 29 | [dependencies.web-sys] 30 | version = "0.3.64" 31 | optional = true 32 | features = [ 33 | "Document", 34 | "HtmlScriptElement", 35 | "HtmlStyleElement", 36 | "DomTokenList", 37 | "HtmlLinkElement", 38 | "HtmlMetaElement", 39 | "HtmlBaseElement", 40 | "NodeList", 41 | ] 42 | 43 | [features] 44 | ssr = ["html-escape"] 45 | query = ["async-trait"] 46 | helmet = ["gloo", "web-sys"] 47 | 48 | [dev-dependencies] 49 | wasm-bindgen-test = "0.3.37" 50 | gloo = { version = "0.10.0", features = ["futures"] } 51 | yew = { version = "0.21", features = ["csr", "ssr"] } 52 | thiserror = "1" 53 | 54 | [dev-dependencies.web-sys] 55 | version = "0.3.64" 56 | features = ["HtmlInputElement"] 57 | 58 | [package.metadata.docs.rs] 59 | all-features = true 60 | rustdoc-args = ["--cfg", "documenting"] 61 | -------------------------------------------------------------------------------- /crates/bounce/src/any_state.rs: -------------------------------------------------------------------------------- 1 | use std::any::{Any, TypeId}; 2 | use std::rc::Rc; 3 | 4 | use anymap2::AnyMap; 5 | 6 | /// A common trait for all states. 7 | pub(crate) trait AnyState { 8 | /// Applies a notion. 9 | fn apply(&self, notion: Rc); 10 | 11 | /// Returns a list of notion ids that this state accepts. 12 | fn notion_ids(&self) -> Vec { 13 | Vec::new() 14 | } 15 | 16 | /// Creates a state from a possible initialise value. 17 | fn create(init_states: &mut AnyMap) -> Self 18 | where 19 | Self: Sized; 20 | } 21 | -------------------------------------------------------------------------------- /crates/bounce/src/helmet/comp.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::rc::Rc; 3 | use std::sync::Arc; 4 | 5 | use wasm_bindgen::throw_str; 6 | use yew::prelude::*; 7 | use yew::virtual_dom::{VNode, VTag}; 8 | 9 | use super::state::{HelmetState, HelmetTag}; 10 | use crate::states::artifact::Artifact; 11 | use crate::utils::Id; 12 | 13 | /// Properties for [Helmet]. 14 | #[derive(Properties, Debug, PartialEq)] 15 | pub struct HelmetProps { 16 | /// Children of the [Helmet] component. 17 | /// 18 | /// This property only accepts a list of elements denoted in the module documentation. 19 | #[prop_or_default] 20 | pub children: Children, 21 | } 22 | 23 | fn collect_str_in_children(tag: &VNode) -> String { 24 | match tag { 25 | VNode::VTag(_) => throw_str("expected text content, found tag."), 26 | VNode::VText(ref m) => m.text.to_string(), 27 | VNode::VList(ref m) => { 28 | let mut s = "".to_string(); 29 | 30 | for i in m.iter() { 31 | s.push_str(&collect_str_in_children(i)); 32 | } 33 | 34 | s 35 | } 36 | VNode::VComp(_) => throw_str("expected text content, found component."), 37 | VNode::VPortal(_) => throw_str("expected text content, found portal."), 38 | VNode::VRef(_) => throw_str("expected text content, found node reference."), 39 | VNode::VSuspense(_) => throw_str("expected text content, found suspense."), 40 | VNode::VRaw(_) => throw_str("expected text content, found raw html."), 41 | } 42 | } 43 | 44 | fn collect_text_content(tag: &VTag) -> String { 45 | if let Some(children) = tag.children() { 46 | collect_str_in_children(children) 47 | } else { 48 | String::default() 49 | } 50 | } 51 | 52 | fn collect_attributes(tag: &VTag) -> BTreeMap, Arc> { 53 | let mut map = BTreeMap::new(); 54 | 55 | for (k, v) in tag.attributes.iter() { 56 | map.insert(k.into(), v.into()); 57 | } 58 | 59 | map 60 | } 61 | 62 | fn assert_empty_node(node: &VNode) { 63 | match node { 64 | VNode::VTag(_) => throw_str("expected nothing, found tag."), 65 | VNode::VText(_) => throw_str("expected nothing, found text content."), 66 | VNode::VList(ref m) => { 67 | for node in m.iter() { 68 | assert_empty_node(node); 69 | } 70 | } 71 | VNode::VComp(_) => throw_str("expected nothing, found component."), 72 | VNode::VPortal(_) => throw_str("expected nothing, found portal."), 73 | VNode::VRef(_) => throw_str("expected nothing, found node reference."), 74 | VNode::VSuspense(_) => throw_str("expected nothing, found suspense."), 75 | VNode::VRaw(_) => throw_str("expected nothing, found raw html."), 76 | } 77 | } 78 | 79 | fn assert_empty_children(tag: &VTag) { 80 | if let Some(children) = tag.children() { 81 | assert_empty_node(children); 82 | } 83 | } 84 | 85 | #[derive(Properties, PartialEq, Clone)] 86 | struct ScriptHelmetProps { 87 | attrs: BTreeMap, Arc>, 88 | content: Arc, 89 | } 90 | 91 | // A special component to render the script tag with a unique id. 92 | #[function_component(ScriptHelmet)] 93 | fn script_helmet(props: &ScriptHelmetProps) -> Html { 94 | let id = *use_state(Id::new); 95 | let ScriptHelmetProps { attrs, content } = props.clone(); 96 | 97 | let tags = vec![Arc::new(HelmetTag::Script { 98 | attrs, 99 | content, 100 | _id: id, 101 | })]; 102 | let state = Rc::new(HelmetState { tags }); 103 | 104 | html! { value={state} />} 105 | } 106 | 107 | /// A component to register head elements. 108 | /// 109 | /// # Panics 110 | /// 111 | /// This component will panic if unsupported elements are passed as children. 112 | /// 113 | /// # Example 114 | /// 115 | /// ``` 116 | /// # use yew::prelude::*; 117 | /// # use bounce::BounceRoot; 118 | /// # use bounce::prelude::*; 119 | /// use bounce::helmet::Helmet; 120 | /// 121 | /// # #[function_component(Comp)] 122 | /// # fn comp() -> Html { 123 | /// html! { 124 | /// 125 | /// // The title to apply. 126 | /// {"page a title"} 127 | /// 128 | /// } 129 | /// # } 130 | /// ``` 131 | #[function_component(Helmet)] 132 | pub fn helmet(props: &HelmetProps) -> Html { 133 | let mut script_helmets = Vec::new(); 134 | 135 | let tags = props 136 | .children 137 | .clone() 138 | .into_iter() 139 | .filter_map(|m| match m { 140 | VNode::VTag(m) => match m.tag() { 141 | "title" => Some(HelmetTag::Title(collect_text_content(&m).into()).into()), 142 | 143 | "script" => { 144 | let attrs = collect_attributes(&m); 145 | let content: Arc = collect_text_content(&m).into(); 146 | 147 | script_helmets.push(html! { }); 148 | 149 | None 150 | } 151 | "style" => { 152 | let attrs = collect_attributes(&m); 153 | let content: Arc = collect_text_content(&m).into(); 154 | 155 | Some(HelmetTag::Style { attrs, content }.into()) 156 | } 157 | 158 | "html" => { 159 | assert_empty_children(&m); 160 | let attrs = collect_attributes(&m); 161 | 162 | Some(HelmetTag::Html { attrs }.into()) 163 | } 164 | "body" => { 165 | assert_empty_children(&m); 166 | let attrs = collect_attributes(&m); 167 | 168 | Some(HelmetTag::Body { attrs }.into()) 169 | } 170 | 171 | "base" => { 172 | assert_empty_children(&m); 173 | let attrs = collect_attributes(&m); 174 | 175 | Some(HelmetTag::Base { attrs }.into()) 176 | } 177 | "link" => { 178 | assert_empty_children(&m); 179 | let attrs = collect_attributes(&m); 180 | 181 | Some(HelmetTag::Link { attrs }.into()) 182 | } 183 | "meta" => { 184 | assert_empty_children(&m); 185 | let attrs = collect_attributes(&m); 186 | 187 | Some(HelmetTag::Meta { attrs }.into()) 188 | } 189 | _ => throw_str(&format!("unsupported helmet tag type: {}", m.tag())), 190 | }, 191 | _ => throw_str("unsupported helmet node type, expect a supported helmet tag."), 192 | }) 193 | .collect::>>(); 194 | 195 | let state = Rc::new(HelmetState { tags }); 196 | 197 | html! { 198 | <> 199 | value={state} /> 200 | {script_helmets} 201 | 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /crates/bounce/src/helmet/mod.rs: -------------------------------------------------------------------------------- 1 | //! A module to manipulate common tags under the `` element. 2 | //! 3 | //! The Helmet component supports the following elements: 4 | //! 5 | //! - `title` 6 | //! - `style` 7 | //! - `script` 8 | //! - `base` 9 | //! - `link` 10 | //! - `meta` 11 | //! 12 | //! The Helmet component supports setting attributes of the following elements: 13 | //! 14 | //! - `html` 15 | //! - `body` 16 | //! 17 | //! # Example 18 | //! 19 | //! ``` 20 | //! # use yew::prelude::*; 21 | //! # use bounce::BounceRoot; 22 | //! # use bounce::prelude::*; 23 | //! use bounce::helmet::{Helmet, HelmetBridge}; 24 | //! 25 | //! #[function_component(PageA)] 26 | //! fn page_a() -> Html { 27 | //! html! { 28 | //! <> 29 | //! 30 | //! // The title to apply. 31 | //! {"page a title"} 32 | //! 33 | //!
{"This is page A."}
34 | //! 35 | //! } 36 | //! } 37 | //! 38 | //! #[function_component(App)] 39 | //! fn app() -> Html { 40 | //! html! { 41 | //! 42 | //! // A helmet bridge is required to apply helmet elements to the head element. 43 | //! // You only need 1 helmet bridge per bounce root. 44 | //! // The helmet bridge is intended to live as long as the BounceRoot. 45 | //! 46 | //! 47 | //! // The title to apply. 48 | //! // 49 | //! // However, as also renders a title element, elements rendered later 50 | //! // will have a higher priority. Hence, "page a title" will become the document 51 | //! // title. 52 | //! {"app title"} 53 | //! 54 | //! 55 | //! 56 | //! } 57 | //! } 58 | //! ``` 59 | //! 60 | //! Bounce Helmet also supports [Server-side rendering](render_static). 61 | 62 | use yew::prelude::*; 63 | 64 | mod bridge; 65 | mod comp; 66 | #[cfg(feature = "ssr")] 67 | mod ssr; 68 | mod state; 69 | 70 | pub use bridge::{HelmetBridge, HelmetBridgeProps}; 71 | pub use comp::{Helmet, HelmetProps}; 72 | #[cfg(feature = "ssr")] 73 | pub(crate) use ssr::StaticWriterState; 74 | #[cfg(feature = "ssr")] 75 | #[cfg_attr(documenting, doc(cfg(feature = "ssr")))] 76 | pub use ssr::{render_static, StaticRenderer, StaticWriter}; 77 | pub use state::HelmetTag; 78 | 79 | type FormatTitle = Callback; 80 | -------------------------------------------------------------------------------- /crates/bounce/src/helmet/ssr.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::fmt; 3 | use std::fmt::Write; 4 | use std::iter; 5 | use std::sync::{Arc, Mutex}; 6 | 7 | // The static renderer can run outside of the Yew runtime. 8 | // We use a send oneshot channel for this purpose. 9 | use futures::channel::oneshot as sync_oneshot; 10 | 11 | use crate::root_state::BounceStates; 12 | use crate::Atom; 13 | 14 | use super::state::{merge_helmet_states, HelmetState, HelmetTag}; 15 | use super::FormatTitle; 16 | 17 | use yew::prelude::*; 18 | 19 | pub struct StaticWriterInner { 20 | tx: sync_oneshot::Sender>, 21 | } 22 | 23 | /// The writer of [StaticRenderer]. 24 | /// 25 | /// This writer is passed to a `` for tags to be rendered with it. 26 | #[derive(Clone)] 27 | pub struct StaticWriter { 28 | inner: Arc>>, 29 | } 30 | 31 | impl PartialEq for StaticWriter { 32 | fn eq(&self, other: &Self) -> bool { 33 | Arc::ptr_eq(&self.inner, &other.inner) 34 | } 35 | } 36 | 37 | impl Eq for StaticWriter {} 38 | 39 | impl fmt::Debug for StaticWriter { 40 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 41 | f.debug_struct("StaticWriter").field("inner", &"_").finish() 42 | } 43 | } 44 | 45 | impl StaticWriter { 46 | pub(crate) fn send_helmet( 47 | &self, 48 | states: BounceStates, 49 | format_title: Option, 50 | default_title: Option, 51 | ) { 52 | let StaticWriterInner { tx } = match self.inner.lock().unwrap().take() { 53 | Some(m) => m, 54 | None => return, 55 | }; 56 | 57 | let helmet_states = states.get_artifacts::(); 58 | let tags = merge_helmet_states(&helmet_states, format_title.as_ref(), default_title); 59 | 60 | // We ignore cases where the StaticRenderer is dropped. 61 | let _ = tx.send( 62 | tags.into_iter() 63 | .map(|m| Arc::try_unwrap(m).unwrap_or_else(|e| (*e).clone())) 64 | .collect::>(), 65 | ); 66 | } 67 | } 68 | 69 | /// A Helmet Static Renderer. 70 | /// 71 | /// This renderer provides support to statically render helmet tags to string to be prefixed to a 72 | /// server-side rendered artifact. 73 | #[derive(Debug)] 74 | pub struct StaticRenderer { 75 | rx: sync_oneshot::Receiver>, 76 | } 77 | 78 | impl StaticRenderer { 79 | /// Renders the helmet tags collected in the current renderer. 80 | /// 81 | /// # Notes 82 | /// 83 | /// For applications using streamed server-side rendering, the renderer will discard any tags 84 | /// rendered after this method is called. 85 | pub async fn render(self) -> Vec { 86 | self.rx.await.expect("failed to receive value.") 87 | } 88 | } 89 | 90 | impl HelmetTag { 91 | fn write_attrs_from( 92 | w: &mut dyn Write, 93 | attrs: &BTreeMap, Arc>, 94 | write_data_attr: bool, 95 | ) -> fmt::Result { 96 | let mut data_tag_written = false; 97 | 98 | for (index, (name, value)) in attrs 99 | .iter() 100 | .map(|(name, value)| (&**name, &**value)) 101 | .chain(iter::from_fn(|| { 102 | (write_data_attr && !data_tag_written).then(|| { 103 | data_tag_written = true; 104 | ("data-bounce-helmet", "pre-render") 105 | }) 106 | })) 107 | .enumerate() 108 | { 109 | if index > 0 { 110 | write!(w, " ")?; 111 | } 112 | 113 | write!( 114 | w, 115 | r#"{}="{}""#, 116 | name, 117 | html_escape::decode_script_double_quoted_text(value) 118 | )?; 119 | } 120 | 121 | Ok(()) 122 | } 123 | 124 | /// Writes the attributes of the current tag into a `std::fmt::Write`. 125 | /// 126 | /// You can use this method to write attributes to the `` or `` tag. 127 | pub fn write_attrs(&self, w: &mut dyn Write) -> fmt::Result { 128 | match self { 129 | Self::Title(_) => Ok(()), 130 | Self::Body { attrs } | Self::Html { attrs } => Self::write_attrs_from(w, attrs, false), 131 | Self::Meta { attrs } 132 | | Self::Link { attrs } 133 | | Self::Script { attrs, .. } 134 | | Self::Style { attrs, .. } 135 | | Self::Base { attrs } => Self::write_attrs_from(w, attrs, true), 136 | } 137 | } 138 | 139 | /// Writes the content of a tag into a `std::fmt::Write`. 140 | /// 141 | /// `` and `` tags are not written. 142 | /// 143 | /// To write attributes for html and body tags, 144 | /// you can use the [`write_attrs`](Self::write_attrs) method instead. 145 | pub fn write_static(&self, w: &mut dyn Write) -> fmt::Result { 146 | match self { 147 | Self::Title(m) => { 148 | write!(w, "{m}") 149 | } 150 | Self::Script { content, attrs, .. } => { 151 | write!(w, "") 154 | } 155 | Self::Style { content, attrs } => { 156 | write!(w, "") 159 | } 160 | Self::Body { .. } => Ok(()), 161 | Self::Html { .. } => Ok(()), 162 | Self::Base { attrs } => { 163 | write!(w, "") 166 | } 167 | Self::Link { attrs } => { 168 | write!(w, "") 171 | } 172 | Self::Meta { attrs } => { 173 | write!(w, "") 176 | } 177 | } 178 | } 179 | } 180 | 181 | #[derive(Atom, PartialEq, Default)] 182 | pub(crate) struct StaticWriterState { 183 | pub writer: Option, 184 | pub default_title: Option, 185 | pub format_title: Option, 186 | } 187 | 188 | /// Creates a new Static Renderer - Static Writer pair. 189 | /// 190 | /// This function creates a `StaticRenderer` and a `StaticWriter`. 191 | /// You can pass the `StaticWriter` to the `writer` props of a `HelmetBridge`. 192 | /// After the body is rendered, helmet tags can be read by calling `StaticRenderer.render()`. 193 | /// 194 | /// # Example 195 | /// 196 | /// ``` 197 | /// # use yew::prelude::*; 198 | /// # use bounce::BounceRoot; 199 | /// # use bounce::helmet::{StaticWriter, HelmetBridge, Helmet, render_static}; 200 | /// #[derive(Properties, PartialEq, Eq)] 201 | /// pub struct ServerAppProps { 202 | /// pub helmet_writer: StaticWriter, 203 | /// } 204 | /// 205 | /// #[function_component] 206 | /// pub fn ServerApp(props: &ServerAppProps) -> Html { 207 | /// html! { 208 | /// 209 | /// 213 | /// 214 | /// 215 | /// 216 | /// 217 | /// } 218 | /// } 219 | /// 220 | /// # async fn function() { 221 | /// let (helmet_renderer, helmet_writer) = render_static(); 222 | /// let rendered_body = yew::ServerRenderer::::with_props( 223 | /// move || ServerAppProps { helmet_writer } 224 | /// ) 225 | /// .render().await; 226 | /// let rendered_helmet_tags = helmet_renderer.render().await; 227 | /// let mut rendered_head = String::new(); 228 | /// for t in rendered_helmet_tags { 229 | /// t.write_static(&mut rendered_head).unwrap(); 230 | /// } 231 | /// 232 | /// assert_eq!( 233 | /// rendered_head, 234 | /// r#""# 235 | /// ); 236 | /// # } 237 | /// ``` 238 | pub fn render_static() -> (StaticRenderer, StaticWriter) { 239 | let (tx, rx) = sync_oneshot::channel(); 240 | 241 | ( 242 | StaticRenderer { rx }, 243 | StaticWriter { 244 | inner: Arc::new(Mutex::new(Some(StaticWriterInner { tx }))), 245 | }, 246 | ) 247 | } 248 | -------------------------------------------------------------------------------- /crates/bounce/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The uncomplicated Yew State management library. 2 | 3 | #![deny(clippy::all)] 4 | #![deny(missing_debug_implementations)] 5 | #![deny(unsafe_code)] 6 | #![deny(non_snake_case)] 7 | #![deny(clippy::cognitive_complexity)] 8 | #![deny(missing_docs)] 9 | #![cfg_attr(documenting, feature(doc_cfg))] 10 | #![cfg_attr(documenting, feature(doc_auto_cfg))] 11 | #![cfg_attr(any(releasing, not(debug_assertions)), deny(dead_code, unused_imports))] 12 | 13 | extern crate self as bounce; 14 | 15 | mod any_state; 16 | mod provider; 17 | mod root_state; 18 | mod states; 19 | mod utils; 20 | 21 | #[cfg_attr(documenting, doc(cfg(feature = "query")))] 22 | #[cfg(feature = "query")] 23 | pub mod query; 24 | 25 | #[cfg_attr(documenting, doc(cfg(feature = "helmet")))] 26 | #[cfg(feature = "helmet")] 27 | pub mod helmet; 28 | 29 | /// A simple state that is Copy-on-Write and notifies registered hooks when `prev_value != next_value`. 30 | /// 31 | /// It can be derived for any state that implements [`PartialEq`] + [`Default`]. 32 | /// 33 | /// # Example 34 | /// 35 | /// ``` 36 | /// use std::rc::Rc; 37 | /// use bounce::prelude::*; 38 | /// use yew::prelude::*; 39 | /// 40 | /// #[derive(PartialEq, Atom)] 41 | /// struct Username { 42 | /// inner: String, 43 | /// } 44 | /// 45 | /// impl Default for Username { 46 | /// fn default() -> Self { 47 | /// Self { 48 | /// inner: "Jane Doe".into(), 49 | /// } 50 | /// } 51 | /// } 52 | /// ``` 53 | /// See: [`use_atom`](crate::use_atom) 54 | pub use states::atom::Atom; 55 | 56 | /// A reducer-based state that is Copy-on-Write and notifies registered hooks when `prev_value != next_value`. 57 | /// 58 | /// It can be derived for any state that implements [`Reducible`](yew::functional::Reducible) + [`PartialEq`] + [`Default`]. 59 | /// 60 | /// # Example 61 | /// 62 | /// ``` 63 | /// use std::rc::Rc; 64 | /// use bounce::prelude::*; 65 | /// use yew::prelude::*; 66 | /// 67 | /// enum CounterAction { 68 | /// Increment, 69 | /// Decrement, 70 | /// } 71 | /// 72 | /// #[derive(PartialEq, Default, Slice)] 73 | /// struct Counter(u64); 74 | /// 75 | /// impl Reducible for Counter { 76 | /// type Action = CounterAction; 77 | /// 78 | /// fn reduce(self: Rc, action: Self::Action) -> Rc { 79 | /// match action { 80 | /// CounterAction::Increment => Self(self.0 + 1).into(), 81 | /// CounterAction::Decrement => Self(self.0 - 1).into(), 82 | /// } 83 | /// } 84 | /// } 85 | /// ``` 86 | /// See: [`use_slice`](crate::use_slice) 87 | pub use states::slice::Slice; 88 | 89 | /// A future-based notion that notifies states when it begins and finishes. 90 | /// 91 | /// A future notion accepts a signle argument as input and returns an output. 92 | /// 93 | /// It can optionally accept a `states` parameter which has a type of [`BounceStates`] that can be 94 | /// used to access bounce states when being run. 95 | /// 96 | /// The async function must have a signature of either 97 | /// `Fn(&I) -> impl Future` or `Fn(&BounceState, &I) -> impl Future`. 98 | /// 99 | /// Both `Input` and `Output` must live `'static`. 100 | /// 101 | /// # Example 102 | /// 103 | /// ``` 104 | /// use std::rc::Rc; 105 | /// use bounce::prelude::*; 106 | /// use yew::prelude::*; 107 | /// 108 | /// struct User { 109 | /// id: u64, 110 | /// name: String, 111 | /// } 112 | /// 113 | /// #[future_notion(FetchData)] 114 | /// async fn fetch_user(id: &u64) -> User { 115 | /// // fetch user 116 | /// 117 | /// User { id: *id, name: "John Smith".into() } 118 | /// } 119 | /// ``` 120 | /// See: [`use_future_notion_runner`](crate::use_future_notion_runner) 121 | pub use bounce_macros::future_notion; 122 | 123 | pub use provider::{BounceRoot, BounceRootProps}; 124 | pub use root_state::BounceStates; 125 | 126 | pub use states::artifact::{use_artifacts, Artifact, ArtifactProps}; 127 | pub use states::atom::{use_atom, use_atom_setter, use_atom_value, CloneAtom, UseAtomHandle}; 128 | pub use states::future_notion::{use_future_notion_runner, Deferred, FutureNotion}; 129 | pub use states::input_selector::{use_input_selector_value, InputSelector}; 130 | pub use states::notion::{use_notion_applier, WithNotion}; 131 | pub use states::observer::Observed; 132 | pub use states::selector::{use_selector_value, Selector}; 133 | pub use states::slice::{ 134 | use_slice, use_slice_dispatch, use_slice_value, CloneSlice, UseSliceHandle, 135 | }; 136 | 137 | pub mod prelude { 138 | //! Default Bounce exports. 139 | 140 | pub use crate::future_notion; 141 | pub use crate::BounceStates; 142 | pub use crate::Observed; 143 | pub use crate::{use_artifacts, Artifact, ArtifactProps}; 144 | pub use crate::{use_atom, use_atom_setter, use_atom_value, Atom, CloneAtom, UseAtomHandle}; 145 | pub use crate::{use_future_notion_runner, Deferred, FutureNotion}; 146 | pub use crate::{use_input_selector_value, InputSelector}; 147 | pub use crate::{use_notion_applier, WithNotion}; 148 | pub use crate::{use_selector_value, Selector}; 149 | pub use crate::{ 150 | use_slice, use_slice_dispatch, use_slice_value, CloneSlice, Slice, UseSliceHandle, 151 | }; 152 | } 153 | 154 | // vendored dependencies used by macros. 155 | #[doc(hidden)] 156 | pub mod __vendored { 157 | pub use futures; 158 | pub use once_cell; 159 | pub use yew; 160 | } 161 | -------------------------------------------------------------------------------- /crates/bounce/src/provider.rs: -------------------------------------------------------------------------------- 1 | use anymap2::AnyMap; 2 | use yew::prelude::*; 3 | 4 | use crate::root_state::BounceRootState; 5 | 6 | /// Properties for [`BounceRoot`]. 7 | #[derive(Properties, Debug, PartialEq, Clone)] 8 | pub struct BounceRootProps { 9 | /// Children of a Bounce Root. 10 | #[prop_or_default] 11 | pub children: Children, 12 | 13 | /// A callback that retrieves an `AnyMap` that contains initial states. 14 | /// 15 | /// States not provided will use `Default`. 16 | /// 17 | /// This only affects [`Atom`](macro@crate::Atom) and [`Slice`](macro@crate::Slice). 18 | #[prop_or_default] 19 | pub get_init_states: Option>, 20 | } 21 | 22 | /// A ``. 23 | /// 24 | /// For bounce states to function, A `` must present and registered as a context 25 | /// provider. 26 | /// 27 | /// # Example 28 | /// 29 | /// ``` 30 | /// # use yew::prelude::*; 31 | /// # use bounce::prelude::*; 32 | /// # use bounce::BounceRoot; 33 | /// #[function_component(App)] 34 | /// fn app() -> Html { 35 | /// html! { 36 | /// 37 | /// // children... 38 | /// 39 | /// } 40 | /// } 41 | /// 42 | /// ``` 43 | #[function_component(BounceRoot)] 44 | pub fn bounce_root(props: &BounceRootProps) -> Html { 45 | let BounceRootProps { 46 | children, 47 | get_init_states, 48 | } = props.clone(); 49 | 50 | let root_state = (*use_state(move || { 51 | let init_states = get_init_states.map(|m| m.emit(())).unwrap_or_default(); 52 | BounceRootState::new(init_states) 53 | })) 54 | .clone(); 55 | 56 | #[allow(clippy::redundant_clone)] 57 | { 58 | let root_state = root_state.clone(); 59 | use_effect_with((), move |_| { 60 | // We clear all states manually. 61 | move || { 62 | root_state.clear(); 63 | } 64 | }); 65 | } 66 | 67 | #[allow(clippy::unused_unit, clippy::redundant_clone)] 68 | { 69 | let _root_state = root_state.clone(); 70 | let _ = use_transitive_state!((), move |_| -> () { 71 | #[cfg(feature = "ssr")] 72 | #[cfg(feature = "helmet")] 73 | { 74 | // Workaround to send helmet states back to static writer 75 | use crate::helmet::StaticWriterState; 76 | 77 | let states = _root_state.states(); 78 | let writer_state = states.get_atom_value::(); 79 | 80 | if let Some(ref w) = writer_state.writer { 81 | w.send_helmet( 82 | states, 83 | writer_state.format_title.clone(), 84 | writer_state.default_title.clone(), 85 | ); 86 | } 87 | } 88 | 89 | // We drop the root state on SSR as well. 90 | _root_state.clear(); 91 | }); 92 | } 93 | 94 | html! { 95 | context={root_state}>{children}> 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crates/bounce/src/query/mod.rs: -------------------------------------------------------------------------------- 1 | //! A module to provide helper states to facilitate data fetching. 2 | //! 3 | //! It provides hook-based access to APIs with automatic caching and request deduplication backed 4 | //! by Bounce's state management mechanism. 5 | //! 6 | //! This module is inspired by [RTK Query](https://redux-toolkit.js.org/rtk-query/overview). 7 | //! 8 | //! There are two methods to interact with APIs: [Query](use_query()) and 9 | //! [Mutation](use_mutation()) 10 | //! 11 | //! # Note 12 | //! 13 | //! Bounce does not provide an implementation of HTTP Client. 14 | //! 15 | //! You can use reqwest or gloo-net if you need a generic HTTP Client. 16 | //! 17 | //! If your backend is GraphQL, you can use graphql-client in conjunction with reqwest. 18 | 19 | mod mutation_states; 20 | mod query_states; 21 | mod traits; 22 | mod use_mutation; 23 | mod use_prepared_query; 24 | mod use_query; 25 | mod use_query_value; 26 | 27 | pub use traits::{Mutation, MutationResult, Query, QueryResult}; 28 | pub use use_mutation::{use_mutation, MutationState, UseMutationHandle}; 29 | pub use use_prepared_query::use_prepared_query; 30 | pub use use_query::{use_query, QueryState, UseQueryHandle}; 31 | pub use use_query_value::{use_query_value, QueryValueState, UseQueryValueHandle}; 32 | -------------------------------------------------------------------------------- /crates/bounce/src/query/traits.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use std::hash::Hash; 3 | use std::rc::Rc; 4 | 5 | use crate::root_state::BounceStates; 6 | 7 | /// A Result returned by queries. 8 | pub type QueryResult = std::result::Result, ::Error>; 9 | 10 | /// A trait to be implemented on queries. 11 | /// 12 | /// # Note 13 | /// 14 | /// This trait is implemented with [async_trait](macro@async_trait), you should apply an `#[async_trait(?Send)]` 15 | /// attribute to your implementation of this trait. 16 | /// 17 | /// # Example 18 | /// 19 | /// ``` 20 | /// use std::rc::Rc; 21 | /// use std::convert::Infallible; 22 | /// use bounce::prelude::*; 23 | /// use bounce::query::{Query, QueryResult}; 24 | /// use yew::prelude::*; 25 | /// use async_trait::async_trait; 26 | /// 27 | /// #[derive(Debug, PartialEq)] 28 | /// struct User { 29 | /// id: u64, 30 | /// name: String, 31 | /// } 32 | /// 33 | /// #[derive(Debug, PartialEq)] 34 | /// struct UserQuery { 35 | /// value: User 36 | /// } 37 | /// 38 | /// #[async_trait(?Send)] 39 | /// impl Query for UserQuery { 40 | /// type Input = u64; 41 | /// type Error = Infallible; 42 | /// 43 | /// async fn query(_states: &BounceStates, input: Rc) -> QueryResult { 44 | /// // fetch user 45 | /// 46 | /// Ok(UserQuery{ value: User { id: *input, name: "John Smith".into() } }.into()) 47 | /// } 48 | /// } 49 | /// ``` 50 | /// 51 | /// See: [`use_query`](super::use_query()) and [`use_query_value`](super::use_query_value()) 52 | #[async_trait(?Send)] 53 | pub trait Query: PartialEq { 54 | /// The Input type of a query. 55 | /// 56 | /// The input type must implement Hash and Eq as it is used as the key of results in a 57 | /// HashMap. 58 | type Input: Hash + Eq + 'static; 59 | 60 | /// The Error type of a query. 61 | type Error: 'static + std::error::Error + PartialEq + Clone; 62 | 63 | /// Runs a query. 64 | /// 65 | /// This method will only be called when the result is not already cached. 66 | /// 67 | /// # Note 68 | /// 69 | /// When implementing this method with async_trait, you can use the following function 70 | /// signature: 71 | /// 72 | /// ```ignore 73 | /// async fn query(states: &BounceStates, input: Rc) -> QueryResult 74 | /// ``` 75 | async fn query(states: &BounceStates, input: Rc) -> QueryResult; 76 | } 77 | 78 | /// A Result returned by mutations. 79 | pub type MutationResult = std::result::Result, ::Error>; 80 | 81 | /// A trait to be implemented on mutations. 82 | /// 83 | /// # Note 84 | /// 85 | /// This trait is implemented with [async_trait](macro@async_trait), you should apply an `#[async_trait(?Send)]` 86 | /// attribute to your implementation of this trait. 87 | /// 88 | /// # Example 89 | /// 90 | /// ``` 91 | /// use std::rc::Rc; 92 | /// use std::convert::Infallible; 93 | /// use bounce::prelude::*; 94 | /// use bounce::query::{Mutation, MutationResult}; 95 | /// use yew::prelude::*; 96 | /// use async_trait::async_trait; 97 | /// 98 | /// #[derive(Debug, PartialEq)] 99 | /// struct User { 100 | /// id: u64, 101 | /// name: String, 102 | /// } 103 | /// 104 | /// #[derive(Debug, PartialEq)] 105 | /// struct UpdateUserMutation { 106 | /// } 107 | /// 108 | /// #[async_trait(?Send)] 109 | /// impl Mutation for UpdateUserMutation { 110 | /// type Input = User; 111 | /// type Error = Infallible; 112 | /// 113 | /// async fn run(_states: &BounceStates, _input: Rc) -> MutationResult { 114 | /// // updates the user information. 115 | /// 116 | /// Ok(UpdateUserMutation {}.into()) 117 | /// } 118 | /// } 119 | /// ``` 120 | /// 121 | /// See: [`use_mutation`](super::use_mutation()) 122 | #[async_trait(?Send)] 123 | pub trait Mutation: PartialEq { 124 | /// The Input type. 125 | type Input: 'static; 126 | 127 | /// The Error type. 128 | type Error: 'static + std::error::Error + PartialEq + Clone; 129 | 130 | /// Runs a mutation. 131 | /// 132 | /// # Note 133 | /// 134 | /// When implementing this method with async_trait, you can use the following function 135 | /// signature: 136 | /// 137 | /// ```ignore 138 | /// async fn run(states: &BounceStates, input: Rc) -> MutationResult 139 | /// ``` 140 | async fn run(states: &BounceStates, input: Rc) -> MutationResult; 141 | } 142 | -------------------------------------------------------------------------------- /crates/bounce/src/query/use_mutation.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::marker::PhantomData; 3 | use std::rc::Rc; 4 | 5 | use yew::platform::pinned::oneshot; 6 | use yew::prelude::*; 7 | 8 | use super::traits::{Mutation, MutationResult}; 9 | use crate::states::future_notion::{use_future_notion_runner, FutureNotion}; 10 | use crate::states::input_selector::use_input_selector_value; 11 | use crate::states::slice::use_slice_dispatch; 12 | 13 | use super::mutation_states::{ 14 | HandleId, MutationId, MutationSelector, MutationSlice, MutationSliceAction, MutationSliceValue, 15 | RunMutation, RunMutationInput, 16 | }; 17 | 18 | /// Mutation State 19 | #[derive(Debug, PartialEq)] 20 | pub enum MutationState 21 | where 22 | T: Mutation + 'static, 23 | { 24 | /// The mutation has not started yet. 25 | Idle, 26 | /// The mutation is loading. 27 | Loading, 28 | /// The mutation has completed. 29 | Completed { 30 | /// Result of the completed mutation. 31 | result: MutationResult, 32 | }, 33 | /// A previous mutation has completed and a new mutation is currently loading. 34 | Refreshing { 35 | /// Result of last completed mutation. 36 | last_result: MutationResult, 37 | }, 38 | } 39 | 40 | impl Clone for MutationState 41 | where 42 | T: Mutation + 'static, 43 | { 44 | fn clone(&self) -> Self { 45 | match self { 46 | Self::Idle => Self::Idle, 47 | Self::Loading => Self::Loading, 48 | Self::Completed { result } => Self::Completed { 49 | result: result.clone(), 50 | }, 51 | Self::Refreshing { last_result } => Self::Refreshing { 52 | last_result: last_result.clone(), 53 | }, 54 | } 55 | } 56 | } 57 | 58 | impl PartialEq<&MutationState> for MutationState 59 | where 60 | T: Mutation + 'static, 61 | { 62 | fn eq(&self, other: &&MutationState) -> bool { 63 | self == *other 64 | } 65 | } 66 | 67 | impl PartialEq> for &'_ MutationState 68 | where 69 | T: Mutation + 'static, 70 | { 71 | fn eq(&self, other: &MutationState) -> bool { 72 | *self == other 73 | } 74 | } 75 | 76 | /// A handle returned by [`use_mutation`]. 77 | pub struct UseMutationHandle 78 | where 79 | T: Mutation + 'static, 80 | { 81 | id: HandleId, 82 | state: Rc>, 83 | run_mutation: Rc as FutureNotion>::Input)>, 84 | _marker: PhantomData, 85 | } 86 | 87 | impl UseMutationHandle 88 | where 89 | T: Mutation + 'static, 90 | { 91 | /// Returns the state of current mutation. 92 | pub fn state(&self) -> &MutationState { 93 | self.state.as_ref() 94 | } 95 | 96 | /// Returns the result of last finished mutation (if any). 97 | /// 98 | /// - `None` indicates that a mutation is currently loading or has yet to start(idling). 99 | /// - `Some(Ok(m))` indicates that the last mutation is successful and the content is stored in `m`. 100 | /// - `Some(Err(e))` indicates that the last mutation has failed and the error is stored in `e`. 101 | pub fn result(&self) -> Option<&MutationResult> { 102 | match self.state() { 103 | MutationState::Idle | MutationState::Loading => None, 104 | MutationState::Completed { result } 105 | | MutationState::Refreshing { 106 | last_result: result, 107 | } => Some(result), 108 | } 109 | } 110 | 111 | /// Runs a mutation with input. 112 | pub async fn run(&self, input: impl Into>) -> MutationResult { 113 | let id = MutationId::default(); 114 | let input = input.into(); 115 | let (sender, receiver) = oneshot::channel(); 116 | 117 | (self.run_mutation)(RunMutationInput { 118 | handle_id: self.id, 119 | mutation_id: id, 120 | input, 121 | sender: Some(sender).into(), 122 | }); 123 | 124 | receiver.await.unwrap() 125 | } 126 | } 127 | 128 | impl fmt::Debug for UseMutationHandle 129 | where 130 | T: Mutation + fmt::Debug + 'static, 131 | { 132 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 133 | f.debug_struct("UseMutationHandle") 134 | .field("state", &self.state) 135 | .finish() 136 | } 137 | } 138 | 139 | impl Clone for UseMutationHandle 140 | where 141 | T: Mutation + 'static, 142 | { 143 | fn clone(&self) -> Self { 144 | Self { 145 | id: self.id, 146 | state: self.state.clone(), 147 | run_mutation: self.run_mutation.clone(), 148 | _marker: PhantomData, 149 | } 150 | } 151 | } 152 | 153 | /// A hook to run a mutation and subscribes to its result. 154 | /// 155 | /// A mutation is a state that is not started until the run method is invoked. Mutations are 156 | /// usually used to modify data on the server. 157 | /// 158 | /// # Example 159 | /// 160 | /// ``` 161 | /// use std::rc::Rc; 162 | /// use std::convert::Infallible; 163 | /// use bounce::prelude::*; 164 | /// use bounce::query::{Mutation, MutationResult, use_mutation, MutationState}; 165 | /// use yew::prelude::*; 166 | /// use async_trait::async_trait; 167 | /// use yew::platform::spawn_local; 168 | /// 169 | /// #[derive(Debug, PartialEq)] 170 | /// struct User { 171 | /// id: u64, 172 | /// name: String, 173 | /// } 174 | /// 175 | /// #[derive(Debug, PartialEq)] 176 | /// struct UpdateUserMutation { 177 | /// } 178 | /// 179 | /// #[async_trait(?Send)] 180 | /// impl Mutation for UpdateUserMutation { 181 | /// type Input = User; 182 | /// type Error = Infallible; 183 | /// 184 | /// async fn run(_states: &BounceStates, _input: Rc) -> MutationResult { 185 | /// // updates the user information. 186 | /// 187 | /// Ok(UpdateUserMutation {}.into()) 188 | /// } 189 | /// } 190 | /// 191 | /// #[function_component(Comp)] 192 | /// fn comp() -> Html { 193 | /// let update_user = use_mutation::(); 194 | /// 195 | /// let on_click_update_user = { 196 | /// let update_user = update_user.clone(); 197 | /// Callback::from(move |_| { 198 | /// let update_user = update_user.clone(); 199 | /// spawn_local( 200 | /// async move { 201 | /// // The result is also returned to the run method, but since we will 202 | /// // process the result in the render function, we ignore it here. 203 | /// let _result = update_user.run(User {id: 0, name: "Jane Done".into() }).await; 204 | /// } 205 | /// ); 206 | /// }) 207 | /// }; 208 | /// 209 | /// match update_user.result() { 210 | /// // The result is None if the mutation is currently loading or has yet to start. 211 | /// None => if update_user.state() == MutationState::Idle { 212 | /// html! {
{"Updating User..."}
} 213 | /// } else { 214 | /// html! {} 215 | /// }, 216 | /// // The result is Some(Ok(_)) if the mutation has succeed. 217 | /// Some(Ok(_m)) => html! {
{"User has been successfully updated."}
}, 218 | /// // The result is Some(Err(_)) if an error is returned during fetching. 219 | /// Some(Err(_e)) => html! {
{"Oops, something went wrong."}
}, 220 | /// } 221 | /// } 222 | /// ``` 223 | #[hook] 224 | pub fn use_mutation() -> UseMutationHandle 225 | where 226 | T: Mutation + 'static, 227 | { 228 | let id = *use_memo((), |_| HandleId::default()); 229 | let dispatch_state = use_slice_dispatch::>(); 230 | let run_mutation = use_future_notion_runner::>(); 231 | let state = use_input_selector_value::>(id.into()); 232 | 233 | { 234 | use_effect_with(id, |id| { 235 | let id = *id; 236 | dispatch_state(MutationSliceAction::Create(id)); 237 | 238 | move || { 239 | dispatch_state(MutationSliceAction::Destroy(id)); 240 | } 241 | }); 242 | } 243 | 244 | let state = use_memo(state, |state| match state.value.as_ref() { 245 | Some(MutationSliceValue::Idle) | None => MutationState::Idle, 246 | Some(MutationSliceValue::Loading { .. }) => MutationState::Loading, 247 | Some(MutationSliceValue::Completed { result, .. }) => MutationState::Completed { 248 | result: result.clone(), 249 | }, 250 | Some(MutationSliceValue::Outdated { result, .. }) => MutationState::Refreshing { 251 | last_result: result.clone(), 252 | }, 253 | }); 254 | 255 | UseMutationHandle { 256 | id, 257 | state, 258 | run_mutation, 259 | _marker: PhantomData, 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /crates/bounce/src/query/use_prepared_query.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use serde::de::Deserialize; 4 | use serde::ser::Serialize; 5 | use wasm_bindgen::UnwrapThrowExt; 6 | use yew::prelude::*; 7 | use yew::suspense::{Suspension, SuspensionResult}; 8 | 9 | use super::query_states::{ 10 | QuerySelector, QuerySlice, QuerySliceAction, QuerySliceValue, RunQuery, RunQueryInput, 11 | }; 12 | use super::traits::Query; 13 | use super::use_query::{QueryState, UseQueryHandle}; 14 | use crate::root_state::BounceRootState; 15 | use crate::states::future_notion::use_future_notion_runner; 16 | use crate::states::input_selector::use_input_selector_value; 17 | use crate::states::slice::use_slice_dispatch; 18 | use crate::utils::Id; 19 | 20 | /// A hook to run a query and subscribes to its result, suspending while fetching 21 | /// if server-side rendered values are not available. 22 | /// 23 | /// A query is a state that is cached by an Input and queried automatically upon initialisation of the 24 | /// state and re-queried when the input changes. 25 | /// 26 | /// Queries are usually tied to idempotent methods like `GET`, which means that they should be side-effect 27 | /// free and can be cached. 28 | /// 29 | /// If your endpoint modifies data, then you need to use a [mutation](super::use_mutation_value). 30 | /// 31 | /// # Example 32 | /// 33 | /// ``` 34 | /// use std::rc::Rc; 35 | /// use std::convert::Infallible; 36 | /// use bounce::prelude::*; 37 | /// use bounce::query::{Query, QueryResult, use_prepared_query}; 38 | /// use yew::prelude::*; 39 | /// use async_trait::async_trait; 40 | /// use serde::{Serialize, Deserialize}; 41 | /// use thiserror::Error; 42 | /// 43 | /// #[derive(Error, Debug, PartialEq, Serialize, Deserialize, Clone)] 44 | /// #[error("Something that will never happen")] 45 | /// struct Never {} 46 | /// 47 | /// #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 48 | /// struct User { 49 | /// id: u64, 50 | /// name: String, 51 | /// } 52 | /// 53 | /// #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 54 | /// struct UserQuery { 55 | /// value: User 56 | /// } 57 | /// 58 | /// #[async_trait(?Send)] 59 | /// impl Query for UserQuery { 60 | /// type Input = u64; 61 | /// type Error = Never; 62 | /// 63 | /// async fn query(_states: &BounceStates, input: Rc) -> QueryResult { 64 | /// // fetch user 65 | /// 66 | /// Ok(UserQuery{ value: User { id: *input, name: "John Smith".into() } }.into()) 67 | /// } 68 | /// } 69 | /// 70 | /// #[function_component(Comp)] 71 | /// fn comp() -> HtmlResult { 72 | /// let user = use_prepared_query::(0.into())?; 73 | /// 74 | /// match user.as_ref() { 75 | /// // The result is Some(Ok(_)) if the query has loaded successfully. 76 | /// Ok(m) => Ok(html! {
{"User's name is "}{m.value.name.to_string()}
}), 77 | /// // The result is Some(Err(_)) if an error is returned during fetching. 78 | /// Err(_e) => Ok(html! {
{"Oops, something went wrong."}
}), 79 | /// } 80 | /// } 81 | /// ``` 82 | #[hook] 83 | pub fn use_prepared_query(input: Rc) -> SuspensionResult> 84 | where 85 | T: Query + Clone + Serialize + for<'de> Deserialize<'de> + 'static, 86 | T::Input: Clone + Serialize + for<'de> Deserialize<'de>, 87 | T::Error: Clone + Serialize + for<'de> Deserialize<'de>, 88 | { 89 | let id = *use_memo((), |_| Id::new()); 90 | let value_state = use_input_selector_value::>(input.clone()); 91 | let dispatch_state = use_slice_dispatch::>(); 92 | let run_query = use_future_notion_runner::>(); 93 | 94 | let prepared_value = { 95 | let _run_query = run_query.clone(); 96 | let _root = use_context::().expect_throw("No bounce root found."); 97 | 98 | let prepared_value = 99 | use_prepared_state!((*input).clone(), async move |input| -> std::result::Result< 100 | T, 101 | T::Error, 102 | > { 103 | use std::cell::RefCell; 104 | use std::time::Duration; 105 | 106 | use yew::platform::pinned::oneshot; 107 | use yew::platform::time::sleep; 108 | 109 | let (sender, receiver) = oneshot::channel(); 110 | 111 | _run_query(RunQueryInput { 112 | id, 113 | input: input.clone(), 114 | sender: Rc::new(RefCell::new(Some(sender))), 115 | is_refresh: false, 116 | }); 117 | 118 | if let Ok(m) = receiver.await { 119 | return m.map(|m| (*m).clone()); 120 | } 121 | 122 | loop { 123 | let states = _root.states(); 124 | let value_state = 125 | states.get_input_selector_value::>(input.clone()); 126 | 127 | match value_state.value { 128 | Some(QuerySliceValue::Completed { result: ref m, .. }) 129 | | Some(QuerySliceValue::Outdated { result: ref m, .. }) => { 130 | return m.clone().map(|m| (*m).clone()); 131 | } 132 | None | Some(QuerySliceValue::Loading { .. }) => { 133 | let (sender, receiver) = oneshot::channel::<()>(); 134 | let sender = Rc::new(RefCell::new(Some(sender))); 135 | 136 | states.add_listener_callback(Rc::new(Callback::from(move |_| { 137 | if let Some(m) = sender.borrow_mut().take() { 138 | let _ = m.send(()); 139 | } 140 | }))); 141 | // We subscribe to the selector again. 142 | states.get_input_selector_value::>(input.clone()); 143 | 144 | // We yield to event loop so state updates can be applied. 145 | sleep(Duration::ZERO).await; 146 | 147 | receiver.await.unwrap(); 148 | } 149 | } 150 | } 151 | })?; 152 | 153 | (*use_memo(prepared_value, |p| { 154 | p.clone().map(|m| (*m).clone().map(Rc::new)) 155 | })) 156 | .clone() 157 | }; 158 | 159 | let value = use_memo(value_state.clone(), |v| match v.value { 160 | Some(QuerySliceValue::Loading { .. }) | None => Err(Suspension::new()), 161 | Some(QuerySliceValue::Completed { id, result: ref m }) => { 162 | Ok((id, Rc::new(QueryState::Completed { result: m.clone() }))) 163 | } 164 | Some(QuerySliceValue::Outdated { id, result: ref m }) => Ok(( 165 | id, 166 | Rc::new(QueryState::Refreshing { 167 | last_result: m.clone(), 168 | }), 169 | )), 170 | }); 171 | 172 | { 173 | let input = input.clone(); 174 | let run_query = run_query.clone(); 175 | let dispatch_state = dispatch_state.clone(); 176 | 177 | use_memo((), move |_| match prepared_value { 178 | Some(m) => dispatch_state(QuerySliceAction::LoadPrepared { 179 | id, 180 | input, 181 | result: m, 182 | }), 183 | None => run_query(RunQueryInput { 184 | id, 185 | input: input.clone(), 186 | sender: Rc::default(), 187 | is_refresh: false, 188 | }), 189 | }); 190 | } 191 | 192 | { 193 | let input = input.clone(); 194 | let run_query = run_query.clone(); 195 | 196 | use_effect_with( 197 | (id, input, value_state.clone()), 198 | move |(id, input, value_state)| { 199 | if matches!(value_state.value, Some(QuerySliceValue::Outdated { .. })) { 200 | run_query(RunQueryInput { 201 | id: *id, 202 | input: input.clone(), 203 | sender: Rc::default(), 204 | is_refresh: false, 205 | }); 206 | } 207 | 208 | || {} 209 | }, 210 | ); 211 | } 212 | 213 | match value.as_ref().as_ref().cloned() { 214 | Ok((state_id, state)) => Ok(UseQueryHandle { 215 | state_id, 216 | input, 217 | state, 218 | dispatch_state, 219 | run_query, 220 | }), 221 | Err((s, _)) => Err(s.clone()), 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /crates/bounce/src/query/use_query.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::fmt; 3 | use std::ops::Deref; 4 | use std::rc::Rc; 5 | 6 | use yew::platform::pinned::oneshot; 7 | use yew::prelude::*; 8 | use yew::suspense::{Suspension, SuspensionResult}; 9 | 10 | use super::query_states::{ 11 | QuerySelector, QuerySlice, QuerySliceAction, QuerySliceValue, RunQuery, RunQueryInput, 12 | }; 13 | use super::traits::{Query, QueryResult}; 14 | use crate::states::future_notion::use_future_notion_runner; 15 | use crate::states::input_selector::use_input_selector_value; 16 | use crate::states::slice::use_slice_dispatch; 17 | use crate::utils::Id; 18 | 19 | /// Query State 20 | #[derive(Debug, PartialEq)] 21 | pub enum QueryState 22 | where 23 | T: Query + 'static, 24 | { 25 | /// The query has completed. 26 | Completed { 27 | /// Result of the completed query. 28 | result: QueryResult, 29 | }, 30 | /// A previous query has completed and a new query is currently loading. 31 | Refreshing { 32 | /// Result of last completed query. 33 | last_result: QueryResult, 34 | }, 35 | } 36 | 37 | impl Clone for QueryState 38 | where 39 | T: Query + 'static, 40 | { 41 | fn clone(&self) -> Self { 42 | match self { 43 | Self::Completed { result } => Self::Completed { 44 | result: result.clone(), 45 | }, 46 | Self::Refreshing { last_result } => Self::Refreshing { 47 | last_result: last_result.clone(), 48 | }, 49 | } 50 | } 51 | } 52 | 53 | impl PartialEq<&QueryState> for QueryState 54 | where 55 | T: Query + 'static, 56 | { 57 | fn eq(&self, other: &&QueryState) -> bool { 58 | self == *other 59 | } 60 | } 61 | 62 | impl PartialEq> for &'_ QueryState 63 | where 64 | T: Query + 'static, 65 | { 66 | fn eq(&self, other: &QueryState) -> bool { 67 | *self == other 68 | } 69 | } 70 | 71 | /// A handle returned by [`use_query`]. 72 | pub struct UseQueryHandle 73 | where 74 | T: Query + 'static, 75 | { 76 | pub(super) input: Rc, 77 | pub(super) state_id: Id, 78 | pub(super) state: Rc>, 79 | pub(super) run_query: Rc)>, 80 | pub(super) dispatch_state: Rc)>, 81 | } 82 | 83 | impl UseQueryHandle 84 | where 85 | T: Query + 'static, 86 | { 87 | /// Returns the state of current query. 88 | pub fn state(&self) -> &QueryState { 89 | self.state.as_ref() 90 | } 91 | 92 | /// Refreshes the query. 93 | /// 94 | /// The query will be refreshed with the input provided to the hook. 95 | pub async fn refresh(&self) -> QueryResult { 96 | let id = Id::new(); 97 | (self.dispatch_state)(QuerySliceAction::Refresh { 98 | id, 99 | input: self.input.clone(), 100 | }); 101 | 102 | let (sender, receiver) = oneshot::channel(); 103 | 104 | (self.run_query)(RunQueryInput { 105 | id, 106 | input: self.input.clone(), 107 | sender: Rc::new(RefCell::new(Some(sender))), 108 | is_refresh: true, 109 | }); 110 | 111 | receiver.await.unwrap() 112 | } 113 | } 114 | 115 | impl Clone for UseQueryHandle 116 | where 117 | T: Query + 'static, 118 | { 119 | fn clone(&self) -> Self { 120 | Self { 121 | input: self.input.clone(), 122 | state: self.state.clone(), 123 | state_id: self.state_id, 124 | run_query: self.run_query.clone(), 125 | dispatch_state: self.dispatch_state.clone(), 126 | } 127 | } 128 | } 129 | 130 | impl Deref for UseQueryHandle 131 | where 132 | T: Query + 'static, 133 | { 134 | type Target = QueryResult; 135 | 136 | fn deref(&self) -> &Self::Target { 137 | match self.state() { 138 | QueryState::Completed { result } => result, 139 | QueryState::Refreshing { last_result } => last_result, 140 | } 141 | } 142 | } 143 | 144 | impl fmt::Debug for UseQueryHandle 145 | where 146 | T: Query + fmt::Debug + 'static, 147 | { 148 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 149 | f.debug_struct("UseQueryHandle") 150 | .field("value", self.deref()) 151 | .finish() 152 | } 153 | } 154 | 155 | /// A hook to run a query and subscribes to its result, suspending while fetching. 156 | /// 157 | /// A query is a state that is cached by an Input and queried automatically upon initialisation of the 158 | /// state and re-queried when the input changes. 159 | /// 160 | /// Queries are usually tied to idempotent methods like `GET`, which means that they should be side-effect 161 | /// free and can be cached. 162 | /// 163 | /// If your endpoint modifies data, then you need to use a [mutation](super::use_mutation_value). 164 | /// 165 | /// # Example 166 | /// 167 | /// ``` 168 | /// use std::rc::Rc; 169 | /// use std::convert::Infallible; 170 | /// use bounce::prelude::*; 171 | /// use bounce::query::{Query, QueryResult, use_query}; 172 | /// use yew::prelude::*; 173 | /// use async_trait::async_trait; 174 | /// 175 | /// #[derive(Debug, PartialEq)] 176 | /// struct User { 177 | /// id: u64, 178 | /// name: String, 179 | /// } 180 | /// 181 | /// #[derive(Debug, PartialEq)] 182 | /// struct UserQuery { 183 | /// value: User 184 | /// } 185 | /// 186 | /// #[async_trait(?Send)] 187 | /// impl Query for UserQuery { 188 | /// type Input = u64; 189 | /// type Error = Infallible; 190 | /// 191 | /// async fn query(_states: &BounceStates, input: Rc) -> QueryResult { 192 | /// // fetch user 193 | /// 194 | /// Ok(UserQuery{ value: User { id: *input, name: "John Smith".into() } }.into()) 195 | /// } 196 | /// } 197 | /// 198 | /// #[function_component(Comp)] 199 | /// fn comp() -> HtmlResult { 200 | /// let user = use_query::(0.into())?; 201 | /// 202 | /// match user.as_ref() { 203 | /// // The result is Some(Ok(_)) if the query has loaded successfully. 204 | /// Ok(m) => Ok(html! {
{"User's name is "}{m.value.name.to_string()}
}), 205 | /// // The result is Some(Err(_)) if an error is returned during fetching. 206 | /// Err(_e) => Ok(html! {
{"Oops, something went wrong."}
}), 207 | /// } 208 | /// } 209 | /// ``` 210 | #[hook] 211 | pub fn use_query(input: Rc) -> SuspensionResult> 212 | where 213 | T: Query + 'static, 214 | { 215 | let id = *use_memo((), |_| Id::new()); 216 | let value_state = use_input_selector_value::>(input.clone()); 217 | let dispatch_state = use_slice_dispatch::>(); 218 | let run_query = use_future_notion_runner::>(); 219 | 220 | let value = use_memo(value_state.clone(), |v| match v.value { 221 | Some(QuerySliceValue::Loading { .. }) | None => Err(Suspension::new()), 222 | Some(QuerySliceValue::Completed { id, result: ref m }) => { 223 | Ok((id, Rc::new(QueryState::Completed { result: m.clone() }))) 224 | } 225 | Some(QuerySliceValue::Outdated { id, result: ref m }) => Ok(( 226 | id, 227 | Rc::new(QueryState::Refreshing { 228 | last_result: m.clone(), 229 | }), 230 | )), 231 | }); 232 | 233 | { 234 | let input = input.clone(); 235 | let run_query = run_query.clone(); 236 | 237 | use_memo((), move |_| { 238 | run_query(RunQueryInput { 239 | id, 240 | input: input.clone(), 241 | sender: Rc::default(), 242 | is_refresh: false, 243 | }); 244 | }); 245 | } 246 | 247 | { 248 | let input = input.clone(); 249 | let run_query = run_query.clone(); 250 | 251 | use_effect_with( 252 | (id, input, value_state.clone()), 253 | move |(id, input, value_state)| { 254 | if matches!(value_state.value, Some(QuerySliceValue::Outdated { .. })) { 255 | run_query(RunQueryInput { 256 | id: *id, 257 | input: input.clone(), 258 | sender: Rc::default(), 259 | is_refresh: false, 260 | }); 261 | } 262 | 263 | || {} 264 | }, 265 | ); 266 | } 267 | 268 | value 269 | .as_ref() 270 | .as_ref() 271 | .cloned() 272 | .map(|(state_id, state)| UseQueryHandle { 273 | state, 274 | state_id, 275 | input, 276 | dispatch_state, 277 | run_query, 278 | }) 279 | .map_err(|(s, _)| s.clone()) 280 | } 281 | -------------------------------------------------------------------------------- /crates/bounce/src/query/use_query_value.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::fmt; 3 | use std::rc::Rc; 4 | 5 | use yew::platform::pinned::oneshot; 6 | use yew::prelude::*; 7 | 8 | use super::query_states::{ 9 | QuerySelector, QuerySlice, QuerySliceAction, QuerySliceValue, RunQuery, RunQueryInput, 10 | }; 11 | 12 | use super::traits::{Query, QueryResult}; 13 | use crate::states::future_notion::use_future_notion_runner; 14 | use crate::states::input_selector::use_input_selector_value; 15 | use crate::states::slice::use_slice_dispatch; 16 | use crate::utils::Id; 17 | 18 | /// Query Value State 19 | #[derive(Debug, PartialEq)] 20 | pub enum QueryValueState 21 | where 22 | T: Query + 'static, 23 | { 24 | /// The query is loading. 25 | Loading, 26 | /// The query has completed. 27 | Completed { 28 | /// Result of the completed query. 29 | result: QueryResult, 30 | }, 31 | /// A previous query has completed and a new query is currently loading. 32 | Refreshing { 33 | /// Result of last completed query. 34 | last_result: QueryResult, 35 | }, 36 | } 37 | 38 | impl Clone for QueryValueState 39 | where 40 | T: Query + 'static, 41 | { 42 | fn clone(&self) -> Self { 43 | match self { 44 | Self::Loading => Self::Loading, 45 | Self::Completed { result } => Self::Completed { 46 | result: result.clone(), 47 | }, 48 | Self::Refreshing { last_result } => Self::Refreshing { 49 | last_result: last_result.clone(), 50 | }, 51 | } 52 | } 53 | } 54 | 55 | impl PartialEq<&QueryValueState> for QueryValueState 56 | where 57 | T: Query + 'static, 58 | { 59 | fn eq(&self, other: &&QueryValueState) -> bool { 60 | self == *other 61 | } 62 | } 63 | 64 | impl PartialEq> for &'_ QueryValueState 65 | where 66 | T: Query + 'static, 67 | { 68 | fn eq(&self, other: &QueryValueState) -> bool { 69 | *self == other 70 | } 71 | } 72 | 73 | /// A handle returned by [`use_query_value`]. 74 | pub struct UseQueryValueHandle 75 | where 76 | T: Query + 'static, 77 | { 78 | input: Rc, 79 | state: Rc>, 80 | run_query: Rc)>, 81 | dispatch_state: Rc)>, 82 | } 83 | 84 | impl UseQueryValueHandle 85 | where 86 | T: Query + 'static, 87 | { 88 | /// Returns the state of current query. 89 | pub fn state(&self) -> &QueryValueState { 90 | self.state.as_ref() 91 | } 92 | 93 | /// Returns the result of current query (if any). 94 | /// 95 | /// - `None` indicates that the query is currently loading. 96 | /// - `Some(Ok(m))` indicates that the query is successful and the content is stored in `m`. 97 | /// - `Some(Err(e))` indicates that the query has failed and the error is stored in `e`. 98 | pub fn result(&self) -> Option<&QueryResult> { 99 | match self.state() { 100 | QueryValueState::Completed { result, .. } 101 | | QueryValueState::Refreshing { 102 | last_result: result, 103 | .. 104 | } => Some(result), 105 | _ => None, 106 | } 107 | } 108 | 109 | /// Refreshes the query. 110 | /// 111 | /// The query will be refreshed with the input provided to the hook. 112 | pub async fn refresh(&self) -> QueryResult { 113 | let id = Id::new(); 114 | (self.dispatch_state)(QuerySliceAction::Refresh { 115 | id, 116 | input: self.input.clone(), 117 | }); 118 | 119 | let (sender, receiver) = oneshot::channel(); 120 | 121 | (self.run_query)(RunQueryInput { 122 | id, 123 | input: self.input.clone(), 124 | sender: Rc::new(RefCell::new(Some(sender))), 125 | is_refresh: true, 126 | }); 127 | 128 | receiver.await.unwrap() 129 | } 130 | } 131 | 132 | impl Clone for UseQueryValueHandle 133 | where 134 | T: Query + 'static, 135 | { 136 | fn clone(&self) -> Self { 137 | Self { 138 | input: self.input.clone(), 139 | state: self.state.clone(), 140 | run_query: self.run_query.clone(), 141 | dispatch_state: self.dispatch_state.clone(), 142 | } 143 | } 144 | } 145 | 146 | impl fmt::Debug for UseQueryValueHandle 147 | where 148 | T: Query + fmt::Debug + 'static, 149 | { 150 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 151 | f.debug_struct("UseQueryValueHandle") 152 | .field("value", &self.state) 153 | .finish() 154 | } 155 | } 156 | 157 | /// A hook to run a query and subscribes to its result. 158 | /// 159 | /// A query is a state that is cached by an Input and queried automatically upon initialisation of the 160 | /// state and re-queried when the input changes. 161 | /// 162 | /// Queries are usually tied to idempotent methods like `GET`, which means that they should be side-effect 163 | /// free and can be cached. 164 | /// 165 | /// If your endpoint modifies data, then you need to use a [mutation](super::use_mutation_value). 166 | /// 167 | /// # Example 168 | /// 169 | /// ``` 170 | /// use std::rc::Rc; 171 | /// use std::convert::Infallible; 172 | /// use bounce::prelude::*; 173 | /// use bounce::query::{Query, QueryResult, use_query_value}; 174 | /// use yew::prelude::*; 175 | /// use async_trait::async_trait; 176 | /// 177 | /// #[derive(Debug, PartialEq)] 178 | /// struct User { 179 | /// id: u64, 180 | /// name: String, 181 | /// } 182 | /// 183 | /// #[derive(Debug, PartialEq)] 184 | /// struct UserQuery { 185 | /// value: User 186 | /// } 187 | /// 188 | /// #[async_trait(?Send)] 189 | /// impl Query for UserQuery { 190 | /// type Input = u64; 191 | /// type Error = Infallible; 192 | /// 193 | /// async fn query(_states: &BounceStates, input: Rc) -> QueryResult { 194 | /// // fetch user 195 | /// 196 | /// Ok(UserQuery{ value: User { id: *input, name: "John Smith".into() } }.into()) 197 | /// } 198 | /// } 199 | /// 200 | /// #[function_component(Comp)] 201 | /// fn comp() -> Html { 202 | /// let user = use_query_value::(0.into()); 203 | /// 204 | /// match user.result() { 205 | /// // The result is None if the query is currently loading. 206 | /// None => html! {
{"loading..."}
}, 207 | /// // The result is Some(Ok(_)) if the query has loaded successfully. 208 | /// Some(Ok(m)) => html! {
{"User's name is "}{m.value.name.to_string()}
}, 209 | /// // The result is Some(Err(_)) if an error is returned during fetching. 210 | /// Some(Err(e)) => html! {
{"Oops, something went wrong."}
}, 211 | /// } 212 | /// } 213 | /// ``` 214 | #[hook] 215 | pub fn use_query_value(input: Rc) -> UseQueryValueHandle 216 | where 217 | T: Query + 'static, 218 | { 219 | let id = *use_memo((), |_| Id::new()); 220 | let value = use_input_selector_value::>(input.clone()); 221 | let dispatch_state = use_slice_dispatch::>(); 222 | let run_query = use_future_notion_runner::>(); 223 | 224 | { 225 | let input = input.clone(); 226 | let run_query = run_query.clone(); 227 | use_effect_with( 228 | (id, input, value.value.clone()), 229 | move |(id, input, value)| { 230 | if value.is_none() || matches!(value, Some(QuerySliceValue::Outdated { .. })) { 231 | run_query(RunQueryInput { 232 | id: *id, 233 | input: input.clone(), 234 | sender: Rc::default(), 235 | is_refresh: false, 236 | }); 237 | } 238 | 239 | || {} 240 | }, 241 | ); 242 | } 243 | 244 | let state = use_memo(value, |value| match value.value { 245 | Some(QuerySliceValue::Completed { ref result, .. }) => QueryValueState::Completed { 246 | result: result.clone(), 247 | }, 248 | Some(QuerySliceValue::Outdated { ref result, .. }) => QueryValueState::Refreshing { 249 | last_result: result.clone(), 250 | }, 251 | Some(QuerySliceValue::Loading { .. }) | None => QueryValueState::Loading, 252 | }); 253 | 254 | UseQueryValueHandle { 255 | input, 256 | dispatch_state, 257 | run_query, 258 | state, 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /crates/bounce/src/root_state.rs: -------------------------------------------------------------------------------- 1 | use std::any::{Any, TypeId}; 2 | use std::cell::RefCell; 3 | use std::collections::hash_map; 4 | use std::collections::HashMap; 5 | use std::fmt; 6 | use std::rc::Rc; 7 | 8 | use anymap2::any::CloneAny; 9 | use anymap2::{AnyMap, Entry, Map}; 10 | use yew::callback::Callback; 11 | 12 | use crate::any_state::AnyState; 13 | use crate::states::artifact::ArtifactSlice; 14 | use crate::states::atom::{Atom, AtomSlice}; 15 | use crate::states::input_selector::{InputSelector, InputSelectorsState}; 16 | use crate::states::selector::{Selector, UnitSelector}; 17 | use crate::states::slice::{Slice, SliceState}; 18 | use crate::utils::Id; 19 | use crate::utils::Listener; 20 | 21 | pub(crate) type StateMap = Map; 22 | type AnyStateMap = HashMap>>; 23 | 24 | #[derive(Clone)] 25 | pub(crate) struct BounceRootState { 26 | id: Id, 27 | init_states: Rc>, 28 | states: Rc>, 29 | notion_states: Rc>, 30 | } 31 | 32 | impl Default for BounceRootState { 33 | fn default() -> Self { 34 | Self::new(AnyMap::new()) 35 | } 36 | } 37 | 38 | impl BounceRootState { 39 | #[inline] 40 | pub fn new(init_states: AnyMap) -> Self { 41 | Self { 42 | id: Id::new(), 43 | init_states: Rc::new(RefCell::new(init_states)), 44 | states: Rc::default(), 45 | notion_states: Rc::default(), 46 | } 47 | } 48 | 49 | pub fn get_state(&self) -> T 50 | where 51 | T: AnyState + Clone + Default + 'static, 52 | { 53 | let mut states = self.states.borrow_mut(); 54 | 55 | match states.entry::() { 56 | Entry::Occupied(m) => m.get().clone(), 57 | Entry::Vacant(m) => { 58 | let state = { 59 | let mut init_states = self.init_states.borrow_mut(); 60 | T::create(&mut init_states) 61 | }; 62 | m.insert(state.clone()); 63 | 64 | let mut notion_states = self.notion_states.borrow_mut(); 65 | for notion_id in state.notion_ids() { 66 | match notion_states.entry(notion_id) { 67 | hash_map::Entry::Occupied(mut m) => { 68 | m.get_mut().push(Rc::new(state.clone()) as Rc); 69 | } 70 | hash_map::Entry::Vacant(m) => { 71 | m.insert(vec![Rc::new(state.clone()) as Rc]); 72 | } 73 | } 74 | } 75 | 76 | state 77 | } 78 | } 79 | } 80 | 81 | pub fn apply_notion(&self, notion: Rc) 82 | where 83 | T: 'static, 84 | { 85 | let notion_state = self.notion_states.borrow().get(&TypeId::of::()).cloned(); 86 | 87 | let notion = notion as Rc; 88 | 89 | if let Some(m) = notion_state { 90 | for any_state in m.iter() { 91 | any_state.apply(notion.clone()); 92 | } 93 | } 94 | } 95 | 96 | pub fn states(&self) -> BounceStates { 97 | BounceStates { 98 | inner: self.clone(), 99 | listeners: Rc::default(), 100 | listener_callbacks: Rc::default(), 101 | } 102 | } 103 | 104 | pub fn clear(&self) { 105 | self.notion_states.borrow_mut().clear(); 106 | self.states.borrow_mut().clear(); 107 | } 108 | } 109 | 110 | impl PartialEq for BounceRootState { 111 | fn eq(&self, rhs: &Self) -> bool { 112 | self.id == rhs.id 113 | } 114 | } 115 | 116 | /// A type to access states under a bounce root. 117 | pub struct BounceStates { 118 | inner: BounceRootState, 119 | listeners: Rc>>, 120 | listener_callbacks: Rc>>>>, 121 | } 122 | 123 | impl BounceStates { 124 | /// Returns the value of a `Slice`. 125 | pub fn get_slice_value(&self) -> Rc 126 | where 127 | T: Slice + 'static, 128 | { 129 | let state = self.inner.get_state::>(); 130 | let listener_callbacks = self.listener_callbacks.borrow().clone(); 131 | let mut listeners = Vec::new(); 132 | 133 | for callback in listener_callbacks { 134 | let listener = state.listen(Rc::new(Callback::from(move |_: Rc| { 135 | callback.emit(()); 136 | }))); 137 | 138 | listeners.push(listener); 139 | } 140 | 141 | self.listeners.borrow_mut().extend(listeners); 142 | 143 | state.get() 144 | } 145 | 146 | /// Returns the value of an `Atom`. 147 | pub fn get_atom_value(&self) -> Rc 148 | where 149 | T: Atom + 'static, 150 | { 151 | self.get_slice_value::>().inner.clone() 152 | } 153 | 154 | /// Returns the value of an [`InputSelector`]. 155 | pub fn get_input_selector_value(&self, input: Rc) -> Rc 156 | where 157 | T: InputSelector + 'static, 158 | { 159 | let state = self 160 | .inner 161 | .get_state::>() 162 | .get_state(input); 163 | let listener_callbacks = self.listener_callbacks.borrow().clone(); 164 | let mut listeners = Vec::new(); 165 | 166 | for callback in listener_callbacks { 167 | let listener = state.listen(Rc::new(Callback::from(move |_: Rc| { 168 | callback.emit(()); 169 | }))); 170 | 171 | listeners.push(listener); 172 | } 173 | 174 | self.listeners.borrow_mut().extend(listeners); 175 | 176 | state.get(self.derived_clone()) 177 | } 178 | 179 | /// Returns the value of a [`Selector`]. 180 | pub fn get_selector_value(&self) -> Rc 181 | where 182 | T: Selector + 'static, 183 | { 184 | self.get_input_selector_value::>(Rc::new(())) 185 | .inner 186 | .clone() 187 | } 188 | 189 | /// Returns all values of an [`Artifact`](crate::Artifact). 190 | pub fn get_artifacts(&self) -> Vec> 191 | where 192 | T: PartialEq + 'static, 193 | { 194 | self.get_slice_value::>().get() 195 | } 196 | 197 | pub(crate) fn add_listener_callback(&self, callback: Rc>) { 198 | let mut listener_callbacks = self.listener_callbacks.borrow_mut(); 199 | listener_callbacks.push(callback); 200 | } 201 | 202 | pub(crate) fn take_listeners(&self) -> Vec { 203 | let mut next_listeners = Vec::new(); 204 | let mut last_listeners = self.listeners.borrow_mut(); 205 | 206 | std::mem::swap(&mut next_listeners, &mut last_listeners); 207 | 208 | // Also clears callbacks. 209 | let mut listener_callbacks = self.listener_callbacks.borrow_mut(); 210 | listener_callbacks.clear(); 211 | 212 | next_listeners 213 | } 214 | 215 | /// Creates a sub-states, but with a separate listener holder. 216 | fn derived_clone(&self) -> Self { 217 | Self { 218 | inner: self.inner.clone(), 219 | listeners: Rc::default(), 220 | listener_callbacks: Rc::default(), 221 | } 222 | } 223 | } 224 | 225 | impl fmt::Debug for BounceStates { 226 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 227 | f.debug_struct("BounceStates") 228 | .field("inner", &"BounceRootState") 229 | .finish() 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /crates/bounce/src/states/artifact.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::rc::Rc; 3 | 4 | use wasm_bindgen::prelude::*; 5 | use yew::prelude::*; 6 | 7 | use crate::root_state::BounceRootState; 8 | use crate::states::slice::{use_slice_dispatch, use_slice_value}; 9 | use crate::utils::Id; 10 | use crate::Slice; 11 | 12 | pub(crate) enum ArtifactAction { 13 | Insert(Id, Rc), 14 | Remove(Id), 15 | } 16 | 17 | #[derive(PartialEq, Slice)] 18 | pub(crate) struct ArtifactSlice 19 | where 20 | T: PartialEq + 'static, 21 | { 22 | inner: BTreeMap>, 23 | } 24 | 25 | impl Default for ArtifactSlice 26 | where 27 | T: PartialEq + 'static, 28 | { 29 | fn default() -> Self { 30 | Self { 31 | inner: BTreeMap::new(), 32 | } 33 | } 34 | } 35 | 36 | impl Clone for ArtifactSlice { 37 | fn clone(&self) -> Self { 38 | Self { 39 | inner: self.inner.clone(), 40 | } 41 | } 42 | } 43 | 44 | impl Reducible for ArtifactSlice 45 | where 46 | T: PartialEq + 'static, 47 | { 48 | type Action = ArtifactAction; 49 | 50 | fn reduce(self: Rc, action: Self::Action) -> Rc { 51 | let mut self_ = (*self).clone(); 52 | 53 | match action { 54 | ArtifactAction::Insert(id, artifact) => self_.inner.insert(id, artifact), 55 | ArtifactAction::Remove(id) => self_.inner.remove(&id), 56 | }; 57 | 58 | self_.into() 59 | } 60 | } 61 | 62 | impl ArtifactSlice 63 | where 64 | T: PartialEq + 'static, 65 | { 66 | pub(crate) fn get(&self) -> Vec> { 67 | self.inner.values().cloned().collect() 68 | } 69 | } 70 | 71 | /// A hook to read all artifacts of the current artifact type. 72 | /// 73 | /// An artifact is a global side effect (e.g.: document title) that will be collected in the 74 | /// rendering order. 75 | /// 76 | /// # Note 77 | /// 78 | /// If you are trying to manipulate elements in the `` element (e.g.: document title), 79 | /// it is recommended to use the [Helmet](crate::helmet) API instead. 80 | /// 81 | /// # Example 82 | /// 83 | /// ``` 84 | /// # use yew::prelude::*; 85 | /// # use bounce::prelude::*; 86 | /// # use std::rc::Rc; 87 | /// # 88 | /// #[derive(Debug, PartialEq)] 89 | /// pub struct Title { 90 | /// inner: String, 91 | /// } 92 | /// 93 | /// #[function_component(Inner)] 94 | /// fn inner() -> Html { 95 | /// html! { value={Rc::new(Title { inner: "My Title 2".into() })} />} 96 | /// } 97 | /// 98 | /// #[function_component(Outer)] 99 | /// fn outer() -> Html { 100 | /// html! { 101 | /// <> 102 | /// value={Rc::new(Title { inner: "My Title 1".into() })} /> 103 | /// 104 | /// 105 | /// } 106 | /// } 107 | /// 108 | /// # fn inner() { 109 | /// // [Title { inner: "My Title 1" }, Title { inner: "My Title 2" }] 110 | /// let title_artifacts = use_artifacts::(); 111 | /// # } 112 | /// ``` 113 | #[hook] 114 | pub fn use_artifacts<T>() -> Vec<Rc<T>> 115 | where 116 | T: PartialEq + 'static, 117 | { 118 | use_slice_value::<ArtifactSlice<T>>().get() 119 | } 120 | 121 | /// Properties of the [`Artifact`] Component. 122 | #[derive(Debug, Properties, PartialEq, Eq)] 123 | pub struct ArtifactProps<T> 124 | where 125 | T: PartialEq + 'static, 126 | { 127 | /// The Rc'ed value of the artifact. 128 | pub value: Rc<T>, 129 | } 130 | 131 | /// A component to register an artifact. 132 | /// 133 | /// The artifact is registered in rendering order and is collected into a vector 134 | /// that can be read with the [`use_artifacts`] hook. 135 | /// 136 | /// # Note 137 | /// 138 | /// If you are trying to manipulate elements in the `<head />` element (e.g.: document title), 139 | /// it is recommended to use the [Helmet](crate::helmet) API instead. 140 | /// 141 | /// # Example 142 | /// 143 | /// ``` 144 | /// # use yew::prelude::*; 145 | /// # use bounce::prelude::*; 146 | /// # use std::rc::Rc; 147 | /// # 148 | /// #[derive(Debug, PartialEq)] 149 | /// pub struct Title { 150 | /// inner: String, 151 | /// } 152 | /// 153 | /// let artifact = Rc::new(Title { inner: "My Title".into() }); 154 | /// 155 | /// let rendered = html! {<Artifact<Title> value={artifact} />}; 156 | /// ``` 157 | #[function_component(Artifact)] 158 | pub fn artifact<T>(props: &ArtifactProps<T>) -> Html 159 | where 160 | T: PartialEq + 'static, 161 | { 162 | let id = *use_state(Id::new); 163 | 164 | // we need to register root as a dependency of effects so that when the root changes the artifact can 165 | // be moved from 1 root to another. 166 | let root = use_context::<BounceRootState>().expect_throw("No bounce root found."); 167 | 168 | let artifact_dispatch = use_slice_dispatch::<ArtifactSlice<T>>(); 169 | 170 | { 171 | let artifact_dispatch = artifact_dispatch.clone(); 172 | use_effect_with((props.value.clone(), root.clone()), move |(val, _)| { 173 | artifact_dispatch(ArtifactAction::Insert(id, val.clone())); 174 | || {} 175 | }); 176 | } 177 | 178 | #[allow(clippy::unused_unit)] 179 | { 180 | let _artifact_dispatch = artifact_dispatch.clone(); 181 | let _val = props.value.clone(); 182 | let _ = use_prepared_state!((), move |_| -> () { 183 | _artifact_dispatch(ArtifactAction::Insert(id, _val)); 184 | }); 185 | } 186 | 187 | use_effect_with(root, move |_| { 188 | move || { 189 | artifact_dispatch(ArtifactAction::Remove(id)); 190 | } 191 | }); 192 | 193 | Html::default() 194 | } 195 | -------------------------------------------------------------------------------- /crates/bounce/src/states/atom.rs: -------------------------------------------------------------------------------- 1 | use std::any::{Any, TypeId}; 2 | use std::fmt; 3 | use std::ops::Deref; 4 | use std::rc::Rc; 5 | 6 | use super::slice::{use_slice, use_slice_dispatch, use_slice_value, Slice, UseSliceHandle}; 7 | 8 | use anymap2::AnyMap; 9 | pub use bounce_macros::Atom; 10 | use yew::prelude::*; 11 | 12 | #[doc(hidden)] 13 | pub trait Atom: PartialEq + Default { 14 | /// Applies a notion. 15 | /// 16 | /// This always yields a new instance of [`Rc<Self>`] so it can be compared with the previous 17 | /// atom using [`PartialEq`]. 18 | #[allow(unused_variables)] 19 | fn apply(self: Rc<Self>, notion: Rc<dyn Any>) -> Rc<Self> { 20 | self 21 | } 22 | 23 | /// Returns a list of notion ids that this atom accepts. 24 | fn notion_ids(&self) -> Vec<TypeId>; 25 | 26 | /// Notifies an atom that its value has changed. 27 | fn changed(self: Rc<Self>) {} 28 | 29 | /// Creates a new atom with its initial value. 30 | fn create(init_states: &mut AnyMap) -> Self 31 | where 32 | Self: 'static + Sized, 33 | { 34 | init_states.remove().unwrap_or_default() 35 | } 36 | } 37 | 38 | /// A trait to provide cloning on atoms. 39 | /// 40 | /// This trait provides a `self.clone_atom()` method that can be used as an alias of `(*self).clone()` 41 | /// in apply functions to produce a owned clone of the atom. 42 | pub trait CloneAtom: Atom + Clone { 43 | /// Clones current atom. 44 | #[inline] 45 | fn clone_atom(&self) -> Self { 46 | self.clone() 47 | } 48 | } 49 | 50 | impl<T> CloneAtom for T where T: Atom + Clone {} 51 | 52 | #[derive(PartialEq, Default)] 53 | pub(crate) struct AtomSlice<T> 54 | where 55 | T: Atom, 56 | { 57 | pub inner: Rc<T>, 58 | } 59 | 60 | impl<T> Slice for AtomSlice<T> 61 | where 62 | T: Atom, 63 | { 64 | type Action = T; 65 | 66 | fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> { 67 | Self { 68 | inner: action.into(), 69 | } 70 | .into() 71 | } 72 | 73 | fn apply(self: Rc<Self>, notion: Rc<dyn Any>) -> Rc<Self> { 74 | Self { 75 | inner: self.inner.clone().apply(notion), 76 | } 77 | .into() 78 | } 79 | 80 | fn notion_ids(&self) -> Vec<TypeId> { 81 | self.inner.notion_ids() 82 | } 83 | 84 | fn changed(self: Rc<Self>) { 85 | self.inner.clone().changed(); 86 | } 87 | 88 | fn create(init_states: &mut AnyMap) -> Self 89 | where 90 | Self: 'static + Sized, 91 | { 92 | Self { 93 | inner: T::create(init_states).into(), 94 | } 95 | } 96 | } 97 | 98 | /// A handle returned by [`use_atom`]. 99 | /// 100 | /// This type dereferences to `T` and has a `set` method to set value for current state. 101 | pub struct UseAtomHandle<T> 102 | where 103 | T: Atom, 104 | { 105 | inner: UseSliceHandle<AtomSlice<T>>, 106 | } 107 | 108 | impl<T> UseAtomHandle<T> 109 | where 110 | T: Atom + 'static, 111 | { 112 | /// Sets the value of current atom. 113 | pub fn set(&self, val: T) { 114 | self.inner.dispatch(val) 115 | } 116 | } 117 | 118 | impl<T> Deref for UseAtomHandle<T> 119 | where 120 | T: Atom, 121 | { 122 | type Target = T; 123 | 124 | fn deref(&self) -> &Self::Target { 125 | &(*self.inner).inner 126 | } 127 | } 128 | 129 | impl<T> Clone for UseAtomHandle<T> 130 | where 131 | T: Atom, 132 | { 133 | fn clone(&self) -> Self { 134 | Self { 135 | inner: self.inner.clone(), 136 | } 137 | } 138 | } 139 | 140 | impl<T> fmt::Debug for UseAtomHandle<T> 141 | where 142 | T: Atom + fmt::Debug, 143 | { 144 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 145 | f.debug_struct("UseAtomHandle") 146 | .field("inner", self.inner.inner.as_ref()) 147 | .finish() 148 | } 149 | } 150 | 151 | /// A hook to connect to an [`Atom`](macro@crate::Atom). 152 | /// 153 | /// Returns a [`UseAtomHandle<T>`]. 154 | /// 155 | /// # Example 156 | /// 157 | /// ``` 158 | /// # use std::fmt; 159 | /// # use bounce::prelude::*; 160 | /// # use yew::prelude::*; 161 | /// # use web_sys::HtmlInputElement; 162 | /// # 163 | /// #[derive(PartialEq, Atom)] 164 | /// struct Username { 165 | /// inner: String, 166 | /// } 167 | /// 168 | /// impl Default for Username { 169 | /// fn default() -> Self { 170 | /// Self { 171 | /// inner: "Jane Doe".into(), 172 | /// } 173 | /// } 174 | /// } 175 | /// 176 | /// impl From<String> for Username { 177 | /// fn from(s: String) -> Self { 178 | /// Self { inner: s } 179 | /// } 180 | /// } 181 | /// 182 | /// impl fmt::Display for Username { 183 | /// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 184 | /// write!(f, "{}", self.inner) 185 | /// } 186 | /// } 187 | /// 188 | /// #[function_component(Setter)] 189 | /// fn setter() -> Html { 190 | /// let username = use_atom::<Username>(); 191 | /// 192 | /// let on_text_input = { 193 | /// let username = username.clone(); 194 | /// 195 | /// Callback::from(move |e: InputEvent| { 196 | /// let input: HtmlInputElement = e.target_unchecked_into(); 197 | /// 198 | /// username.set(input.value().into()); 199 | /// }) 200 | /// }; 201 | /// 202 | /// html! { 203 | /// <div> 204 | /// <input type_="text" oninput={on_text_input} value={username.to_string()} /> 205 | /// </div> 206 | /// } 207 | /// } 208 | /// ``` 209 | #[hook] 210 | pub fn use_atom<T>() -> UseAtomHandle<T> 211 | where 212 | T: Atom + 'static, 213 | { 214 | let inner = use_slice::<AtomSlice<T>>(); 215 | 216 | UseAtomHandle { inner } 217 | } 218 | 219 | /// A hook to produce a setter function for an [`Atom`](macro@crate::Atom). 220 | /// 221 | /// Returns a `Rc<dyn Fn(T)>`. 222 | /// 223 | /// This hook will return a setter function that will not change across the entire lifetime of the 224 | /// component. 225 | /// 226 | /// ``` 227 | /// # use bounce::prelude::*; 228 | /// # use std::fmt; 229 | /// # use yew::prelude::*; 230 | /// # use bounce::prelude::*; 231 | /// # #[derive(PartialEq, Atom)] 232 | /// # struct Username { 233 | /// # inner: String, 234 | /// # } 235 | /// # 236 | /// # impl From<&str> for Username { 237 | /// # fn from(s: &str) -> Self { 238 | /// # Self { inner: s.into() } 239 | /// # } 240 | /// # } 241 | /// # 242 | /// # impl Default for Username { 243 | /// # fn default() -> Self { 244 | /// # Self { 245 | /// # inner: "Jane Doe".into(), 246 | /// # } 247 | /// # } 248 | /// # } 249 | /// # 250 | /// # impl fmt::Display for Username { 251 | /// # fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 252 | /// # write!(f, "{}", self.inner) 253 | /// # } 254 | /// # } 255 | /// # 256 | /// # #[function_component(Setter)] 257 | /// # fn setter() -> Html { 258 | /// let set_username = use_atom_setter::<Username>(); 259 | /// set_username("John Smith".into()); 260 | /// # Html::default() 261 | /// # } 262 | /// ``` 263 | #[hook] 264 | pub fn use_atom_setter<T>() -> Rc<dyn Fn(T)> 265 | where 266 | T: Atom + 'static, 267 | { 268 | use_slice_dispatch::<AtomSlice<T>>() 269 | } 270 | 271 | /// A read-only hook to connect to the value of an [`Atom`](macro@crate::Atom). 272 | /// 273 | /// Returns `Rc<T>`. 274 | /// 275 | /// # Example 276 | /// 277 | /// ``` 278 | /// # use std::fmt; 279 | /// # use yew::prelude::*; 280 | /// # use bounce::prelude::*; 281 | /// # #[derive(PartialEq, Atom)] 282 | /// # struct Username { 283 | /// # inner: String, 284 | /// # } 285 | /// # 286 | /// # impl Default for Username { 287 | /// # fn default() -> Self { 288 | /// # Self { 289 | /// # inner: "Jane Doe".into(), 290 | /// # } 291 | /// # } 292 | /// # } 293 | /// # 294 | /// # impl fmt::Display for Username { 295 | /// # fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 296 | /// # write!(f, "{}", self.inner) 297 | /// # } 298 | /// # } 299 | /// # 300 | /// #[function_component(Reader)] 301 | /// fn reader() -> Html { 302 | /// let username = use_atom_value::<Username>(); 303 | /// 304 | /// html! { <div>{"Hello, "}{username}</div> } 305 | /// } 306 | /// ``` 307 | #[hook] 308 | pub fn use_atom_value<T>() -> Rc<T> 309 | where 310 | T: Atom + 'static, 311 | { 312 | use_slice_value::<AtomSlice<T>>().inner.clone() 313 | } 314 | -------------------------------------------------------------------------------- /crates/bounce/src/states/mod.rs: -------------------------------------------------------------------------------- 1 | //! a module that contains different states that are supported by bounce. 2 | 3 | pub(crate) mod artifact; 4 | pub(crate) mod atom; 5 | pub(crate) mod future_notion; 6 | pub(crate) mod input_selector; 7 | pub(crate) mod notion; 8 | pub(crate) mod observer; 9 | pub(crate) mod selector; 10 | pub(crate) mod slice; 11 | -------------------------------------------------------------------------------- /crates/bounce/src/states/notion.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use wasm_bindgen::prelude::*; 4 | use yew::prelude::*; 5 | 6 | use crate::root_state::BounceRootState; 7 | 8 | /// A trait to apply a notion on a state. 9 | /// 10 | /// See: [`use_notion_applier`](crate::use_notion_applier) 11 | pub trait WithNotion<T: 'static> { 12 | /// Applies a notion on current state. 13 | /// 14 | /// This always yields a new instance of [`Rc<Self>`] so it can be compared with the previous 15 | /// state using [`PartialEq`]. 16 | fn apply(self: Rc<Self>, notion: Rc<T>) -> Rc<Self>; 17 | } 18 | 19 | /// A hook to create a function that applies a `Notion`. 20 | /// 21 | /// A `Notion` is an action that can be dispatched to any state that accepts the dispatched notion. 22 | /// 23 | /// Any type that is `'static` can be dispatched as a notion. 24 | /// 25 | /// Returns `Rc<dyn Fn(T)>`. 26 | /// 27 | /// # Note 28 | /// 29 | /// When states receives a notion, it will be wrapped in an `Rc<T>`. 30 | /// 31 | /// # Example 32 | /// 33 | /// ``` 34 | /// # use bounce::prelude::*; 35 | /// # use std::fmt; 36 | /// # use std::rc::Rc; 37 | /// # use yew::prelude::*; 38 | /// # use bounce::prelude::*; 39 | /// pub struct Reset; 40 | /// 41 | /// #[derive(PartialEq, Atom)] 42 | /// #[bounce(with_notion(Reset))] // A #[bounce(with_notion(Notion))] needs to be denoted for the notion. 43 | /// struct Username { 44 | /// inner: String, 45 | /// } 46 | /// 47 | /// // A WithNotion<T> is required for each notion denoted in the #[bounce(with_notion)] attribute. 48 | /// impl WithNotion<Reset> for Username { 49 | /// fn apply(self: Rc<Self>, _notion: Rc<Reset>) -> Rc<Self> { 50 | /// Self::default().into() 51 | /// } 52 | /// } 53 | /// 54 | /// // second state 55 | /// #[derive(PartialEq, Atom, Default)] 56 | /// #[bounce(with_notion(Reset))] 57 | /// struct Session { 58 | /// token: Option<String>, 59 | /// } 60 | /// 61 | /// impl WithNotion<Reset> for Session { 62 | /// fn apply(self: Rc<Self>, _notion: Rc<Reset>) -> Rc<Self> { 63 | /// Self::default().into() 64 | /// } 65 | /// } 66 | /// # 67 | /// # impl Default for Username { 68 | /// # fn default() -> Self { 69 | /// # Self { 70 | /// # inner: "Jane Doe".into(), 71 | /// # } 72 | /// # } 73 | /// # } 74 | /// # 75 | /// # #[function_component(Setter)] 76 | /// # fn setter() -> Html { 77 | /// let reset_everything = use_notion_applier::<Reset>(); 78 | /// reset_everything(Reset); 79 | /// # Html::default() 80 | /// # } 81 | /// ``` 82 | #[hook] 83 | pub fn use_notion_applier<T>() -> Rc<dyn Fn(T)> 84 | where 85 | T: 'static, 86 | { 87 | let root = use_context::<BounceRootState>().expect_throw("No bounce root found."); 88 | 89 | // Recreate the dispatch function in case root has changed. 90 | Rc::new(move |notion: T| { 91 | root.apply_notion(Rc::new(notion)); 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /crates/bounce/src/states/observer.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | /// A trait to be notified when the state value changes. 4 | /// 5 | /// Currently, only Slices and Atoms can be observed. This API may be expanded to other state types 6 | /// in the future. 7 | /// 8 | /// # Example 9 | /// 10 | /// ``` 11 | /// use bounce::prelude::*; 12 | /// use std::rc::Rc; 13 | /// 14 | /// #[derive(Atom, PartialEq, Default)] 15 | /// #[bounce(observed)] // observed states need to be denoted with the observed attribute. 16 | /// struct State { 17 | /// value: usize, 18 | /// } 19 | /// 20 | /// impl Observed for State { 21 | /// fn changed(self: Rc<Self>) { 22 | /// // this method will be called when the value of the state changes. 23 | /// } 24 | /// } 25 | /// ``` 26 | pub trait Observed { 27 | /// Notified when the state value has changed. 28 | fn changed(self: Rc<Self>); 29 | } 30 | -------------------------------------------------------------------------------- /crates/bounce/src/states/selector.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use yew::prelude::*; 4 | 5 | use super::input_selector::{use_input_selector_value, InputSelector}; 6 | use crate::root_state::BounceStates; 7 | 8 | /// An auto-updating derived state. 9 | /// 10 | /// It will automatically update when any selected state changes and only notifies registered 11 | /// hooks when `prev_value != next_value`. 12 | pub trait Selector: PartialEq { 13 | /// Selects `self` from existing bounce states. 14 | /// 15 | /// # Panics 16 | /// 17 | /// `states.get_selector_value::<T>()` will panic if you are trying to create a loop by selecting current selector 18 | /// again. 19 | fn select(states: &BounceStates) -> Rc<Self>; 20 | } 21 | 22 | #[derive(PartialEq)] 23 | pub(crate) struct UnitSelector<T> 24 | where 25 | T: Selector + 'static, 26 | { 27 | pub inner: Rc<T>, 28 | } 29 | 30 | impl<T> InputSelector for UnitSelector<T> 31 | where 32 | T: Selector + 'static, 33 | { 34 | type Input = (); 35 | 36 | fn select(states: &BounceStates, _input: Rc<()>) -> Rc<Self> { 37 | Self { 38 | inner: T::select(states), 39 | } 40 | .into() 41 | } 42 | } 43 | 44 | /// A hook to connect to a [`Selector`]. 45 | /// 46 | /// A selector is a derived state which its value is derived from other states. 47 | /// 48 | /// Its value will be automatically re-calculated when any state used in the selector has changed. 49 | /// 50 | /// Returns a [`Rc<T>`]. 51 | /// 52 | /// # Example 53 | /// 54 | /// ``` 55 | /// # use bounce::prelude::*; 56 | /// # use std::rc::Rc; 57 | /// # use yew::prelude::*; 58 | /// # use bounce::prelude::*; 59 | /// # 60 | /// # enum SliceAction { 61 | /// # Increment, 62 | /// # } 63 | /// # 64 | /// #[derive(Default, PartialEq, Slice)] 65 | /// struct Value(i64); 66 | /// # 67 | /// # impl Reducible for Value { 68 | /// # type Action = SliceAction; 69 | /// # 70 | /// # fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> { 71 | /// # match action { 72 | /// # Self::Action::Increment => Self(self.0 + 1).into(), 73 | /// # } 74 | /// # } 75 | /// # } 76 | /// 77 | /// #[derive(PartialEq)] 78 | /// pub struct IsEven { 79 | /// inner: bool, 80 | /// } 81 | /// 82 | /// impl Selector for IsEven { 83 | /// fn select(states: &BounceStates) -> Rc<Self> { 84 | /// let val = states.get_slice_value::<Value>(); 85 | /// 86 | /// Self { 87 | /// inner: val.0 % 2 == 0, 88 | /// } 89 | /// .into() 90 | /// } 91 | /// } 92 | /// # #[function_component(ShowIsEven)] 93 | /// # fn show_is_even() -> Html { 94 | /// let is_even = use_selector_value::<IsEven>(); 95 | /// # Html::default() 96 | /// # } 97 | /// ``` 98 | #[hook] 99 | pub fn use_selector_value<T>() -> Rc<T> 100 | where 101 | T: Selector + 'static, 102 | { 103 | use_input_selector_value::<UnitSelector<T>>(().into()) 104 | .inner 105 | .clone() 106 | } 107 | -------------------------------------------------------------------------------- /crates/bounce/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::cell::RefCell; 3 | use std::fmt; 4 | use std::rc::{Rc, Weak}; 5 | use std::sync::atomic::{AtomicU64, Ordering}; 6 | 7 | use once_cell::sync::Lazy; 8 | use serde::{Deserialize, Serialize}; 9 | use yew::callback::Callback; 10 | 11 | #[derive(PartialEq, Debug, Clone, Eq, Hash, PartialOrd, Ord, Copy, Serialize, Deserialize)] 12 | pub struct Id(u64); 13 | 14 | impl Default for Id { 15 | fn default() -> Self { 16 | static CTR: Lazy<AtomicU64> = Lazy::new(AtomicU64::default); 17 | 18 | Self(CTR.fetch_add(1, Ordering::SeqCst)) 19 | } 20 | } 21 | 22 | impl Id { 23 | pub fn new() -> Self { 24 | Self::default() 25 | } 26 | } 27 | 28 | pub(crate) struct Listener { 29 | _listener: Rc<dyn Any>, 30 | } 31 | 32 | impl Listener { 33 | pub fn new(inner: Rc<dyn Any>) -> Self { 34 | Self { _listener: inner } 35 | } 36 | } 37 | 38 | impl fmt::Debug for Listener { 39 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 40 | f.debug_struct("Listener").finish_non_exhaustive() 41 | } 42 | } 43 | 44 | pub(crate) type ListenerVec<T> = Vec<Weak<Callback<Rc<T>>>>; 45 | 46 | pub(crate) fn notify_listeners<T>(listeners: Rc<RefCell<ListenerVec<T>>>, val: Rc<T>) { 47 | let callables = { 48 | let mut callbacks_ref = listeners.borrow_mut(); 49 | 50 | // Any gone weak references are removed when called. 51 | let (callbacks, callbacks_weak) = callbacks_ref.iter().cloned().fold( 52 | (Vec::new(), Vec::new()), 53 | |(mut callbacks, mut callbacks_weak), m| { 54 | if let Some(m_strong) = m.clone().upgrade() { 55 | callbacks.push(m_strong); 56 | callbacks_weak.push(m); 57 | } 58 | 59 | (callbacks, callbacks_weak) 60 | }, 61 | ); 62 | 63 | *callbacks_ref = callbacks_weak; 64 | 65 | callbacks 66 | }; 67 | 68 | for callback in callables { 69 | callback.emit(val.clone()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/bounce/tests/init_states.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anymap2::AnyMap; 4 | use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; 5 | 6 | wasm_bindgen_test_configure!(run_in_browser); 7 | 8 | use bounce::prelude::*; 9 | use bounce::BounceRoot; 10 | use gloo::timers::future::sleep; 11 | use gloo::utils::document; 12 | use yew::prelude::*; 13 | 14 | async fn get_text_content<S: AsRef<str>>(selector: S) -> String { 15 | sleep(Duration::ZERO).await; 16 | 17 | document() 18 | .query_selector(selector.as_ref()) 19 | .unwrap() 20 | .unwrap() 21 | .text_content() 22 | .unwrap() 23 | } 24 | 25 | #[derive(Atom, PartialEq, Default)] 26 | struct State { 27 | inner: u32, 28 | } 29 | 30 | #[function_component(Comp)] 31 | fn comp() -> Html { 32 | let a = use_atom_value::<State>(); 33 | 34 | html! { 35 | <div> 36 | <div id="a">{a.inner}</div> 37 | </div> 38 | } 39 | } 40 | 41 | #[test] 42 | async fn test_without_init_states() { 43 | #[function_component(Root)] 44 | fn root() -> Html { 45 | html! { 46 | <BounceRoot> 47 | <Comp /> 48 | </BounceRoot> 49 | } 50 | } 51 | 52 | yew::Renderer::<Root>::with_root(document().query_selector("#output").unwrap().unwrap()) 53 | .render(); 54 | 55 | let s = get_text_content("#a").await; 56 | assert_eq!(s, "0"); 57 | } 58 | 59 | #[test] 60 | async fn test_with_init_states() { 61 | #[function_component(Root)] 62 | fn root() -> Html { 63 | fn get_init_states(_: ()) -> AnyMap { 64 | let mut map = AnyMap::new(); 65 | map.insert(State { inner: 1 }); 66 | 67 | map 68 | } 69 | 70 | html! { 71 | <BounceRoot {get_init_states}> 72 | <Comp /> 73 | </BounceRoot> 74 | } 75 | } 76 | 77 | yew::Renderer::<Root>::with_root(document().query_selector("#output").unwrap().unwrap()) 78 | .render(); 79 | 80 | let s = get_text_content("#a").await; 81 | assert_eq!(s, "1"); 82 | } 83 | -------------------------------------------------------------------------------- /crates/bounce/tests/notion.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | use std::time::Duration; 3 | 4 | use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; 5 | 6 | wasm_bindgen_test_configure!(run_in_browser); 7 | 8 | use bounce::prelude::*; 9 | use bounce::BounceRoot; 10 | use gloo::timers::future::sleep; 11 | use gloo::utils::document; 12 | use yew::prelude::*; 13 | 14 | async fn get_text_content<S: AsRef<str>>(selector: S) -> String { 15 | sleep(Duration::ZERO).await; 16 | 17 | document() 18 | .query_selector(selector.as_ref()) 19 | .unwrap() 20 | .unwrap() 21 | .text_content() 22 | .unwrap() 23 | } 24 | 25 | #[test] 26 | async fn test_notion_generic() { 27 | #[derive(Atom, PartialEq, Default)] 28 | #[bounce(with_notion(State<T>))] 29 | struct State<T> 30 | where 31 | T: PartialEq + Default + 'static, 32 | { 33 | inner: T, 34 | } 35 | 36 | impl<T> WithNotion<State<T>> for State<T> 37 | where 38 | T: PartialEq + Default + 'static, 39 | { 40 | fn apply(self: Rc<Self>, notion: Rc<State<T>>) -> Rc<Self> { 41 | notion 42 | } 43 | } 44 | 45 | #[function_component(Comp)] 46 | fn comp() -> Html { 47 | let a = use_atom::<State<u32>>(); 48 | let b = use_atom::<State<u64>>(); 49 | 50 | { 51 | let a = a.clone(); 52 | let b = b.clone(); 53 | use_effect_with((), move |_| { 54 | a.set(State { inner: 1 }); 55 | b.set(State { inner: 2 }); 56 | 57 | || {} 58 | }); 59 | } 60 | 61 | html! { 62 | <div> 63 | <div id="a">{a.inner}</div> 64 | <div id="b">{b.inner}</div> 65 | </div> 66 | } 67 | } 68 | 69 | #[function_component(Root)] 70 | fn root() -> Html { 71 | html! { 72 | <BounceRoot> 73 | <Comp /> 74 | </BounceRoot> 75 | } 76 | } 77 | 78 | yew::Renderer::<Root>::with_root(document().query_selector("#output").unwrap().unwrap()) 79 | .render(); 80 | 81 | let s = get_text_content("#a").await; 82 | assert_eq!(s, "1"); 83 | 84 | let s = get_text_content("#b").await; 85 | assert_eq!(s, "2"); 86 | } 87 | -------------------------------------------------------------------------------- /crates/bounce/tests/query.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "query")] 2 | 3 | use std::convert::Infallible; 4 | use std::rc::Rc; 5 | use std::time::Duration; 6 | 7 | use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; 8 | 9 | wasm_bindgen_test_configure!(run_in_browser); 10 | 11 | use async_trait::async_trait; 12 | use bounce::prelude::*; 13 | use bounce::query::{use_query_value, Query, QueryResult}; 14 | use bounce::BounceRoot; 15 | use gloo::timers::future::sleep; 16 | use gloo::utils::document; 17 | use yew::platform::spawn_local; 18 | use yew::prelude::*; 19 | 20 | async fn get_text_content<S: AsRef<str>>(selector: S) -> String { 21 | sleep(Duration::ZERO).await; 22 | 23 | document() 24 | .query_selector(selector.as_ref()) 25 | .unwrap() 26 | .unwrap() 27 | .text_content() 28 | .unwrap() 29 | } 30 | 31 | #[test] 32 | async fn test_query_requery_upon_state_change() { 33 | #[derive(PartialEq, Eq, Default, Atom)] 34 | pub struct MyState { 35 | inner: usize, 36 | } 37 | 38 | #[derive(PartialEq, Eq, Default)] 39 | pub struct MyQuery { 40 | inner: usize, 41 | } 42 | 43 | #[async_trait(?Send)] 44 | impl Query for MyQuery { 45 | type Input = (); 46 | type Error = Infallible; 47 | 48 | async fn query(states: &BounceStates, _input: Rc<()>) -> QueryResult<Self> { 49 | let inner = states.get_atom_value::<MyState>().inner; 50 | 51 | sleep(Duration::ZERO).await; 52 | 53 | Ok(MyQuery { inner }.into()) 54 | } 55 | } 56 | 57 | #[function_component(Comp)] 58 | fn comp() -> Html { 59 | let my_query = use_query_value::<MyQuery>(().into()); 60 | let set_my_state = use_atom_setter(); 61 | 62 | use_effect_with((), move |_| { 63 | spawn_local(async move { 64 | sleep(Duration::from_millis(50)).await; 65 | 66 | set_my_state(MyState { inner: 1 }); 67 | }); 68 | 69 | || {} 70 | }); 71 | 72 | match my_query.result() { 73 | None => { 74 | html! { <div id="content">{"Loading..."}</div> } 75 | } 76 | Some(Ok(m)) => { 77 | html! { <div id="content">{format!("value: {}", m.inner)}</div> } 78 | } 79 | Some(Err(_)) => unreachable!(), 80 | } 81 | } 82 | 83 | #[function_component(App)] 84 | fn app() -> Html { 85 | html! { 86 | <BounceRoot> 87 | <Comp /> 88 | </BounceRoot> 89 | } 90 | } 91 | 92 | yew::Renderer::<App>::with_root(document().query_selector("#output").unwrap().unwrap()) 93 | .render(); 94 | 95 | let s = get_text_content("#content").await; 96 | assert_eq!(s, "Loading..."); 97 | 98 | let s = get_text_content("#content").await; 99 | assert_eq!(s, "value: 0"); 100 | 101 | sleep(Duration::from_millis(100)).await; 102 | 103 | let s = get_text_content("#content").await; 104 | assert_eq!(s, "value: 1"); 105 | } 106 | -------------------------------------------------------------------------------- /examples/divisibility-input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "divisibility-input" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bounce = { path = "../../crates/bounce" } 11 | yew = { version = "0.21", features = ["csr"] } 12 | stylist = { version = "0.13", features = ["yew"] } 13 | log = "0.4.19" 14 | console_log = { version = "1.0.0", features = ["color"] } 15 | wasm-bindgen = "0.2.87" 16 | 17 | [dev-dependencies] 18 | wasm-bindgen-test = "0.3.37" 19 | gloo = { version = "0.10.0", features = ["futures"] } 20 | web-sys = "0.3.64" 21 | -------------------------------------------------------------------------------- /examples/divisibility-input/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8"> 5 | <title>Divisibility Input Example 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/divisibility-input/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use bounce::prelude::*; 4 | use bounce::BounceRoot; 5 | use log::Level; 6 | use stylist::yew::styled_component; 7 | use yew::prelude::*; 8 | 9 | #[derive(Debug)] 10 | pub enum SliceAction { 11 | Increment, 12 | } 13 | 14 | #[derive(Default, PartialEq, Slice, Eq)] 15 | pub struct Value(i64); 16 | 17 | impl Reducible for Value { 18 | type Action = SliceAction; 19 | 20 | fn reduce(self: Rc, action: Self::Action) -> Rc { 21 | match action { 22 | Self::Action::Increment => Self(self.0 + 1).into(), 23 | } 24 | } 25 | } 26 | 27 | #[derive(PartialEq, Eq)] 28 | pub struct DivBy { 29 | inner: bool, 30 | } 31 | 32 | impl InputSelector for DivBy { 33 | type Input = i64; 34 | 35 | fn select(states: &BounceStates, input: Rc) -> Rc { 36 | let val = states.get_slice_value::(); 37 | 38 | Self { 39 | inner: val.0 % *input == 0, 40 | } 41 | .into() 42 | } 43 | } 44 | 45 | #[styled_component(CompIsEven)] 46 | fn comp_is_even() -> Html { 47 | let value = use_slice_value::(); 48 | let is_even = use_input_selector_value::(2.into()); 49 | 50 | let maybe_not = if is_even.inner { "" } else { " not" }; 51 | 52 | html! { 53 |
54 |

{format!("{} is{} even.", value.0, maybe_not)}

55 |
56 | } 57 | } 58 | 59 | #[styled_component(CompDiv3)] 60 | fn comp_div_3() -> Html { 61 | let value = use_slice_value::(); 62 | let is_div_3 = use_input_selector_value::(3.into()); 63 | 64 | let maybe_not = if is_div_3.inner { "" } else { " not" }; 65 | 66 | html! { 67 |
68 |

{format!("{} is{} divisible by 3.", value.0, maybe_not)}

69 |
70 | } 71 | } 72 | 73 | #[styled_component(CompDiv4)] 74 | fn comp_div_4() -> Html { 75 | let value = use_slice_value::(); 76 | let is_div_4 = use_input_selector_value::(4.into()); 77 | 78 | let maybe_not = if is_div_4.inner { "" } else { " not" }; 79 | 80 | html! { 81 |
82 |

{format!("{} is{} divisible by 4.", value.0, maybe_not)}

83 |
84 | } 85 | } 86 | 87 | #[styled_component(Setters)] 88 | fn setters() -> Html { 89 | let dispatch = use_slice_dispatch::(); 90 | 91 | let inc = Callback::from(move |_| dispatch(SliceAction::Increment)); 92 | 93 | html! { 94 |
95 | 96 |
97 | } 98 | } 99 | 100 | #[styled_component(App)] 101 | fn app() -> Html { 102 | html! { 103 | 104 |
105 |
111 | 112 | 113 | 114 |
115 | 116 |
117 |
118 | } 119 | } 120 | 121 | fn main() { 122 | console_log::init_with_level(Level::Trace).expect("Failed to initialise Log!"); 123 | yew::Renderer::::new().render(); 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | use super::*; 129 | use gloo::timers::future::sleep; 130 | use gloo::utils::document; 131 | use std::time::Duration; 132 | use wasm_bindgen::JsCast; 133 | use wasm_bindgen_test::*; 134 | use web_sys::HtmlElement; 135 | 136 | wasm_bindgen_test_configure!(run_in_browser); 137 | 138 | #[wasm_bindgen_test] 139 | async fn test_divisibility_input() { 140 | yew::Renderer::::with_root(document().query_selector("#output").unwrap().unwrap()) 141 | .render(); 142 | 143 | sleep(Duration::ZERO).await; 144 | 145 | let output = document() 146 | .query_selector("#output") 147 | .unwrap() 148 | .unwrap() 149 | .inner_html(); 150 | 151 | assert!(output.contains("0 is even")); 152 | assert!(output.contains("0 is divisible by 3")); 153 | assert!(output.contains("0 is divisible by 4")); 154 | 155 | document() 156 | .query_selector("#inc-btn") 157 | .unwrap() 158 | .unwrap() 159 | .unchecked_into::() 160 | .click(); 161 | 162 | sleep(Duration::ZERO).await; 163 | 164 | let output = document() 165 | .query_selector("#output") 166 | .unwrap() 167 | .unwrap() 168 | .inner_html(); 169 | 170 | assert!(output.contains("1 is not even")); 171 | assert!(output.contains("1 is not divisible by 3")); 172 | assert!(output.contains("1 is not divisible by 4")); 173 | 174 | document() 175 | .query_selector("#inc-btn") 176 | .unwrap() 177 | .unwrap() 178 | .unchecked_into::() 179 | .click(); 180 | 181 | sleep(Duration::ZERO).await; 182 | 183 | let output = document() 184 | .query_selector("#output") 185 | .unwrap() 186 | .unwrap() 187 | .inner_html(); 188 | 189 | assert!(output.contains("2 is even")); 190 | assert!(output.contains("2 is not divisible by 3")); 191 | assert!(output.contains("2 is not divisible by 4")); 192 | 193 | document() 194 | .query_selector("#inc-btn") 195 | .unwrap() 196 | .unwrap() 197 | .unchecked_into::() 198 | .click(); 199 | 200 | sleep(Duration::ZERO).await; 201 | 202 | let output = document() 203 | .query_selector("#output") 204 | .unwrap() 205 | .unwrap() 206 | .inner_html(); 207 | 208 | assert!(output.contains("3 is not even")); 209 | assert!(output.contains("3 is divisible by 3")); 210 | assert!(output.contains("3 is not divisible by 4")); 211 | 212 | document() 213 | .query_selector("#inc-btn") 214 | .unwrap() 215 | .unwrap() 216 | .unchecked_into::() 217 | .click(); 218 | 219 | sleep(Duration::ZERO).await; 220 | 221 | let output = document() 222 | .query_selector("#output") 223 | .unwrap() 224 | .unwrap() 225 | .inner_html(); 226 | 227 | assert!(output.contains("4 is even")); 228 | assert!(output.contains("4 is not divisible by 3")); 229 | assert!(output.contains("4 is divisible by 4")); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /examples/divisibility/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "divisibility" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bounce = { path = "../../crates/bounce" } 11 | yew = { version = "0.21", features = ["csr"] } 12 | stylist = { version = "0.13", features = ["yew"] } 13 | log = "0.4.19" 14 | console_log = { version = "1.0.0", features = ["color"] } 15 | wasm-bindgen = "0.2.87" 16 | 17 | [dev-dependencies] 18 | wasm-bindgen-test = "0.3.37" 19 | gloo = { version = "0.10.0", features = ["futures"] } 20 | web-sys = "0.3.64" 21 | -------------------------------------------------------------------------------- /examples/divisibility/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Divisibility Example 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/divisibility/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use bounce::prelude::*; 4 | use bounce::BounceRoot; 5 | use log::Level; 6 | use stylist::yew::styled_component; 7 | use yew::prelude::*; 8 | 9 | #[derive(Debug)] 10 | pub enum SliceAction { 11 | Increment, 12 | } 13 | 14 | #[derive(Default, PartialEq, Slice, Eq)] 15 | pub struct Value(i64); 16 | 17 | impl Reducible for Value { 18 | type Action = SliceAction; 19 | 20 | fn reduce(self: Rc, action: Self::Action) -> Rc { 21 | match action { 22 | Self::Action::Increment => Self(self.0 + 1).into(), 23 | } 24 | } 25 | } 26 | 27 | #[derive(PartialEq, Eq)] 28 | pub struct IsEven { 29 | inner: bool, 30 | } 31 | 32 | impl Selector for IsEven { 33 | fn select(states: &BounceStates) -> Rc { 34 | let val = states.get_slice_value::(); 35 | 36 | Self { 37 | inner: val.0 % 2 == 0, 38 | } 39 | .into() 40 | } 41 | } 42 | 43 | #[derive(PartialEq, Eq)] 44 | pub struct Div3 { 45 | inner: bool, 46 | } 47 | 48 | impl Selector for Div3 { 49 | fn select(states: &BounceStates) -> Rc { 50 | let val = states.get_slice_value::(); 51 | 52 | Self { 53 | inner: val.0 % 3 == 0, 54 | } 55 | .into() 56 | } 57 | } 58 | 59 | #[derive(PartialEq, Eq)] 60 | pub struct Div4 { 61 | inner: bool, 62 | } 63 | 64 | impl Selector for Div4 { 65 | fn select(states: &BounceStates) -> Rc { 66 | let val = states.get_slice_value::(); 67 | 68 | Self { 69 | inner: val.0 % 4 == 0, 70 | } 71 | .into() 72 | } 73 | } 74 | 75 | #[styled_component(CompIsEven)] 76 | fn comp_is_even() -> Html { 77 | let value = use_slice_value::(); 78 | let is_even = use_selector_value::(); 79 | 80 | let maybe_not = if is_even.inner { "" } else { " not" }; 81 | 82 | html! { 83 |
84 |

{format!("{} is{} even.", value.0, maybe_not)}

85 |
86 | } 87 | } 88 | 89 | #[styled_component(CompDiv3)] 90 | fn comp_div_3() -> Html { 91 | let value = use_slice_value::(); 92 | let is_div_3 = use_selector_value::(); 93 | 94 | let maybe_not = if is_div_3.inner { "" } else { " not" }; 95 | 96 | html! { 97 |
98 |

{format!("{} is{} divisible by 3.", value.0, maybe_not)}

99 |
100 | } 101 | } 102 | 103 | #[styled_component(CompDiv4)] 104 | fn comp_div_4() -> Html { 105 | let value = use_slice_value::(); 106 | let is_div_4 = use_selector_value::(); 107 | 108 | let maybe_not = if is_div_4.inner { "" } else { " not" }; 109 | 110 | html! { 111 |
112 |

{format!("{} is{} divisible by 4.", value.0, maybe_not)}

113 |
114 | } 115 | } 116 | 117 | #[styled_component(Setters)] 118 | fn setters() -> Html { 119 | let dispatch = use_slice_dispatch::(); 120 | 121 | let inc = Callback::from(move |_| dispatch(SliceAction::Increment)); 122 | 123 | html! { 124 |
125 | 126 |
127 | } 128 | } 129 | 130 | #[styled_component(App)] 131 | fn app() -> Html { 132 | html! { 133 | 134 |
135 |
141 | 142 | 143 | 144 |
145 | 146 |
147 |
148 | } 149 | } 150 | 151 | fn main() { 152 | console_log::init_with_level(Level::Trace).expect("Failed to initialise Log!"); 153 | yew::Renderer::::new().render(); 154 | } 155 | 156 | #[cfg(test)] 157 | mod tests { 158 | use super::*; 159 | use gloo::timers::future::sleep; 160 | use gloo::utils::document; 161 | use std::time::Duration; 162 | use wasm_bindgen::JsCast; 163 | use wasm_bindgen_test::*; 164 | use web_sys::HtmlElement; 165 | 166 | wasm_bindgen_test_configure!(run_in_browser); 167 | 168 | #[wasm_bindgen_test] 169 | async fn test_divisibility() { 170 | yew::Renderer::::with_root(document().query_selector("#output").unwrap().unwrap()) 171 | .render(); 172 | 173 | sleep(Duration::ZERO).await; 174 | 175 | let output = document() 176 | .query_selector("#output") 177 | .unwrap() 178 | .unwrap() 179 | .inner_html(); 180 | 181 | assert!(output.contains("0 is even")); 182 | assert!(output.contains("0 is divisible by 3")); 183 | assert!(output.contains("0 is divisible by 4")); 184 | 185 | document() 186 | .query_selector("#inc-btn") 187 | .unwrap() 188 | .unwrap() 189 | .unchecked_into::() 190 | .click(); 191 | 192 | sleep(Duration::ZERO).await; 193 | 194 | let output = document() 195 | .query_selector("#output") 196 | .unwrap() 197 | .unwrap() 198 | .inner_html(); 199 | 200 | assert!(output.contains("1 is not even")); 201 | assert!(output.contains("1 is not divisible by 3")); 202 | assert!(output.contains("1 is not divisible by 4")); 203 | 204 | document() 205 | .query_selector("#inc-btn") 206 | .unwrap() 207 | .unwrap() 208 | .unchecked_into::() 209 | .click(); 210 | 211 | sleep(Duration::ZERO).await; 212 | 213 | let output = document() 214 | .query_selector("#output") 215 | .unwrap() 216 | .unwrap() 217 | .inner_html(); 218 | 219 | assert!(output.contains("2 is even")); 220 | assert!(output.contains("2 is not divisible by 3")); 221 | assert!(output.contains("2 is not divisible by 4")); 222 | 223 | document() 224 | .query_selector("#inc-btn") 225 | .unwrap() 226 | .unwrap() 227 | .unchecked_into::() 228 | .click(); 229 | 230 | sleep(Duration::ZERO).await; 231 | 232 | let output = document() 233 | .query_selector("#output") 234 | .unwrap() 235 | .unwrap() 236 | .inner_html(); 237 | 238 | assert!(output.contains("3 is not even")); 239 | assert!(output.contains("3 is divisible by 3")); 240 | assert!(output.contains("3 is not divisible by 4")); 241 | 242 | document() 243 | .query_selector("#inc-btn") 244 | .unwrap() 245 | .unwrap() 246 | .unchecked_into::() 247 | .click(); 248 | 249 | sleep(Duration::ZERO).await; 250 | 251 | let output = document() 252 | .query_selector("#output") 253 | .unwrap() 254 | .unwrap() 255 | .inner_html(); 256 | 257 | assert!(output.contains("4 is even")); 258 | assert!(output.contains("4 is not divisible by 3")); 259 | assert!(output.contains("4 is divisible by 4")); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /examples/helmet-ssr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "helmet-ssr" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [[bin]] 8 | name = "helmet-ssr-client" 9 | required-features = ["csr"] 10 | 11 | [[bin]] 12 | name = "helmet-ssr-server" 13 | required-features = ["ssr"] 14 | 15 | [dependencies] 16 | bounce = { path = "../../crates/bounce", features = ["helmet"] } 17 | yew = { version = "0.21" } 18 | log = "0.4.19" 19 | console_log = { version = "1.0.0", features = ["color"] } 20 | wasm-bindgen = "0.2.87" 21 | yew-router = "0.18" 22 | gloo = { version = "0.10.0", features = ["futures"] } 23 | web-sys= "0.3.64" 24 | bytes = "1.4.0" 25 | 26 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 27 | tokio = { version = "1.29.1", features = ["full"] } 28 | env_logger = "0.10" 29 | clap = { version = "4.3.10", features = ["derive"] } 30 | warp = "0.3" 31 | futures = "0.3" 32 | html_parser = "0.7.0" 33 | 34 | [dev-dependencies] 35 | wasm-bindgen-test = "0.3.37" 36 | 37 | [features] 38 | csr = ["yew/csr", "yew/hydration"] 39 | ssr = ["yew/ssr", "bounce/ssr"] 40 | -------------------------------------------------------------------------------- /examples/helmet-ssr/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/helmet-ssr/src/bin/helmet-ssr-client.rs: -------------------------------------------------------------------------------- 1 | use helmet_ssr::App; 2 | use log::Level; 3 | 4 | fn main() { 5 | console_log::init_with_level(Level::Trace).expect("Failed to initialise Log!"); 6 | yew::Renderer::::new().hydrate(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/helmet-ssr/src/bin/helmet-ssr-server.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | mod target_non_wasm32 { 3 | use std::collections::HashMap; 4 | use std::fmt::Write; 5 | use std::path::PathBuf; 6 | 7 | use bounce::helmet::{self, HelmetTag}; 8 | use clap::Parser; 9 | use helmet_ssr::{ServerApp, ServerAppProps}; 10 | use warp::path::FullPath; 11 | use warp::Filter; 12 | 13 | /// A basic example 14 | #[derive(Parser, Debug)] 15 | struct Opt { 16 | /// the "dist" created by trunk directory to be served for hydration. 17 | #[clap(short, long)] 18 | dir: PathBuf, 19 | } 20 | 21 | async fn render( 22 | script_content: String, 23 | url: String, 24 | queries: HashMap, 25 | ) -> String { 26 | let (renderer, writer) = helmet::render_static(); 27 | 28 | let body_s = yew::ServerRenderer::::with_props(move || ServerAppProps { 29 | url: url.into(), 30 | queries, 31 | helmet_writer: writer, 32 | }) 33 | .render() 34 | .await; 35 | 36 | let mut html_tag: Option = None; 37 | let mut body_tag: Option = None; 38 | 39 | let mut helmet_s = "".to_string(); 40 | 41 | let rendered: Vec = renderer.render().await; 42 | let mut s = String::with_capacity(body_s.len()); 43 | 44 | for tag in rendered { 45 | let _ = tag.write_static(&mut helmet_s); 46 | 47 | match tag { 48 | HelmetTag::Html { .. } => { 49 | html_tag = Some(tag); 50 | } 51 | HelmetTag::Body { .. } => { 52 | body_tag = Some(tag); 53 | } 54 | _ => {} 55 | } 56 | } 57 | 58 | let _ = writeln!(s, ""); 59 | { 60 | let mut html_s = String::new(); 61 | html_tag.map(|m| m.write_attrs(&mut html_s)); 62 | 63 | if html_s.is_empty() { 64 | let _ = writeln!(s, ""); 65 | } else { 66 | let _ = writeln!(s, ""); 67 | } 68 | } 69 | let _ = writeln!(s, ""); 70 | s.push_str(&helmet_s); 71 | let _ = writeln!(s, r#""#); 72 | let _ = writeln!(s, ""); 73 | 74 | { 75 | let mut body_s = String::new(); 76 | body_tag.map(|m| m.write_attrs(&mut body_s)); 77 | 78 | if body_s.is_empty() { 79 | let _ = writeln!(s, ""); 80 | } else { 81 | let _ = writeln!(s, ""); 82 | } 83 | } 84 | s.push_str(&body_s); 85 | 86 | let _ = writeln!(s, ""); 87 | let _ = writeln!(s, ""); 88 | 89 | s 90 | } 91 | 92 | fn extract_script_content(index_html: &str) -> String { 93 | fn extract(nodes: I) -> Option 94 | where 95 | I: IntoIterator, 96 | { 97 | fn extract_text(nodes: I) -> Option 98 | where 99 | I: IntoIterator, 100 | { 101 | for node in nodes { 102 | match node { 103 | html_parser::Node::Comment(_) => {} 104 | html_parser::Node::Element(_) => {} 105 | html_parser::Node::Text(t) => { 106 | return Some(t); 107 | } 108 | } 109 | } 110 | 111 | None 112 | } 113 | 114 | let nodes = nodes.into_iter(); 115 | for node in nodes { 116 | match node { 117 | html_parser::Node::Comment(_) => {} 118 | html_parser::Node::Element(e) => { 119 | if e.name.to_lowercase().as_str() == "script" { 120 | return extract_text(e.children); 121 | } 122 | 123 | if let Some(m) = extract(e.children) { 124 | return Some(m); 125 | } 126 | } 127 | html_parser::Node::Text(_) => {} 128 | } 129 | } 130 | 131 | None 132 | } 133 | 134 | let dom = html_parser::Dom::parse(index_html).expect("failed to parse"); 135 | 136 | extract(dom.children).expect("failed to find script tag") 137 | } 138 | 139 | pub async fn main() { 140 | env_logger::init(); 141 | 142 | let opts = Opt::parse(); 143 | 144 | let index_html_s = tokio::fs::read_to_string(opts.dir.join("index.html")) 145 | .await 146 | .expect("failed to read index.html"); 147 | 148 | let script_content = extract_script_content(&index_html_s); 149 | 150 | let render_f = warp::path::full().and(warp::query()).then( 151 | move |path: FullPath, queries: HashMap| { 152 | let script_content = script_content.clone(); 153 | 154 | async move { 155 | warp::reply::html( 156 | render(script_content, path.as_str().to_string(), queries).await, 157 | ) 158 | } 159 | }, 160 | ); 161 | 162 | let routes = warp::path::end() 163 | .and(render_f.clone()) 164 | .or(warp::path("index.html").and(render_f.clone())) 165 | .or(warp::fs::dir(opts.dir.clone())) 166 | .or(render_f); 167 | 168 | println!("You can view the website at: http://localhost:8080/"); 169 | warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; 170 | } 171 | } 172 | 173 | #[cfg(not(target_arch = "wasm32"))] 174 | #[tokio::main] 175 | async fn main() { 176 | target_non_wasm32::main().await; 177 | } 178 | 179 | #[cfg(target_arch = "wasm32")] 180 | fn main() {} 181 | -------------------------------------------------------------------------------- /examples/helmet-ssr/src/lib.rs: -------------------------------------------------------------------------------- 1 | use bounce::helmet::{Helmet, HelmetBridge}; 2 | use bounce::BounceRoot; 3 | use yew::prelude::*; 4 | use yew_router::prelude::*; 5 | 6 | #[derive(PartialEq, Eq)] 7 | pub struct Title { 8 | value: String, 9 | } 10 | 11 | #[derive(Routable, PartialEq, Clone, Eq)] 12 | pub enum Route { 13 | #[at("/a")] 14 | A, 15 | #[at("/b")] 16 | B, 17 | #[at("/")] 18 | Home, 19 | } 20 | 21 | #[function_component(A)] 22 | fn a() -> Html { 23 | html! { 24 | <> 25 | 26 | {"Page A"} 27 | 28 | 29 | 30 |
{"This is page A."}
31 | 32 | } 33 | } 34 | 35 | #[function_component(B)] 36 | fn b() -> Html { 37 | html! { 38 |
{"This is page B. This page does not have a specific title, so default title will be used instead."}
39 | } 40 | } 41 | 42 | #[function_component(Home)] 43 | fn home() -> Html { 44 | html! { 45 | <> 46 | 47 | {"Home Page"} 48 | 49 | 50 |
{"This is home page."}
51 | 52 | } 53 | } 54 | 55 | #[function_component(Links)] 56 | fn links() -> Html { 57 | html! { 58 | <> 59 |
to={Route::A}>{"Go to A"}>
60 |
to={Route::B}>{"Go to B"}>
61 |
to={Route::Home}>{"Go to Home"}>
62 | 63 | } 64 | } 65 | 66 | fn render_fn(route: Route) -> Html { 67 | match route { 68 | Route::A => html! {}, 69 | Route::B => html! {}, 70 | Route::Home => html! {}, 71 | } 72 | } 73 | 74 | fn format_title(s: AttrValue) -> AttrValue { 75 | format!("{s} - Example").into() 76 | } 77 | 78 | #[function_component(App)] 79 | pub fn app() -> Html { 80 | html! { 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | render={render_fn} /> 89 | 90 | 91 | 92 | } 93 | } 94 | 95 | #[cfg(feature = "ssr")] 96 | mod feat_ssr { 97 | use super::*; 98 | 99 | use std::collections::HashMap; 100 | 101 | use bounce::helmet::StaticWriter; 102 | use yew_router::history::{AnyHistory, History, MemoryHistory}; 103 | 104 | #[derive(Properties, PartialEq, Eq, Debug)] 105 | 106 | pub struct ServerAppProps { 107 | pub url: AttrValue, 108 | pub queries: HashMap, 109 | pub helmet_writer: StaticWriter, 110 | } 111 | 112 | #[function_component] 113 | pub fn ServerApp(props: &ServerAppProps) -> Html { 114 | let history = AnyHistory::from(MemoryHistory::new()); 115 | history 116 | .push_with_query(&*props.url, &props.queries) 117 | .unwrap(); 118 | 119 | html! { 120 | 121 | 126 | 127 | 128 | 129 | 130 | 131 | render={render_fn} /> 132 | 133 | 134 | 135 | } 136 | } 137 | } 138 | 139 | #[cfg(feature = "ssr")] 140 | pub use feat_ssr::*; 141 | 142 | #[cfg(test)] 143 | #[cfg(feature = "ssr")] 144 | #[cfg(not(target_arch = "wasm32"))] 145 | mod ssr_tests { 146 | use std::collections::BTreeMap; 147 | use std::sync::Arc; 148 | 149 | use super::*; 150 | use bounce::helmet::{self, HelmetTag}; 151 | 152 | #[tokio::test] 153 | async fn test_render() { 154 | let (renderer, writer) = helmet::render_static(); 155 | 156 | yew::ServerRenderer::::with_props(move || ServerAppProps { 157 | url: "/".into(), 158 | queries: Default::default(), 159 | helmet_writer: writer, 160 | }) 161 | .render() 162 | .await; 163 | 164 | let rendered: Vec = renderer.render().await; 165 | let expected = vec![ 166 | HelmetTag::Title("Home Page - Example".into()), 167 | HelmetTag::Meta { 168 | attrs: { 169 | let mut map = BTreeMap::new(); 170 | map.insert(Arc::from("charset"), Arc::from("utf-8")); 171 | map 172 | }, 173 | }, 174 | HelmetTag::Meta { 175 | attrs: { 176 | let mut map = BTreeMap::new(); 177 | map.insert(Arc::from("content"), Arc::from("home page")); 178 | map.insert(Arc::from("name"), Arc::from("description")); 179 | map 180 | }, 181 | }, 182 | ]; 183 | 184 | assert_eq!(rendered, expected); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /examples/helmet-title/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "helmet-title" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bounce = { path = "../../crates/bounce", features = ["helmet"] } 11 | yew = { version = "0.21", features = ["csr"] } 12 | log = "0.4.19" 13 | console_log = { version = "1.0.0", features = ["color"] } 14 | wasm-bindgen = "0.2.87" 15 | yew-router = "0.18" 16 | gloo = { version = "0.10.0", features = ["futures"] } 17 | web-sys= "0.3.64" 18 | 19 | [dev-dependencies] 20 | wasm-bindgen-test = "0.3.37" 21 | -------------------------------------------------------------------------------- /examples/helmet-title/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Helmet Title Example 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/helmet-title/src/main.rs: -------------------------------------------------------------------------------- 1 | use bounce::helmet::{Helmet, HelmetBridge}; 2 | use bounce::BounceRoot; 3 | use log::Level; 4 | use yew::prelude::*; 5 | use yew_router::prelude::*; 6 | 7 | #[derive(PartialEq, Eq)] 8 | pub struct Title { 9 | value: String, 10 | } 11 | 12 | #[derive(Routable, PartialEq, Clone, Eq)] 13 | pub enum Route { 14 | #[at("/a")] 15 | A, 16 | #[at("/b")] 17 | B, 18 | #[at("/")] 19 | Home, 20 | } 21 | 22 | #[function_component(A)] 23 | fn a() -> Html { 24 | html! { 25 | <> 26 | 27 | {"Page A"} 28 | 29 | 30 | 31 |
{"This is page A."}
32 | 33 | } 34 | } 35 | 36 | #[function_component(B)] 37 | fn b() -> Html { 38 | html! { 39 |
{"This is page B. This page does not have a specific title, so default title will be used instead."}
40 | } 41 | } 42 | 43 | #[function_component(Home)] 44 | fn home() -> Html { 45 | html! { 46 | <> 47 | 48 | {"Home Page"} 49 | 50 | 51 |
{"This is home page."}
52 | 53 | } 54 | } 55 | 56 | #[function_component(Links)] 57 | fn links() -> Html { 58 | html! { 59 | <> 60 |
to={Route::A}>{"Go to A"}>
61 |
to={Route::B}>{"Go to B"}>
62 |
to={Route::Home}>{"Go to Home"}>
63 | 64 | } 65 | } 66 | 67 | fn render_fn(route: Route) -> Html { 68 | match route { 69 | Route::A => html! {
}, 70 | Route::B => html! {}, 71 | Route::Home => html! {}, 72 | } 73 | } 74 | 75 | fn format_title(s: AttrValue) -> AttrValue { 76 | format!("{s} - Example").into() 77 | } 78 | 79 | #[function_component(App)] 80 | fn app() -> Html { 81 | html! { 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | render={render_fn} /> 90 | 91 | 92 | 93 | } 94 | } 95 | 96 | fn main() { 97 | console_log::init_with_level(Level::Trace).expect("Failed to initialise Log!"); 98 | yew::Renderer::::new().render(); 99 | } 100 | 101 | #[cfg(test)] 102 | mod tests { 103 | use super::*; 104 | use gloo::timers::future::sleep; 105 | use gloo::utils::document; 106 | use std::time::Duration; 107 | use wasm_bindgen::JsCast; 108 | use wasm_bindgen_test::*; 109 | use web_sys::{HtmlElement, HtmlMetaElement}; 110 | 111 | wasm_bindgen_test_configure!(run_in_browser); 112 | 113 | async fn click_by_id>(id: S) { 114 | sleep(Duration::ZERO).await; 115 | 116 | document() 117 | .query_selector(&format!("#{}", id.as_ref())) 118 | .unwrap() 119 | .unwrap() 120 | .unchecked_into::() 121 | .click(); 122 | } 123 | 124 | #[wasm_bindgen_test] 125 | async fn test_title() { 126 | yew::Renderer::::with_root(document().query_selector("#output").unwrap().unwrap()) 127 | .render(); 128 | 129 | sleep(Duration::ZERO).await; 130 | 131 | assert_eq!(document().title(), "Home Page - Example"); 132 | 133 | sleep(Duration::ZERO).await; 134 | 135 | let description = document() 136 | .query_selector("meta[name='description']") 137 | .unwrap() 138 | .unwrap() 139 | .unchecked_into::() 140 | .content(); 141 | assert_eq!("home page", description); 142 | 143 | click_by_id("go-to-a").await; 144 | 145 | sleep(Duration::ZERO).await; 146 | 147 | assert_eq!(document().title(), "Page A - Example"); 148 | 149 | sleep(Duration::ZERO).await; 150 | 151 | let description = document() 152 | .query_selector("meta[name='description']") 153 | .unwrap() 154 | .unwrap() 155 | .unchecked_into::() 156 | .content(); 157 | assert_eq!("page A", description); 158 | 159 | click_by_id("go-to-b").await; 160 | 161 | sleep(Duration::ZERO).await; 162 | 163 | assert_eq!(document().title(), "Example"); 164 | 165 | sleep(Duration::ZERO).await; 166 | 167 | let description = document() 168 | .query_selector("meta[name='description']") 169 | .unwrap() 170 | .unwrap() 171 | .unchecked_into::() 172 | .content(); 173 | assert_eq!("default page", description); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /examples/notion/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "notion" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bounce = { path = "../../crates/bounce" } 11 | yew = { version = "0.21", features = ["csr"] } 12 | stylist = { version = "0.13", features = ["yew"] } 13 | log = "0.4.19" 14 | console_log = { version = "1.0.0", features = ["color"] } 15 | wasm-bindgen = "0.2.87" 16 | 17 | [dev-dependencies] 18 | wasm-bindgen-test = "0.3.37" 19 | gloo = { version = "0.10.0", features = ["futures"] } 20 | web-sys = "0.3.64" 21 | -------------------------------------------------------------------------------- /examples/notion/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Notion Example 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/partial-render/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "partial-render" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bounce = { path = "../../crates/bounce" } 11 | yew = { version = "0.21", features = ["csr"] } 12 | stylist = { version = "0.13", features = ["yew"] } 13 | log = "0.4.19" 14 | console_log = { version = "1.0.0", features = ["color"] } 15 | wasm-bindgen = "0.2.87" 16 | 17 | [dev-dependencies] 18 | wasm-bindgen-test = "0.3.37" 19 | gloo = { version = "0.10.0", features = ["futures"] } 20 | web-sys = "0.3.64" 21 | -------------------------------------------------------------------------------- /examples/partial-render/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Partial Render Example 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/persist/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "persist" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bounce = { path = "../../crates/bounce" } 11 | yew = { version = "0.21", features = ["csr"] } 12 | log = "0.4.19" 13 | console_log = { version = "1.0.0", features = ["color"] } 14 | wasm-bindgen = "0.2.87" 15 | gloo = { version = "0.10.0", features = ["futures"] } 16 | 17 | [dependencies.web-sys] 18 | version = "0.3.64" 19 | features = [ 20 | "HtmlInputElement", 21 | ] 22 | 23 | [dev-dependencies] 24 | wasm-bindgen-test = "0.3.37" 25 | -------------------------------------------------------------------------------- /examples/persist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Persist Example 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/persist/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::rc::Rc; 3 | 4 | use bounce::*; 5 | use gloo::storage::{LocalStorage, Storage}; 6 | use log::Level; 7 | use web_sys::HtmlInputElement; 8 | use yew::prelude::*; 9 | use yew::InputEvent; 10 | 11 | #[derive(PartialEq, Atom, Eq)] 12 | #[bounce(observed)] 13 | struct Username { 14 | inner: String, 15 | } 16 | 17 | impl From for Username { 18 | fn from(s: String) -> Self { 19 | Self { inner: s } 20 | } 21 | } 22 | 23 | impl Default for Username { 24 | fn default() -> Self { 25 | Self { 26 | inner: LocalStorage::get("username").unwrap_or_else(|_| "Jane Doe".into()), 27 | } 28 | } 29 | } 30 | 31 | impl fmt::Display for Username { 32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | write!(f, "{}", self.inner) 34 | } 35 | } 36 | 37 | impl Observed for Username { 38 | fn changed(self: Rc) { 39 | LocalStorage::set("username", &self.inner).expect("failed to set username."); 40 | } 41 | } 42 | 43 | #[function_component(Reader)] 44 | fn reader() -> Html { 45 | let username = use_atom_value::(); 46 | 47 | html! {
{"Hello, "}{username}
} 48 | } 49 | 50 | #[function_component(Setter)] 51 | fn setter() -> Html { 52 | let username = use_atom::(); 53 | 54 | let on_text_input = { 55 | let username = username.clone(); 56 | 57 | Callback::from(move |e: InputEvent| { 58 | let input: HtmlInputElement = e.target_unchecked_into(); 59 | 60 | username.set(input.value().into()); 61 | }) 62 | }; 63 | 64 | html! { 65 |
66 | 67 |
68 | } 69 | } 70 | 71 | #[function_component(App)] 72 | fn app() -> Html { 73 | html! { 74 | 75 | 76 | 77 |
{"Type a username, and it will be saved in the local storage."}
78 |
79 | } 80 | } 81 | 82 | fn main() { 83 | console_log::init_with_level(Level::Trace).expect("Failed to initialise Log!"); 84 | yew::Renderer::::new().render(); 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use super::*; 90 | use gloo::timers::future::sleep; 91 | use gloo::utils::document; 92 | use std::time::Duration; 93 | use wasm_bindgen::JsCast; 94 | use wasm_bindgen_test::*; 95 | use web_sys::Event; 96 | use web_sys::EventTarget; 97 | use web_sys::HtmlInputElement; 98 | 99 | wasm_bindgen_test_configure!(run_in_browser); 100 | 101 | async fn get_text_content_by_id>(id: S) -> String { 102 | sleep(Duration::ZERO).await; 103 | 104 | document() 105 | .query_selector(&format!("#{}", id.as_ref())) 106 | .unwrap() 107 | .unwrap() 108 | .text_content() 109 | .unwrap() 110 | } 111 | 112 | #[wasm_bindgen_test] 113 | async fn test_persist() { 114 | let handle = 115 | yew::Renderer::::with_root(document().query_selector("#output").unwrap().unwrap()) 116 | .render(); 117 | 118 | assert_eq!(get_text_content_by_id("reader").await, "Hello, Jane Doe"); 119 | 120 | document() 121 | .query_selector("#input") 122 | .unwrap() 123 | .unwrap() 124 | .unchecked_into::() 125 | .set_value("John Smith"); 126 | 127 | document() 128 | .query_selector("#input") 129 | .unwrap() 130 | .unwrap() 131 | .unchecked_into::() 132 | .dispatch_event(&Event::new("input").unwrap()) 133 | .unwrap(); 134 | 135 | assert_eq!(get_text_content_by_id("reader").await, "Hello, John Smith"); 136 | 137 | handle.destroy(); 138 | 139 | // make sure that app has been destroyed. 140 | assert_eq!(get_text_content_by_id("output").await, ""); 141 | 142 | yew::Renderer::::with_root(document().query_selector("#output").unwrap().unwrap()) 143 | .render(); 144 | 145 | assert_eq!(get_text_content_by_id("reader").await, "Hello, John Smith"); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /examples/queries-mutations/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "queries-mutations" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bounce = { path = "../../crates/bounce", features = ["query"] } 11 | yew = { version = "0.21", features = ["csr"] } 12 | log = "0.4.19" 13 | console_log = { version = "1.0.0", features = ["color"] } 14 | reqwest = { version = "0.11.18", features = ["json"] } 15 | serde = { version = "1.0.164", features = ["derive"] } 16 | uuid = { version = "1.4.0", features = ["serde"] } 17 | async-trait = "0.1.68" 18 | wasm-bindgen = "0.2.87" 19 | 20 | [dependencies.web-sys] 21 | version = "0.3.64" 22 | features = [ 23 | "HtmlInputElement", 24 | ] 25 | 26 | [dev-dependencies] 27 | wasm-bindgen-test = "0.3.37" 28 | gloo = { version = "0.10.0", features = ["futures"] } 29 | -------------------------------------------------------------------------------- /examples/queries-mutations/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bounce Query Example 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/queries-ssr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "queries-ssr" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [[bin]] 10 | name = "queries-ssr-client" 11 | required-features = ["csr"] 12 | 13 | [[bin]] 14 | name = "queries-ssr-server" 15 | required-features = ["ssr"] 16 | 17 | [dependencies] 18 | bounce = { path = "../../crates/bounce", features = ["query"] } 19 | yew = { version = "0.21" } 20 | log = "0.4.19" 21 | console_log = { version = "1.0.0", features = ["color"] } 22 | reqwest = { version = "0.11.18", features = ["json"] } 23 | serde = { version = "1.0.164", features = ["derive"] } 24 | uuid = { version = "1.4.0", features = ["serde"] } 25 | async-trait = "0.1.68" 26 | wasm-bindgen = "0.2.87" 27 | thiserror = "1.0.40" 28 | 29 | [dependencies.web-sys] 30 | version = "0.3.64" 31 | features = [ 32 | "HtmlInputElement", 33 | ] 34 | 35 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 36 | tokio = { version = "1.29.1", features = ["full"] } 37 | env_logger = "0.10" 38 | clap = { version = "4.3.10", features = ["derive"] } 39 | warp = "0.3" 40 | futures = "0.3" 41 | html_parser = "0.7.0" 42 | 43 | [dev-dependencies] 44 | wasm-bindgen-test = "0.3.37" 45 | gloo = { version = "0.10.0", features = ["futures"] } 46 | 47 | [features] 48 | csr = ["yew/csr", "yew/hydration"] 49 | ssr = ["yew/ssr", "bounce/ssr"] 50 | -------------------------------------------------------------------------------- /examples/queries-ssr/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Bounce Query SSR Example 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/queries-ssr/src/bin/queries-ssr-client.rs: -------------------------------------------------------------------------------- 1 | use log::Level; 2 | use queries_ssr::App; 3 | 4 | fn main() { 5 | console_log::init_with_level(Level::Trace).expect("Failed to initialise Log!"); 6 | yew::Renderer::::new().hydrate(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/queries-ssr/src/bin/queries-ssr-server.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | mod target_non_wasm32 { 3 | use std::fmt::Write; 4 | use std::path::PathBuf; 5 | 6 | use clap::Parser; 7 | use queries_ssr::App; 8 | use warp::Filter; 9 | 10 | /// A basic example 11 | #[derive(Parser, Debug)] 12 | struct Opt { 13 | /// the "dist" created by trunk directory to be served for hydration. 14 | #[clap(short, long)] 15 | dir: PathBuf, 16 | } 17 | 18 | async fn render(script_content: String) -> String { 19 | let body_s = yew::ServerRenderer::::new().render().await; 20 | 21 | let mut s = String::new(); 22 | 23 | let _ = writeln!(s, ""); 24 | let _ = writeln!(s, ""); 25 | let _ = writeln!(s, ""); 26 | 27 | let _ = writeln!(s, r#""#); 28 | let _ = writeln!(s, ""); 29 | 30 | let _ = writeln!(s, ""); 31 | s.push_str(&body_s); 32 | 33 | let _ = writeln!(s, ""); 34 | let _ = writeln!(s, ""); 35 | 36 | s 37 | } 38 | 39 | fn extract_script_content(index_html: &str) -> String { 40 | fn extract(nodes: I) -> Option 41 | where 42 | I: IntoIterator, 43 | { 44 | fn extract_text(nodes: I) -> Option 45 | where 46 | I: IntoIterator, 47 | { 48 | for node in nodes { 49 | match node { 50 | html_parser::Node::Comment(_) => {} 51 | html_parser::Node::Element(_) => {} 52 | html_parser::Node::Text(t) => { 53 | return Some(t); 54 | } 55 | } 56 | } 57 | 58 | None 59 | } 60 | 61 | let nodes = nodes.into_iter(); 62 | for node in nodes { 63 | match node { 64 | html_parser::Node::Comment(_) => {} 65 | html_parser::Node::Element(e) => { 66 | if e.name.to_lowercase().as_str() == "script" { 67 | return extract_text(e.children); 68 | } 69 | 70 | if let Some(m) = extract(e.children) { 71 | return Some(m); 72 | } 73 | } 74 | html_parser::Node::Text(_) => {} 75 | } 76 | } 77 | 78 | None 79 | } 80 | 81 | let dom = html_parser::Dom::parse(index_html).expect("failed to parse"); 82 | 83 | extract(dom.children).expect("failed to find script tag") 84 | } 85 | 86 | pub async fn main() { 87 | env_logger::init(); 88 | 89 | let opts = Opt::parse(); 90 | 91 | let index_html_s = tokio::fs::read_to_string(opts.dir.join("index.html")) 92 | .await 93 | .expect("failed to read index.html"); 94 | 95 | let script_content = extract_script_content(&index_html_s); 96 | 97 | let render_f = warp::get().then(move || { 98 | let script_content = script_content.clone(); 99 | 100 | async move { warp::reply::html(render(script_content).await) } 101 | }); 102 | 103 | let routes = warp::path::end() 104 | .and(render_f.clone()) 105 | .or(warp::path("index.html").and(render_f.clone())) 106 | .or(warp::fs::dir(opts.dir.clone())); 107 | 108 | println!("You can view the website at: http://localhost:8080/"); 109 | warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; 110 | } 111 | } 112 | 113 | #[cfg(not(target_arch = "wasm32"))] 114 | #[tokio::main] 115 | async fn main() { 116 | target_non_wasm32::main().await; 117 | } 118 | 119 | #[cfg(target_arch = "wasm32")] 120 | fn main() {} 121 | -------------------------------------------------------------------------------- /examples/queries-ssr/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use async_trait::async_trait; 4 | use bounce::prelude::*; 5 | use bounce::query::{use_prepared_query, Query, QueryResult}; 6 | use bounce::BounceRoot; 7 | use serde::{Deserialize, Serialize}; 8 | use thiserror::Error; 9 | use uuid::Uuid; 10 | use yew::platform::spawn_local; 11 | use yew::prelude::*; 12 | 13 | #[derive(PartialEq, Debug, Serialize, Deserialize, Eq, Clone)] 14 | struct UuidQuery { 15 | uuid: Uuid, 16 | } 17 | 18 | // To be replaced with `!` once it is stable. 19 | #[derive(Error, Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] 20 | #[error("this will never happen.")] 21 | struct Never {} 22 | 23 | #[async_trait(?Send)] 24 | impl Query for UuidQuery { 25 | type Input = (); 26 | type Error = Never; 27 | 28 | async fn query(_states: &BounceStates, _input: Rc<()>) -> QueryResult { 29 | // errors should be handled properly in actual application. 30 | let resp = reqwest::get("https://httpbin.org/uuid").await.unwrap(); 31 | let uuid_resp = resp.json::().await.unwrap(); 32 | 33 | Ok(uuid_resp.into()) 34 | } 35 | } 36 | 37 | #[derive(Debug, Properties, PartialEq, Eq)] 38 | struct ContentProps { 39 | ord: usize, 40 | } 41 | 42 | #[function_component(SuspendContent)] 43 | fn suspend_content(props: &ContentProps) -> HtmlResult { 44 | let uuid_state = use_prepared_query::(().into())?; 45 | 46 | let text = match uuid_state.as_deref() { 47 | Ok(m) => format!("Random UUID: {}", m.uuid), 48 | Err(_) => unreachable!(), 49 | }; 50 | 51 | Ok(html! { 52 | <> 53 |
{text}
54 | 55 | }) 56 | } 57 | 58 | #[function_component(Refresher)] 59 | fn refresher() -> HtmlResult { 60 | let uuid_state = use_prepared_query::(().into())?; 61 | 62 | let on_fetch_clicked = Callback::from(move |_| { 63 | let uuid_state = uuid_state.clone(); 64 | spawn_local(async move { 65 | let _ignore = uuid_state.refresh().await; 66 | }); 67 | }); 68 | 69 | Ok(html! { 70 | <> 71 | 72 | 73 | }) 74 | } 75 | 76 | #[function_component(App)] 77 | pub fn app() -> Html { 78 | let fallback = html! { 79 | <> 80 |
{"Loading UUID, Please wait..."}
81 | 82 | }; 83 | 84 | let fallback_refresh = html! { 85 | <> 86 | 87 | 88 | }; 89 | 90 | html! { 91 | 92 |

{"Query"}

93 |
{"This UUID is fetched at the server-side. If you click fetch, it will be refreshed on the client side."}
94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 | } 102 | } 103 | -------------------------------------------------------------------------------- /examples/random-uuid/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "random-uuid" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bounce = { path = "../../crates/bounce" } 11 | yew = { version = "0.21", features = ["csr"] } 12 | log = "0.4.19" 13 | console_log = { version = "1.0.0", features = ["color"] } 14 | reqwest = { version = "0.11.18", features = ["json"] } 15 | serde = { version = "1.0.164", features = ["derive"] } 16 | wasm-bindgen = "0.2.87" 17 | 18 | [dependencies.web-sys] 19 | version = "0.3.64" 20 | features = [ 21 | "HtmlInputElement", 22 | ] 23 | 24 | [dev-dependencies] 25 | wasm-bindgen-test = "0.3.37" 26 | gloo = { version = "0.10.0", features = ["futures"] } 27 | -------------------------------------------------------------------------------- /examples/random-uuid/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Random UUID Example 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/random-uuid/src/main.rs: -------------------------------------------------------------------------------- 1 | //! This is a demo of the low-level future notion API. 2 | //! 3 | //! If you are interacting with APIs, it is recommended to use the Query API which is implemented 4 | //! based on the future notion API with automatic caching and request deduplication. 5 | 6 | use std::rc::Rc; 7 | 8 | use bounce::*; 9 | use log::Level; 10 | use serde::{Deserialize, Serialize}; 11 | use yew::prelude::*; 12 | 13 | #[derive(Serialize, Deserialize)] 14 | struct UuidResponse { 15 | uuid: String, 16 | } 17 | 18 | #[future_notion(FetchUuid)] 19 | async fn fetch_uuid(_input: &()) -> String { 20 | // errors should be handled properly in actual application. 21 | let resp = reqwest::get("https://httpbin.org/uuid").await.unwrap(); 22 | let uuid_resp = resp.json::().await.unwrap(); 23 | 24 | uuid_resp.uuid 25 | } 26 | 27 | #[derive(PartialEq, Atom, Eq)] 28 | #[bounce(with_notion(Deferred))] 29 | enum UuidState { 30 | NotStarted, 31 | Pending, 32 | Complete(String), 33 | } 34 | 35 | impl Default for UuidState { 36 | fn default() -> UuidState { 37 | Self::NotStarted 38 | } 39 | } 40 | 41 | impl WithNotion> for UuidState { 42 | fn apply(self: Rc, notion: Rc>) -> Rc { 43 | match notion.output() { 44 | Some(m) => Self::Complete(m.to_string()).into(), 45 | None => Self::Pending.into(), 46 | } 47 | } 48 | } 49 | 50 | #[function_component(Reader)] 51 | fn reader() -> Html { 52 | let uuid_state = use_atom_value::(); 53 | 54 | let text = match *uuid_state { 55 | UuidState::NotStarted => { 56 | "Please click on Fetch to fetch a random UUID from remote.".to_string() 57 | } 58 | UuidState::Pending => "Loading UUID, Please wait...".to_string(), 59 | UuidState::Complete(ref m) => format!("Random UUID: {m}"), 60 | }; 61 | 62 | html! {
{text}
} 63 | } 64 | 65 | #[function_component(Loader)] 66 | fn loader() -> Html { 67 | let uuid_state = use_atom::(); 68 | let run_fetch_uuid = use_future_notion_runner::(); 69 | 70 | let on_fetch_clicked = Callback::from(move |_| run_fetch_uuid(())); 71 | 72 | let disabled = *uuid_state == UuidState::Pending; 73 | 74 | html! { } 75 | } 76 | 77 | #[function_component(App)] 78 | fn app() -> Html { 79 | html! { 80 | 81 | 82 | 83 | 84 | } 85 | } 86 | 87 | fn main() { 88 | console_log::init_with_level(Level::Trace).expect("Failed to initialise Log!"); 89 | yew::Renderer::::new().render(); 90 | } 91 | -------------------------------------------------------------------------------- /examples/simple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bounce = { path = "../../crates/bounce" } 11 | yew = { version = "0.21", features = ["csr"] } 12 | log = "0.4.19" 13 | console_log = { version = "1.0.0", features = ["color"] } 14 | wasm-bindgen = "0.2.87" 15 | 16 | [dependencies.web-sys] 17 | version = "0.3.64" 18 | features = [ 19 | "HtmlInputElement", 20 | ] 21 | 22 | [dev-dependencies] 23 | wasm-bindgen-test = "0.3.37" 24 | gloo = { version = "0.10.0", features = ["futures"] } 25 | -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Example 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/simple/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use bounce::*; 4 | use log::Level; 5 | use web_sys::HtmlInputElement; 6 | use yew::prelude::*; 7 | use yew::InputEvent; 8 | 9 | #[derive(PartialEq, Atom, Eq)] 10 | struct Username { 11 | inner: String, 12 | } 13 | 14 | impl From for Username { 15 | fn from(s: String) -> Self { 16 | Self { inner: s } 17 | } 18 | } 19 | 20 | impl Default for Username { 21 | fn default() -> Self { 22 | Self { 23 | inner: "Jane Doe".into(), 24 | } 25 | } 26 | } 27 | 28 | impl fmt::Display for Username { 29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 | write!(f, "{}", self.inner) 31 | } 32 | } 33 | 34 | #[function_component(Reader)] 35 | fn reader() -> Html { 36 | let username = use_atom_value::(); 37 | 38 | html! {
{"Hello, "}{username}
} 39 | } 40 | 41 | #[function_component(Setter)] 42 | fn setter() -> Html { 43 | let username = use_atom::(); 44 | 45 | let on_text_input = { 46 | let username = username.clone(); 47 | 48 | Callback::from(move |e: InputEvent| { 49 | let input: HtmlInputElement = e.target_unchecked_into(); 50 | 51 | username.set(input.value().into()); 52 | }) 53 | }; 54 | 55 | html! { 56 |
57 | 58 |
59 | } 60 | } 61 | 62 | #[function_component(Resetter)] 63 | fn resetter() -> Html { 64 | let set_username = use_atom_setter::(); 65 | 66 | let on_reset_clicked = Callback::from(move |_| set_username(Username::default())); 67 | 68 | html! { } 69 | } 70 | 71 | #[function_component(App)] 72 | fn app() -> Html { 73 | html! { 74 | 75 | 76 | 77 | 78 | 79 | } 80 | } 81 | 82 | fn main() { 83 | console_log::init_with_level(Level::Trace).expect("Failed to initialise Log!"); 84 | yew::Renderer::::new().render(); 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use super::*; 90 | use gloo::timers::future::sleep; 91 | use gloo::utils::document; 92 | use std::time::Duration; 93 | use wasm_bindgen::JsCast; 94 | use wasm_bindgen_test::*; 95 | use web_sys::Event; 96 | use web_sys::EventTarget; 97 | use web_sys::HtmlElement; 98 | use web_sys::HtmlInputElement; 99 | 100 | wasm_bindgen_test_configure!(run_in_browser); 101 | 102 | async fn get_text_content_by_id>(id: S) -> String { 103 | sleep(Duration::ZERO).await; 104 | 105 | document() 106 | .query_selector(&format!("#{}", id.as_ref())) 107 | .unwrap() 108 | .unwrap() 109 | .text_content() 110 | .unwrap() 111 | } 112 | 113 | async fn click_by_id>(id: S) { 114 | sleep(Duration::ZERO).await; 115 | 116 | document() 117 | .query_selector(&format!("#{}", id.as_ref())) 118 | .unwrap() 119 | .unwrap() 120 | .unchecked_into::() 121 | .click(); 122 | } 123 | 124 | #[wasm_bindgen_test] 125 | async fn test_simple() { 126 | yew::Renderer::::with_root(document().query_selector("#output").unwrap().unwrap()) 127 | .render(); 128 | 129 | assert_eq!(get_text_content_by_id("reader").await, "Hello, Jane Doe"); 130 | 131 | document() 132 | .query_selector("#input") 133 | .unwrap() 134 | .unwrap() 135 | .unchecked_into::() 136 | .set_value("John Smith"); 137 | 138 | document() 139 | .query_selector("#input") 140 | .unwrap() 141 | .unwrap() 142 | .unchecked_into::() 143 | .dispatch_event(&Event::new("input").unwrap()) 144 | .unwrap(); 145 | 146 | assert_eq!(get_text_content_by_id("reader").await, "Hello, John Smith"); 147 | 148 | click_by_id("btn-reset").await; 149 | 150 | assert_eq!(get_text_content_by_id("reader").await, "Hello, Jane Doe"); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /examples/title/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "title" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bounce = { path = "../../crates/bounce" } 11 | yew = { version = "0.21", features = ["csr"] } 12 | log = "0.4.19" 13 | console_log = { version = "1.0.0", features = ["color"] } 14 | wasm-bindgen = "0.2.87" 15 | yew-router = "0.18" 16 | gloo = { version = "0.10.0", features = ["futures"] } 17 | web-sys= "0.3.64" 18 | 19 | [dev-dependencies] 20 | wasm-bindgen-test = "0.3.37" 21 | -------------------------------------------------------------------------------- /examples/title/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title Example 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/title/src/main.rs: -------------------------------------------------------------------------------- 1 | //! An example to demonstrate the artifact API. 2 | //! 3 | //! In your application, if you want to change title based on the content rendered, 4 | //! you may want to use the helmet API instead. It's a high-level API that's built with the 5 | //! Artifact API. 6 | 7 | use std::rc::Rc; 8 | 9 | use bounce::prelude::*; 10 | use bounce::BounceRoot; 11 | use gloo::utils::document; 12 | use log::Level; 13 | use yew::prelude::*; 14 | use yew_router::prelude::*; 15 | 16 | #[derive(PartialEq, Eq)] 17 | pub struct Title { 18 | value: String, 19 | } 20 | 21 | /// A component to apply the title when it changes. 22 | #[function_component(TitleApplier)] 23 | pub fn title_applier() -> Html { 24 | let titles = use_artifacts::(); 25 | 26 | let title = titles 27 | .last() 28 | .map(|m| m.value.to_owned()) 29 | .unwrap_or_else(|| "unknown title".into()); 30 | 31 | use_effect_with(title, |m| { 32 | document().set_title(m); 33 | 34 | || {} 35 | }); 36 | 37 | Html::default() 38 | } 39 | 40 | #[derive(Routable, PartialEq, Clone, Eq)] 41 | pub enum Route { 42 | #[at("/a")] 43 | A, 44 | #[at("/b")] 45 | B, 46 | #[at("/")] 47 | Home, 48 | } 49 | 50 | #[function_component(A)] 51 | fn a() -> Html { 52 | html! { 53 | <> 54 | <Artifact<Title> value={Rc::new(Title { value: "Page A - Example".into() })} /> 55 | <div>{"This is page A."}</div> 56 | </> 57 | } 58 | } 59 | 60 | #[function_component(B)] 61 | fn b() -> Html { 62 | html! { 63 | <div>{"This is page B. This page does not have a specific title, so default title will be used instead."}</div> 64 | } 65 | } 66 | 67 | #[function_component(Home)] 68 | fn home() -> Html { 69 | html! { 70 | <> 71 | <Artifact<Title> value={Rc::new(Title { value: "Home Page - Example".into() })} /> 72 | <div>{"This is home page."}</div> 73 | </> 74 | } 75 | } 76 | 77 | #[function_component(Links)] 78 | fn links() -> Html { 79 | html! { 80 | <> 81 | <div><Link<Route> to={Route::A}><span id="go-to-a">{"Go to A"}</span></Link<Route>></div> 82 | <div><Link<Route> to={Route::B}><span id="go-to-b">{"Go to B"}</span></Link<Route>></div> 83 | <div><Link<Route> to={Route::Home}><span id="go-to-home">{"Go to Home"}</span></Link<Route>></div> 84 | </> 85 | } 86 | } 87 | 88 | fn render_fn(route: Route) -> Html { 89 | match route { 90 | Route::A => html! {<A />}, 91 | Route::B => html! {<B />}, 92 | Route::Home => html! {<Home />}, 93 | } 94 | } 95 | 96 | #[function_component(App)] 97 | fn app() -> Html { 98 | html! { 99 | <BounceRoot> 100 | <TitleApplier /> 101 | <Artifact<Title> value={Rc::new(Title { value: "Example".into() })} /> 102 | <BrowserRouter> 103 | <Switch<Route> render={render_fn} /> 104 | <Links /> 105 | </BrowserRouter> 106 | </BounceRoot> 107 | } 108 | } 109 | 110 | fn main() { 111 | console_log::init_with_level(Level::Trace).expect("Failed to initialise Log!"); 112 | yew::Renderer::<App>::default().render(); 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use super::*; 118 | use gloo::timers::future::sleep; 119 | use gloo::utils::document; 120 | use std::time::Duration; 121 | use wasm_bindgen::JsCast; 122 | use wasm_bindgen_test::*; 123 | use web_sys::HtmlElement; 124 | 125 | wasm_bindgen_test_configure!(run_in_browser); 126 | 127 | async fn click_by_id<S: AsRef<str>>(id: S) { 128 | sleep(Duration::ZERO).await; 129 | 130 | document() 131 | .query_selector(&format!("#{}", id.as_ref())) 132 | .unwrap() 133 | .unwrap() 134 | .unchecked_into::<HtmlElement>() 135 | .click(); 136 | } 137 | 138 | #[wasm_bindgen_test] 139 | async fn test_title() { 140 | yew::Renderer::<App>::with_root(document().query_selector("#output").unwrap().unwrap()) 141 | .render(); 142 | 143 | sleep(Duration::ZERO).await; 144 | 145 | assert_eq!(document().title(), "Home Page - Example"); 146 | 147 | click_by_id("go-to-a").await; 148 | 149 | sleep(Duration::ZERO).await; 150 | 151 | assert_eq!(document().title(), "Page A - Example"); 152 | 153 | click_by_id("go-to-b").await; 154 | 155 | sleep(Duration::ZERO).await; 156 | 157 | assert_eq!(document().title(), "Example"); 158 | } 159 | } 160 | --------------------------------------------------------------------------------