mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
10 Commits
async-docs
...
562
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bb3169a69 | ||
|
|
a985ae5660 | ||
|
|
efe29fd057 | ||
|
|
05b5b25c0e | ||
|
|
46f6deddbf | ||
|
|
e9c4b490e5 | ||
|
|
f7d0eea04d | ||
|
|
bf246a62e7 | ||
|
|
b8acfd758d | ||
|
|
dc8fd37461 |
@@ -11,6 +11,7 @@ members = [
|
||||
# integrations
|
||||
"integrations/actix",
|
||||
"integrations/axum",
|
||||
"integrations/viz",
|
||||
"integrations/utils",
|
||||
|
||||
# libraries
|
||||
|
||||
@@ -40,6 +40,7 @@ dependencies = [
|
||||
{ name = "check", path = "examples/tailwind" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_axum" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_viz" },
|
||||
{ name = "check", path = "examples/todomvc" },
|
||||
]
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
- [Passing Children to Components](./view/09_component_children.md)
|
||||
- [Interlude: Reactivity and Functions](./interlude_functions.md)
|
||||
- [Testing](./testing.md)
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Async]()
|
||||
- [Resource]()
|
||||
- [Suspense]()
|
||||
- [Async](./async/README.md)
|
||||
- [Loading Data with Resources](./async/10_resources.md)
|
||||
- [Suspense](./async/11_suspense.md)
|
||||
- [Transition]()
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [State Management]()
|
||||
- [Interlude: Advanced Reactivity]()
|
||||
- [Router]()
|
||||
|
||||
53
docs/book/src/async/10_resources.md
Normal file
53
docs/book/src/async/10_resources.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Loading Data with Resources
|
||||
|
||||
A [Resource](https://docs.rs/leptos/latest/leptos/struct.Resource.html) is a reactive data structure that reflects the current state of an asynchronous task, allowing you to integrate asynchronous `Future`s into the synchronous reactive system. Rather than waiting for its data to load with `.await`, you transform the `Future` into a signal that returns `Some(T)` if it has resolved, and `None` if it’s still pending.
|
||||
|
||||
You do this by using the [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) function. This takes two arguments (other than the ubiquitous `cx`):
|
||||
|
||||
1. a source signal, which will generate a new `Future` whenever it changes
|
||||
2. a fetcher function, which takes the data from that signal and returns a `Future`
|
||||
|
||||
Here’s an example
|
||||
|
||||
```rust
|
||||
// our source signal: some synchronous, local state
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
// our resource
|
||||
let async_data = create_resource(cx,
|
||||
count,
|
||||
// every time `count` changes, this will run
|
||||
|value| async move {
|
||||
log!("loading data from API");
|
||||
load_data(value).await
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
To create a resource that simply runs once, you can pass a non-reactive, empty source signal:
|
||||
|
||||
```rust
|
||||
let once = create_resource(cx, || (), |_| async move { load_data().await });
|
||||
```
|
||||
|
||||
To access the value you can use `.read(cx)` or `.with(cx, |data| /* */)`. These work just like `.get()` and `.with()` on a signal—`read` clones the value and returns it, `with` applies a closure to it—but with two differences
|
||||
|
||||
1. For any `Resource<_, T>`, they always return `Option<T>`, not `T`: because it’s always possible that your resource is still loading.
|
||||
2. They take a `Scope` argument. You’ll see why in the next chapter, on `<Suspense/>`.
|
||||
|
||||
So, you can show the current state of a resource in your view:
|
||||
|
||||
```rust
|
||||
let once = create_resource(cx, || (), |_| async move { load_data().await });
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
Resources also provide a `refetch()` method that allow you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
72
docs/book/src/async/11_suspense.md
Normal file
72
docs/book/src/async/11_suspense.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# `<Suspense/>`
|
||||
|
||||
In the previous chapter, we showed how you can create a simple loading screen to show some fallback while a resource is loading.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
But what if we have two resources, and want to wait for both of them?
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count2, set_count2) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match (a.read(cx), b.read(cx)) {
|
||||
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
(Some(a), Some(b)) => view! { cx,
|
||||
<ShowA a/>
|
||||
<ShowA b/>
|
||||
}.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
That’s not _so_ bad, but it’s kind of annoying. What if we could invert the flow of control?
|
||||
|
||||
The [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) component lets us do exactly that. You give it a `fallback` prop and children, one or more of which usually involves reading from a resource. Reading from a resource “under” a `<Suspense/>` (i.e., in one of its children) registers that resource with the `<Suspense/>`. If it’s still waiting for resources to load, it shows the `fallback`. When they’ve all loaded, it shows the children.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count2, set_count2) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
<Suspense
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
>
|
||||
<h2>"My Data"</h2>
|
||||
<h3>"A"</h3>
|
||||
{move || {
|
||||
a.read(cx)
|
||||
.map(|a| view! { cx, <ShowA a/> })
|
||||
}}
|
||||
<h3>"B"</h3>
|
||||
{move || {
|
||||
b.read(cx)
|
||||
.map(|b| view! { cx, <ShowB b/> })
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
```
|
||||
|
||||
Every time one of the resources is reloading, the `"Loading..."` fallback will show again.
|
||||
|
||||
This inversion of the flow of control makes it easier to add or remove individual resources, as you don’t need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which we’ll talk about during a later chapter.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
9
docs/book/src/async/12_transition.md
Normal file
9
docs/book/src/async/12_transition.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# `<Transition/>`
|
||||
|
||||
You’ll notice in the `<Suspense/>` example that if you keep reloading the data, it keeps flickering back to `"Loading..."`. Sometimes this is fine. For other times, there’s [`<Transition/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html).
|
||||
|
||||
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.
|
||||
|
||||
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data laods. This can be a much better user experience than constantly falling back to a loading message.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
9
docs/book/src/async/README.md
Normal file
9
docs/book/src/async/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Working with `async`
|
||||
|
||||
So far we’ve only been working with synchronous users interfaces: You provide some input,
|
||||
the app immediately process it and updates the interface. This is great, but is a tiny
|
||||
subset of what web applications do. In particular, most web apps have to deal with some kind
|
||||
of asynchronous data loading, usually loading something from an API.
|
||||
|
||||
Asynchronous data is notoriously hard to integrate with the synchronous parts of your code.
|
||||
In this chapter, we’ll see how Leptos helps smooth out that process for you.
|
||||
@@ -122,3 +122,5 @@ view! { cx,
|
||||
</WrappedChildren>
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px"></iframe>
|
||||
|
||||
99
examples/todo_app_sqlite_viz/Cargo.toml
Normal file
99
examples/todo_app_sqlite_viz/Cargo.toml
Normal file
@@ -0,0 +1,99 @@
|
||||
[package]
|
||||
name = "todo_app_sqlite_viz"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_viz = { path = "../../integrations/viz", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_reactive = { path = "../../leptos_reactive", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
viz = { version = "0.4.8", features = ["serve"], optional = true }
|
||||
tokio = { version = "1.25.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:viz",
|
||||
"dep:tokio",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_viz"
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["viz", "tokio", "sqlx", "leptos_viz"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "todo_app_sqlite_viz"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
21
examples/todo_app_sqlite_viz/LICENSE
Normal file
21
examples/todo_app_sqlite_viz/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Greg Johnston
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
9
examples/todo_app_sqlite_viz/Makefile.toml
Normal file
9
examples/todo_app_sqlite_viz/Makefile.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
42
examples/todo_app_sqlite_viz/README.md
Normal file
42
examples/todo_app_sqlite_viz/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Leptos Todo App Sqlite with Viz
|
||||
|
||||
This example creates a basic todo app with a Viz backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
||||
|
||||
## Client Side Rendering
|
||||
This example cannot be built as a trunk standalone CSR-only app. Only the server may directly connect to the database.
|
||||
|
||||
## Server Side Rendering with cargo-leptos
|
||||
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||
|
||||
1. Install cargo-leptos
|
||||
```bash
|
||||
cargo install --locked cargo-leptos
|
||||
```
|
||||
2. Build the site in watch mode, recompiling on file changes
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
```
|
||||
|
||||
## Server Side Rendering without cargo-leptos
|
||||
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
|
||||
|
||||
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
|
||||
1. Install wasm-pack
|
||||
```bash
|
||||
cargo install wasm-pack
|
||||
```
|
||||
2. Build the Webassembly used to hydrate the HTML from the server
|
||||
```bash
|
||||
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||
```
|
||||
3. Run the server to serve the Webassembly, JS, and HTML
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
BIN
examples/todo_app_sqlite_viz/Todos.db
Normal file
BIN
examples/todo_app_sqlite_viz/Todos.db
Normal file
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
|
||||
CREATE TABLE IF NOT EXISTS todos
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
title VARCHAR,
|
||||
completed BOOLEAN
|
||||
);
|
||||
BIN
examples/todo_app_sqlite_viz/public/favicon.ico
Normal file
BIN
examples/todo_app_sqlite_viz/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
61
examples/todo_app_sqlite_viz/src/error_template.rs
Normal file
61
examples/todo_app_sqlite_viz/src/error_template.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use crate::errors::TodoAppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{Errors, *};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_viz::ResponseOptions;
|
||||
|
||||
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
||||
// here than just displaying them
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
cx: Scope,
|
||||
#[prop(optional)] outside_errors: Option<Errors>,
|
||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||
) -> impl IntoView {
|
||||
let errors = match outside_errors {
|
||||
Some(e) => create_rw_signal(cx, e),
|
||||
None => match errors {
|
||||
Some(e) => e,
|
||||
None => panic!("No Errors found and we expected errors!"),
|
||||
},
|
||||
};
|
||||
|
||||
// Get Errors from Signal
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors: Vec<TodoAppError> = errors
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter_map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
|
||||
.collect();
|
||||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
cfg_if! {
|
||||
if #[cfg(feature="ssr")]{
|
||||
let response = use_context::<ResponseOptions>(cx);
|
||||
if let Some(response) = response{
|
||||
response.set_status(errors[0].status_code());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view! {cx,
|
||||
<h1>"Errors"</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
view= move |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! {
|
||||
cx,
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
21
examples/todo_app_sqlite_viz/src/errors.rs
Normal file
21
examples/todo_app_sqlite_viz/src/errors.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use http::status::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum TodoAppError {
|
||||
#[error("Not Found")]
|
||||
NotFound,
|
||||
#[error("Internal Server Error")]
|
||||
InternalServerError,
|
||||
}
|
||||
|
||||
impl TodoAppError {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
TodoAppError::NotFound => StatusCode::NOT_FOUND,
|
||||
TodoAppError::InternalServerError => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
examples/todo_app_sqlite_viz/src/fallback.rs
Normal file
58
examples/todo_app_sqlite_viz/src/fallback.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use crate::{
|
||||
error_template::{ErrorTemplate, ErrorTemplateProps},
|
||||
errors::TodoAppError,
|
||||
};
|
||||
use http::Uri;
|
||||
use leptos::{view, Errors, LeptosOptions};
|
||||
use std::sync::Arc;
|
||||
use viz::{
|
||||
handlers::serve, header::HeaderMap, types::RouteInfo, Body, Error, Handler,
|
||||
Request, RequestExt, Response, ResponseExt, Result,
|
||||
};
|
||||
|
||||
pub async fn file_and_error_handler(req: Request<Body>) -> Result<Response> {
|
||||
let uri = req.uri().clone();
|
||||
let headers = req.headers().clone();
|
||||
let route_info = req.route_info().clone();
|
||||
let options = &*req.state::<Arc<LeptosOptions>>().ok_or(
|
||||
Error::Responder(Response::text("missing state type LeptosOptions")),
|
||||
)?;
|
||||
let root = &options.site_root;
|
||||
let resp = get_static_file(uri, &root, headers, route_info).await?;
|
||||
let status = resp.status();
|
||||
|
||||
if status.is_success() || status.is_redirection() {
|
||||
Ok(resp)
|
||||
} else {
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(TodoAppError::NotFound);
|
||||
let handler = leptos_viz::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move |cx| view! {cx, <ErrorTemplate outside_errors=errors.clone()/>},
|
||||
);
|
||||
handler(req).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
headers: HeaderMap,
|
||||
route_info: Arc<RouteInfo>,
|
||||
) -> Result<Response> {
|
||||
let mut req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.extension(route_info)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
*req.headers_mut() = headers;
|
||||
// This path is relative to the cargo root
|
||||
serve::Dir::new(root).call(req).await
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
25
examples/todo_app_sqlite_viz/src/lib.rs
Normal file
25
examples/todo_app_sqlite_viz/src/lib.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
pub mod fallback;
|
||||
pub mod todo;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(|cx| {
|
||||
view! { cx, <TodoApp/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
80
examples/todo_app_sqlite_viz/src/main.rs
Normal file
80
examples/todo_app_sqlite_viz/src/main.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use crate::fallback::file_and_error_handler;
|
||||
use crate::todo::*;
|
||||
use leptos_viz::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
use todo_app_sqlite_viz::*;
|
||||
use viz::{
|
||||
types::{State, StateError},
|
||||
Request, RequestExt, Response, Result, Router, ServiceMaker,
|
||||
};
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
async fn custom_handler(req: Request) -> Result<Response> {
|
||||
let id = req.params::<String>()?;
|
||||
let options = &*req
|
||||
.state::<Arc<LeptosOptions>>()
|
||||
.ok_or(StateError::new::<LeptosOptions>())?;
|
||||
let handler = leptos_viz::render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
move |cx| {
|
||||
provide_context(cx, id.clone());
|
||||
},
|
||||
|cx| view! { cx, <TodoApp/> },
|
||||
);
|
||||
handler(req).await
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Debug)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
let conn = db().await.expect("couldn't connect to DB");
|
||||
/* sqlx::migrate!()
|
||||
.run(&mut conn)
|
||||
.await
|
||||
.expect("could not run SQLx migrations"); */
|
||||
|
||||
crate::todo::register_server_functions();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.post("/api/:fn_name*", leptos_viz::handle_server_fns)
|
||||
.get("/special/:id", custom_handler)
|
||||
.leptos_routes(
|
||||
leptos_options.clone(),
|
||||
routes,
|
||||
|cx| view! { cx, <TodoApp/> },
|
||||
)
|
||||
.get("/*", file_and_error_handler)
|
||||
.with(State(Arc::new(leptos_options)));
|
||||
|
||||
// run our app with hyper
|
||||
// `viz::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
viz::Server::bind(&addr)
|
||||
.serve(ServiceMaker::from(app))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
pub fn main() {
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// Only the server may directly connect to the database.
|
||||
}
|
||||
}
|
||||
}
|
||||
220
examples/todo_app_sqlite_viz/src/todo.rs
Normal file
220
examples/todo_app_sqlite_viz/src/todo.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
pub fn register_server_functions() {
|
||||
_ = GetTodos::register();
|
||||
_ = AddTodo::register();
|
||||
_ = DeleteTodo::register();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
} else {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(GetTodos, "/api")]
|
||||
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
// this is just an example of how to access server context injected in the handlers
|
||||
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
|
||||
let req_parts = use_context::<leptos_viz::RequestParts>(cx);
|
||||
|
||||
if let Some(req_parts) = req_parts {
|
||||
println!("Uri = {:?}", req_parts.uri);
|
||||
}
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let mut conn = db().await?;
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
// Add a random header(because why not)
|
||||
// let mut res_headers = HeaderMap::new();
|
||||
// res_headers.insert(SET_COOKIE, HeaderValue::from_str("fizz=buzz").unwrap());
|
||||
|
||||
// let res_parts = leptos_viz::ResponseParts {
|
||||
// headers: res_headers,
|
||||
// status: Some(StatusCode::IM_A_TEAPOT),
|
||||
// };
|
||||
|
||||
// let res_options_outer = use_context::<leptos_viz::ResponseOptions>(cx);
|
||||
// if let Some(res_options) = res_options_outer {
|
||||
// res_options.overwrite(res_parts).await;
|
||||
// }
|
||||
|
||||
Ok(todos)
|
||||
}
|
||||
|
||||
#[server(AddTodo, "/api")]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||
|
||||
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
.bind(title)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(_row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[server(DeleteTodo, "/api")]
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
//let id = use_context::<String>(cx);
|
||||
provide_meta_context(cx);
|
||||
view! {
|
||||
cx,
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/todo_app_sqlite_viz.css"/>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"My Tasks"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<Todos/>
|
||||
</ErrorBoundary>
|
||||
}/> //Route
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
let add_todo = create_server_multi_action::<AddTodo>(cx);
|
||||
let delete_todo = create_server_action::<DeleteTodo>(cx);
|
||||
let submissions = add_todo.submissions();
|
||||
|
||||
// list of todos is loaded from the server in reaction to changes
|
||||
let todos = create_resource(
|
||||
cx,
|
||||
move || (add_todo.version().get(), delete_todo.version().get()),
|
||||
move |_| get_todos(cx),
|
||||
);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<MultiActionForm action=add_todo>
|
||||
<label>
|
||||
"Add a Todo"
|
||||
<input type="text" name="title"/>
|
||||
</label>
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
vec![view! { cx, <p>"No tasks were found."</p> }.into_any()]
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
.into_any()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
3
examples/todo_app_sqlite_viz/style.css
Normal file
3
examples/todo_app_sqlite_viz/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
21
integrations/viz/Cargo.toml
Normal file
21
integrations/viz/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "leptos_viz"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston", "Fangdun Tsai"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Viz integrations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
viz = { version = "0.4.8" }
|
||||
futures = "0.3"
|
||||
http = "0.2.8"
|
||||
hyper = "0.14.23"
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_config = { workspace = true }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
1077
integrations/viz/src/lib.rs
Normal file
1077
integrations/viz/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -58,8 +58,9 @@
|
||||
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
|
||||
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
|
||||
//! a fully-functional that works as intended even before WASM has loaded and begun to run.
|
||||
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite) and
|
||||
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum)
|
||||
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite),
|
||||
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum), and
|
||||
//! [`todo_app_sqlite_viz`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_viz)
|
||||
//! show how to build a full-stack app using server functions and database connections.
|
||||
//! - [`tailwind`](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind) shows how to integrate
|
||||
//! TailwindCSS with `cargo-leptos`.
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use leptos_dom::{Fragment, IntoView, View};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{Scope, SignalSetter};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use leptos_reactive::{use_context, Scope, SignalSetter, SuspenseContext};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
/// If any [Resource](leptos_reactive::Resource)s are read in the `children` of this
|
||||
/// component, it will show the `fallback` while they are loading. Once all are resolved,
|
||||
@@ -74,18 +77,34 @@ where
|
||||
F: Fn() -> E + 'static,
|
||||
E: IntoView,
|
||||
{
|
||||
let prev_children = std::rc::Rc::new(RefCell::new(None::<Vec<View>>));
|
||||
let prev_children = Rc::new(RefCell::new(None::<Vec<View>>));
|
||||
|
||||
let first_run = Rc::new(std::cell::Cell::new(true));
|
||||
let child_runs = Cell::new(0);
|
||||
|
||||
crate::Suspense(
|
||||
cx,
|
||||
crate::SuspenseProps::builder()
|
||||
.fallback({
|
||||
let prev_child = Rc::clone(&prev_children);
|
||||
let first_run = Rc::clone(&first_run);
|
||||
move || {
|
||||
let suspense_context = use_context::<SuspenseContext>(cx)
|
||||
.expect("there to be a SuspenseContext");
|
||||
|
||||
let is_first_run =
|
||||
is_first_run(&first_run, &suspense_context);
|
||||
first_run.set(is_first_run);
|
||||
|
||||
if let Some(set_pending) = &set_pending {
|
||||
set_pending.set(true);
|
||||
}
|
||||
if let Some(prev_children) = &*prev_child.borrow() {
|
||||
prev_children.clone().into_view(cx)
|
||||
if is_first_run {
|
||||
fallback().into_view(cx)
|
||||
} else {
|
||||
prev_children.clone().into_view(cx)
|
||||
}
|
||||
} else {
|
||||
fallback().into_view(cx)
|
||||
}
|
||||
@@ -93,7 +112,21 @@ where
|
||||
})
|
||||
.children(Box::new(move |cx| {
|
||||
let frag = children(cx);
|
||||
*prev_children.borrow_mut() = Some(frag.nodes.clone());
|
||||
|
||||
let suspense_context = use_context::<SuspenseContext>(cx)
|
||||
.expect("there to be a SuspenseContext");
|
||||
|
||||
if is_first_run(&first_run, &suspense_context) {
|
||||
let has_local_only = suspense_context.has_local_only();
|
||||
*prev_children.borrow_mut() = Some(frag.nodes.clone());
|
||||
if (has_local_only && child_runs.get() > 0)
|
||||
|| !has_local_only
|
||||
{
|
||||
first_run.set(false);
|
||||
}
|
||||
}
|
||||
child_runs.set(child_runs.get() + 1);
|
||||
|
||||
if let Some(set_pending) = &set_pending {
|
||||
set_pending.set(false);
|
||||
}
|
||||
@@ -102,3 +135,22 @@ where
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
fn is_first_run(
|
||||
first_run: &Rc<Cell<bool>>,
|
||||
suspense_context: &SuspenseContext,
|
||||
) -> bool {
|
||||
match (
|
||||
first_run.get(),
|
||||
cfg!(feature = "hydrate"),
|
||||
suspense_context.has_local_only(),
|
||||
) {
|
||||
(false, _, _) => false,
|
||||
// is in hydrate mode, and has non-local resources (so, has streamed)
|
||||
(_, false, false) => false,
|
||||
// is in hydrate mode, but with only local resources (so, has not streamed)
|
||||
(_, false, true) => true,
|
||||
// either SSR or client mode: it's the first run
|
||||
(_, true, _) => true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct ConfFile {
|
||||
}
|
||||
|
||||
/// This struct serves as a convenient place to store details used for configuring Leptos.
|
||||
/// It's used in our actix and axum integrations to generate the
|
||||
/// It's used in our actix, axum, and viz integrations to generate the
|
||||
/// correct path for WASM, JS, and Websockets, as well as other configuration tasks.
|
||||
/// It shares keys with cargo-leptos, to allow for easy interoperability
|
||||
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
|
||||
|
||||
@@ -226,6 +226,7 @@ generate_event_types! {
|
||||
submit: SubmitEvent,
|
||||
suspend: Event,
|
||||
timeupdate: Event,
|
||||
#[does_not_bubble]
|
||||
toggle: Event,
|
||||
touchcancel: TouchEvent,
|
||||
touchend: TouchEvent,
|
||||
|
||||
@@ -130,6 +130,7 @@ where
|
||||
resolved: Rc::new(Cell::new(resolved)),
|
||||
scheduled: Rc::new(Cell::new(false)),
|
||||
suspense_contexts: Default::default(),
|
||||
serializable: true,
|
||||
});
|
||||
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
@@ -253,6 +254,7 @@ where
|
||||
resolved: Rc::new(Cell::new(resolved)),
|
||||
scheduled: Rc::new(Cell::new(false)),
|
||||
suspense_contexts: Default::default(),
|
||||
serializable: false,
|
||||
});
|
||||
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
@@ -548,6 +550,7 @@ where
|
||||
resolved: Rc<Cell<bool>>,
|
||||
scheduled: Rc<Cell<bool>>,
|
||||
suspense_contexts: Rc<RefCell<HashSet<SuspenseContext>>>,
|
||||
serializable: bool,
|
||||
}
|
||||
|
||||
impl<S, T> ResourceState<S, T>
|
||||
@@ -574,6 +577,13 @@ where
|
||||
let suspense_contexts = self.suspense_contexts.clone();
|
||||
let has_value = v.is_some();
|
||||
|
||||
let serializable = self.serializable;
|
||||
if let Some(suspense_cx) = &suspense_cx {
|
||||
if serializable {
|
||||
suspense_cx.has_local_only.set_value(false);
|
||||
}
|
||||
}
|
||||
|
||||
let increment = move |_: Option<()>| {
|
||||
if let Some(s) = &suspense_cx {
|
||||
if let Ok(ref mut contexts) = suspense_contexts.try_borrow_mut()
|
||||
@@ -585,7 +595,7 @@ where
|
||||
// because the context has been tracked here
|
||||
// on the first read, resource is already loading without having incremented
|
||||
if !has_value {
|
||||
s.increment();
|
||||
s.increment(serializable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -626,10 +636,11 @@ where
|
||||
let suspense_contexts = self.suspense_contexts.clone();
|
||||
|
||||
for suspense_context in suspense_contexts.borrow().iter() {
|
||||
suspense_context.increment();
|
||||
suspense_context.increment(self.serializable);
|
||||
}
|
||||
|
||||
// run the Future
|
||||
let serializable = self.serializable;
|
||||
spawn_local({
|
||||
let resolved = self.resolved.clone();
|
||||
let set_value = self.set_value;
|
||||
@@ -643,7 +654,7 @@ where
|
||||
set_loading.update(|n| *n = false);
|
||||
|
||||
for suspense_context in suspense_contexts.borrow().iter() {
|
||||
suspense_context.decrement();
|
||||
suspense_context.decrement(serializable);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -381,8 +381,11 @@ impl Scope {
|
||||
let (tx2, mut rx2) = futures::channel::mpsc::unbounded();
|
||||
|
||||
create_isomorphic_effect(*self, move |_| {
|
||||
let pending =
|
||||
context.pending_resources.try_with(|n| *n).unwrap_or(0);
|
||||
let pending = context
|
||||
.pending_serializable_resources
|
||||
.read_only()
|
||||
.try_with(|n| *n)
|
||||
.unwrap_or(0);
|
||||
if pending == 0 {
|
||||
_ = tx1.unbounded_send(());
|
||||
_ = tx2.unbounded_send(());
|
||||
@@ -415,9 +418,7 @@ impl Scope {
|
||||
{
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let mut shared_context = runtime.shared_context.borrow_mut();
|
||||
let f = std::mem::take(&mut shared_context.pending_fragments);
|
||||
println!("pending_fragments = {}", f.len());
|
||||
f
|
||||
std::mem::take(&mut shared_context.pending_fragments)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ pub fn create_slice<T, O>(
|
||||
setter: impl Fn(&mut T, O) + Clone + Copy + 'static,
|
||||
) -> (Signal<O>, SignalSetter<O>)
|
||||
where
|
||||
O: Eq,
|
||||
O: PartialEq,
|
||||
{
|
||||
let getter = create_memo(cx, move |_| signal.with(getter));
|
||||
let setter = move |value| signal.update(|x| setter(x, value));
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
create_signal, queue_microtask, ReadSignal, Scope, SignalUpdate,
|
||||
WriteSignal,
|
||||
create_rw_signal, create_signal, queue_microtask, store_value, ReadSignal,
|
||||
RwSignal, Scope, SignalUpdate, StoredValue, WriteSignal,
|
||||
};
|
||||
use futures::Future;
|
||||
use std::{borrow::Cow, pin::Pin};
|
||||
@@ -15,6 +15,15 @@ pub struct SuspenseContext {
|
||||
/// The number of resources that are currently pending.
|
||||
pub pending_resources: ReadSignal<usize>,
|
||||
set_pending_resources: WriteSignal<usize>,
|
||||
pub(crate) pending_serializable_resources: RwSignal<usize>,
|
||||
pub(crate) has_local_only: StoredValue<bool>,
|
||||
}
|
||||
|
||||
impl SuspenseContext {
|
||||
/// Whether the suspense contains local resources at this moment, and therefore can't be
|
||||
pub fn has_local_only(&self) -> bool {
|
||||
self.has_local_only.get_value()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for SuspenseContext {
|
||||
@@ -35,29 +44,47 @@ impl SuspenseContext {
|
||||
/// Creates an empty suspense context.
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
let (pending_resources, set_pending_resources) = create_signal(cx, 0);
|
||||
let pending_serializable_resources = create_rw_signal(cx, 0);
|
||||
let has_local_only = store_value(cx, true);
|
||||
Self {
|
||||
pending_resources,
|
||||
set_pending_resources,
|
||||
pending_serializable_resources,
|
||||
has_local_only,
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies the suspense context that a new resource is now pending.
|
||||
pub fn increment(&self) {
|
||||
pub fn increment(&self, serializable: bool) {
|
||||
let setter = self.set_pending_resources;
|
||||
let serializable_resources = self.pending_serializable_resources;
|
||||
let has_local_only = self.has_local_only;
|
||||
queue_microtask(move || {
|
||||
setter.update(|n| *n += 1);
|
||||
if serializable {
|
||||
serializable_resources.update(|n| *n += 1);
|
||||
has_local_only.set_value(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Notifies the suspense context that a resource has resolved.
|
||||
pub fn decrement(&self) {
|
||||
pub fn decrement(&self, serializable: bool) {
|
||||
let setter = self.set_pending_resources;
|
||||
let serializable_resources = self.pending_serializable_resources;
|
||||
queue_microtask(move || {
|
||||
setter.update(|n| {
|
||||
if *n > 0 {
|
||||
*n -= 1
|
||||
}
|
||||
});
|
||||
if serializable {
|
||||
serializable_resources.update(|n| {
|
||||
if *n > 0 {
|
||||
*n -= 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,18 +8,37 @@ use std::{cell::RefCell, rc::Rc};
|
||||
pub struct HtmlContext {
|
||||
lang: Rc<RefCell<Option<TextProp>>>,
|
||||
dir: Rc<RefCell<Option<TextProp>>>,
|
||||
class: Rc<RefCell<Option<TextProp>>>,
|
||||
}
|
||||
|
||||
impl HtmlContext {
|
||||
/// Converts the `<html>` metadata into an HTML string.
|
||||
pub fn as_string(&self) -> Option<String> {
|
||||
match (self.lang.borrow().as_ref(), self.dir.borrow().as_ref()) {
|
||||
(None, None) => None,
|
||||
(Some(lang), None) => Some(format!(" lang=\"{}\"", lang.get())),
|
||||
(None, Some(dir)) => Some(format!(" dir=\"{}\"", dir.get())),
|
||||
(Some(lang), Some(dir)) => {
|
||||
Some(format!(" lang=\"{}\" dir=\"{}\"", lang.get(), dir.get()))
|
||||
}
|
||||
let lang = self
|
||||
.lang
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|val| format!("lang=\"{}\"", val.get()));
|
||||
let dir = self
|
||||
.dir
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|val| format!("dir=\"{}\"", val.get()));
|
||||
let class = self
|
||||
.class
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|val| format!("class=\"{}\"", val.get()));
|
||||
let mut val = [lang, dir, class]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
if val.is_empty() {
|
||||
None
|
||||
} else {
|
||||
val.insert(0, ' ');
|
||||
Some(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +76,9 @@ pub fn Html(
|
||||
/// The `dir` attribute on the `<html>`.
|
||||
#[prop(optional, into)]
|
||||
dir: Option<TextProp>,
|
||||
/// The `class` attribute on the `<html>`.
|
||||
#[prop(optional, into)]
|
||||
class: Option<TextProp>,
|
||||
) -> impl IntoView {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
@@ -71,15 +93,24 @@ pub fn Html(
|
||||
}
|
||||
|
||||
if let Some(dir) = dir {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |_| {
|
||||
let value = dir.get();
|
||||
_ = el.set_attribute("dir", &value);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(class) = class {
|
||||
create_render_effect(cx, move |_| {
|
||||
let value = class.get();
|
||||
_ = el.set_attribute("class", &value);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let meta = crate::use_head(cx);
|
||||
*meta.html.lang.borrow_mut() = lang;
|
||||
*meta.html.dir.borrow_mut() = dir;
|
||||
*meta.html.class.borrow_mut() = class;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ pub fn Link(
|
||||
|
||||
let builder_el = leptos::leptos_dom::html::link(cx)
|
||||
.attr("id", &id)
|
||||
.attr("as_", as_)
|
||||
.attr("as", as_)
|
||||
.attr("crossorigin", crossorigin)
|
||||
.attr("disabled", disabled.unwrap_or(false))
|
||||
.attr("fetchpriority", fetchpriority)
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::rc::Rc;
|
||||
/// an absolute path, prefix it with `/`).
|
||||
///
|
||||
/// **Note**: Support for server-side redirects is provided by the server framework
|
||||
/// integrations (`leptos_actix` and `leptos_axum`). If you’re not using one of those
|
||||
/// integrations (`leptos_actix`, `leptos_axum`, and `leptos_viz`). If you’re not using one of those
|
||||
/// integrations, you should manually provide a way of redirecting on the server
|
||||
/// using [provide_server_redirect].
|
||||
#[component]
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{cell::RefCell, rc::Rc};
|
||||
pub struct PossibleBranchContext(pub(crate) Rc<RefCell<Vec<Branch>>>);
|
||||
|
||||
/// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router
|
||||
/// format. Odds are you want `generate_route_list()` from either the actix or axum integrations if you want
|
||||
/// format. Odds are you want `generate_route_list()` from either the actix, axum, or viz integrations if you want
|
||||
/// to work with their router
|
||||
pub fn generate_route_list_inner<IV>(
|
||||
app_fn: impl FnOnce(Scope) -> IV + 'static,
|
||||
|
||||
Reference in New Issue
Block a user