mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 13:43:01 -05:00
Compare commits
1 Commits
v0.3.1
...
api-routes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
beea04a050 |
28
Cargo.toml
28
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.1"
|
||||
version = "0.3.0-alpha"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.3.1" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.1" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.1" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.1" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.1" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.1" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.3.1" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.1" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.1" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.1" }
|
||||
leptos_router = { path = "./router", version = "0.3.1" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.3.1" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.1" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0-alpha" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0-alpha" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0-alpha" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0-alpha" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_router = { path = "./router", version = "0.3.0-alpha" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0-alpha" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -69,11 +69,7 @@ dependencies = [
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = [
|
||||
"test-all",
|
||||
"test-leptos_macro-example",
|
||||
"doc-leptos_macro-example",
|
||||
]
|
||||
dependencies = ["test-all", "test-leptos_macro-example", "doc-leptos_macro-example"]
|
||||
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
@@ -106,15 +102,9 @@ cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "verify-flow"]
|
||||
|
||||
[tasks.clean-examples]
|
||||
description = "Clean all example projects"
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "clean-all"]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
|
||||
LEPTOS_OUTPUT_NAME="ci" # allows examples to check/build without cargo-leptos
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS = "-D warnings"
|
||||
|
||||
@@ -171,4 +171,4 @@ data flow and of fine-grained reactive updates.
|
||||
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh">
|
||||
|
||||
@@ -34,10 +34,9 @@
|
||||
- [`cargo-leptos`](./ssr/21_cargo_leptos.md)
|
||||
- [The Life of a Page Load](./ssr/22_life_cycle.md)
|
||||
- [Async Rendering and SSR “Modes”](./ssr/23_ssr_modes.md)
|
||||
- [Hydration Bugs](./ssr/24_hydration_bugs.md)
|
||||
- [Working with the Server](./server/README.md)
|
||||
- [Server Functions](./server/25_server_functions.md)
|
||||
- [Request/Response]()
|
||||
- [Hydration Footguns](./ssr/24_hydration_bugs.md)
|
||||
- [Server Functions]()
|
||||
- [Request/Response]()
|
||||
- [Extractors]()
|
||||
- [Axum]()
|
||||
- [Actix]()
|
||||
@@ -47,6 +46,6 @@
|
||||
- [Actions]()
|
||||
- [Forms]()
|
||||
- [`<ActionForm/>`s]()
|
||||
- [Turning off WebAssembly: Progressive Enhancement and Graceful Degradation]()
|
||||
- [Turning off WebAssembly]()
|
||||
- [Advanced Reactivity]()
|
||||
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
So far we’ve only been working with synchronous users interfaces: You provide some input,
|
||||
the app immediately processes 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. Leptos provides a cross-platform [`spawn_local`](https://docs.rs/leptos/latest/leptos/fn.spawn_local.html) function that makes it easy to run a `Future`, but there’s much more to it than that.
|
||||
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.
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
# Server Functions
|
||||
|
||||
If you’re creating anything beyond a toy app, you’ll need to run code on the server all the time: reading from or writing to a database that only runs on the server, running expensive computations using libraries you don’t want to ship down to the client, accessing APIs that need to be called from the server rather than the client for CORS reasons or because you need a secret API key that’s stored on the server and definitely shouldn’t be shipped down to a user’s browser.
|
||||
|
||||
Traditionally, this is done by separating your server and client code, and by setting up something like a REST API or GraphQL API to allow your client to fetch and mutate data on the server. This is fine, but it requires you to write and maintain your code in multiple separate places (client-side code for fetching, server-side functions to run), as well as creating a third thing to manage, which is the API contract between the two.
|
||||
|
||||
Leptos is one of a number of modern frameworks that introduce the concept of **server functions**. Server functions have two key characteristics:
|
||||
|
||||
1. Server functions are **co-located** with your component code, so that you can organize your work by feature, not by technology. For example, you might have a “dark mode” feature that should persist a user’s dark/light mode preference across sessions, and be applied during server rendering so there’s no flicker. This requires a component that needs to be interactive on the client, and some work to be done on the server (setting a cookie, maybe even storing a user in a database.) Traditionally, this feature might end up being split between two different locations in your code, one in your “frontend” and one in your “backend.” With server functions, you’ll probably just write them both in one `dark_mode.rs` and forget about it.
|
||||
2. Server functions are **isomorphic**, i.e., they can be called either from the server or the browser. This is done by generating code differently for the two platforms. On the server, a server function simply runs. In the browser, the server function’s body is replaced with a stub that actually makes a fetch request to the server, serializing the arguments into the request and deserializing the return value from the response. But on either end, the function can simply be called: you can create an `add_todo` function that writes to your database, and simply call it from a click handler on a button in the browser!
|
||||
|
||||
## Using Server Functions
|
||||
|
||||
Actually, I kind of like that example. What would it look like? It’s pretty simple, actually.
|
||||
|
||||
```rust
|
||||
// todo.rs
|
||||
|
||||
#[server(AddTodo, "/api")]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
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())),
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BusyButton(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<button on:click=move |_| {
|
||||
spawn_local(async {
|
||||
add_todo("So much to do!".to_string()).await;
|
||||
});
|
||||
}>
|
||||
"Add Todo"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
// somewhere in main.rs
|
||||
fn main() {
|
||||
// ...
|
||||
|
||||
AddTodo::register();
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
You’ll notice a couple things here right away:
|
||||
|
||||
- Server functions can use server-only dependencies, like `sqlx`, and can access server-only resources, like our database.
|
||||
- Server functions are `async`. Even if they only did synchronous work on the server, the function signature would still need to be `async`, because calling them from the browser _must_ be asynchronous.
|
||||
- Server functions return `Result<T, ServerFnError>`. Again, even if they only do infallible work on the server, this is true, because `ServerFnError`’s variants include the various things that can be wrong during the process of making a network request.
|
||||
- Server functions can be called from the client. Take a look at our click handler. This is code that will _only ever_ run on the client. But it can call the function `add_todo` (using `spawn_local` to run the `Future`) as if it were an ordinary async function:
|
||||
|
||||
```rust
|
||||
move |_| {
|
||||
spawn_local(async {
|
||||
add_todo("So much to do!".to_string()).await;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- Server functions are top-level functions defined with `fn`. Unlike event listeners, derived signals, and most everything else in Leptos, they are not closures! As `fn` calls, they have no access to the reactive state of your app or anything else that is not passed in as an argument. And again, this makes perfect sense: When you make a request to the server, the server doesn’t have access to client state unless you send it explicitly. (Otherwise we’d have to serialize the whole reactive system and send it across the wire with every request, which—while it served classic ASP for a while—is a really bad idea.)
|
||||
- Server function arguments and return values both need to be serializable with `serde`. Again, hopefully this makes sense: while function arguments in general don’t need to be serialized, calling a server function from the browser means serializing the arguments and sending them over HTTP.
|
||||
|
||||
There are a few things to note about the way you define a server function, too.
|
||||
|
||||
- Server functions are created by using the [`#[server]` macro](https://docs.rs/leptos_server/latest/leptos_server/index.html#server) to annotate a top-level function, which can be defined anywhere.
|
||||
- We provide the macro a type name. The type name is used to register the server function (in `main.rs`), and it’s used internally as a container to hold, serialize, and deserialize the arguments.
|
||||
- We provide the macro a path. This is a prefix for the path at which we’ll mount a server function handler on our server. (See examples for [Actix](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/main.rs#L44) and [Axum](https://github.com/leptos-rs/leptos/blob/598523cd9d0d775b017cb721e41ebae9349f01e2/examples/todo_app_sqlite_axum/src/main.rs#L51).)
|
||||
- You’ll need to have `serde` as a dependency with the `derive` featured enabled for the macro to work properly. You can easily add it to `Cargo.toml` with `cargo add serde --features=derive`.
|
||||
|
||||
## Server Function Encodings
|
||||
|
||||
By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body of the request. (This means that server functions can be called from HTML forms, which we’ll see in a future chapter.) But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` macro to specify an alternate encoding:
|
||||
|
||||
```rust
|
||||
#[server(AddTodo, "/api", "Url")]
|
||||
#[server(AddTodo, "/api", "GetJson")]
|
||||
#[server(AddTodo, "/api", "Cbor")]
|
||||
#[server(AddTodo, "/api", "GetCbor")]
|
||||
```
|
||||
|
||||
The four options use different combinations of HTTP verbs and encoding methods:
|
||||
|
||||
| Name | Method | Request | Response |
|
||||
| ----------------- | ------ | ----------- | -------- |
|
||||
| **Url** (default) | POST | URL encoded | JSON |
|
||||
| **GetJson** | GET | URL encoded | JSON |
|
||||
| **Cbor** | POST | CBOR | CBOR |
|
||||
| **GetCbor** | GET | URL encoded | CBOR |
|
||||
|
||||
In other words, you have two choices:
|
||||
|
||||
- `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached, `GET` requests can be.
|
||||
- Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64 string)?
|
||||
|
||||
**But remember**: Leptos will handle all the details of this encoding and decoding for you. When you use a server function, it looks just like calling any other asynchronous function!
|
||||
|
||||
## An Important Note on Security
|
||||
|
||||
Server functions are a cool technology, but it’s very important to remember. **Server functions are not magic; they’re syntax sugar for defining a public API.** The _body_ of a server function is never made public; it’s just part of your server binary. But the server function is a publicly accessible API endpoint, and it’s return value is just a JSON or similar blob. You should _never_ return something sensitive from a server function.
|
||||
|
||||
## Integrating Server Functions with Leptos
|
||||
|
||||
So far, everything I’ve said is actually framework agnostic. (And in fact, the Leptos server function crate has been integrated into Dioxus as well!) Server functions are simply a way of defining a function-like RPC call that leans on Web standards like HTTP requests and URL encoding.
|
||||
|
||||
But in a way, they also provide the last missing primitive in our story so far. Because a server function is just a plain Rust async function, it integrates perfectly with the async Leptos primitives we discussed [earlier](../async/README.md). So you can easily integrate your server functions with the rest of your applications:
|
||||
|
||||
- Create **resources** that call the server function to load data from the server
|
||||
- Read these resources under `<Suspense/>` or `<Transition/>` to enable streaming SSR and fallback states while data loads.
|
||||
- Create **actions** that call the server function to mutate data on the server
|
||||
|
||||
The final section of this book will make this a little more concrete by introducing patterns that use progressively-enhanced HTML forms to run these server actions.
|
||||
|
||||
But in the next few chapters, we’ll actually take a look at some of the details of what you might want to do with your server functions, including the best ways to integrate with the powerful extractors provided by the Actix and Axum server frameworks.
|
||||
@@ -1,11 +0,0 @@
|
||||
# Working with the Server
|
||||
|
||||
The previous section described the process of server-side rendering, using the server to generate an HTML version of the page that will become interactive in the browser. So far, everything has been “isomorphic” or “universal”; in other words, your app has had the “same (_iso_) shape (_morphe_)” on the client and the server.
|
||||
|
||||
But a server can do a lot more than just render HTML! In fact, a server can do a whole bunch of things your browser _can’t,_ like reading from and writing to a SQL database.
|
||||
|
||||
If you’re used to building JavaScript frontend apps, you’re probably used to calling out to some kind of REST API to do this sort of server work. If you’re used to building sites with PHP or Python or Ruby (or Java or C# or...), this server-side work is your bread and butter, and it’s the client-side interactivity that tends to be an afterthought.
|
||||
|
||||
With Leptos, you can do both: not only in the same language, not only sharing the same types, but even in the same files!
|
||||
|
||||
This section will talk about how to build the uniquely-server-side parts of your application.
|
||||
@@ -32,6 +32,6 @@ cargo leptos watch
|
||||
|
||||
Once your app has compiled you can open up your browser to [`http://localhost:3000`](http://localhost:3000) to see it.
|
||||
|
||||
`cargo-leptos` has lots of additional features and built in tools. You can learn more [in its `README`](https://github.com/leptos-rs/cargo-leptos/blob/main/README.md).
|
||||
`cargo-leptos` has lots of additional features and built in tools. You can learn more [in its `README`](https://github.com/leptos-rs/leptos/blob/main/examples/hackernews/src/api.rs).
|
||||
|
||||
But what exactly is happening when you open our browser to `localhost:3000`? Well, read on to find out.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# `view`: Dynamic Classes, Styles and Attributes
|
||||
# `view`: Dynamic Attributes and Classes
|
||||
|
||||
So far we’ve seen how to use the `view` macro to create event listeners and to
|
||||
create dynamic text by passing a function (such as a signal) into the view.
|
||||
|
||||
But of course there are other things you might want to update in your user interface.
|
||||
In this section, we’ll look at how to update classes, styles and attributes dynamically,
|
||||
In this section, we’ll look at how to update attributes and classes dynamically,
|
||||
and we’ll introduce the concept of a **derived signal**.
|
||||
|
||||
Let’s start with a simple component that should be familiar: click a button to
|
||||
@@ -52,42 +52,12 @@ reactively update when the signal changes.
|
||||
Now every time I click the button, the text should toggle between red and black as
|
||||
the number switches between even and odd.
|
||||
|
||||
Some CSS class names can’t be directly parsed by the `view` macro, especially if they include a mix of dashes and numbers or other characters. In that case, you can use a tuple syntax: `class=("name", value)` still directly updates a single class.
|
||||
|
||||
```rust
|
||||
class=("button-20", move || count() % 2 == 1)
|
||||
```
|
||||
|
||||
> If you’re following along, make sure you go into your `index.html` and add something like this:
|
||||
>
|
||||
>
|
||||
> ```html
|
||||
> <style>
|
||||
> .red {
|
||||
> color: red;
|
||||
> }
|
||||
> </style>
|
||||
> <style>.red { color: red; }</style>
|
||||
> ```
|
||||
|
||||
## Dynamic Styles
|
||||
|
||||
Individual CSS properties can be directly updated with a similar `style:` syntax.
|
||||
|
||||
```rust
|
||||
let (x, set_x) = create_signal(cx, 0);
|
||||
let (y, set_y) = create_signal(cx, 0);
|
||||
view! { cx,
|
||||
<div
|
||||
style="position: absolute"
|
||||
style:left=move || format!("{}px", x() + 100)
|
||||
style:top=move || format!("{}px", y() + 100)
|
||||
style:background-color=move || format!("rgb({}, {}, 100)", x(), y())
|
||||
style=("--columns", x)
|
||||
>
|
||||
"Moves when coordinates change"
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Attributes
|
||||
|
||||
The same applies to plain attributes. Passing a plain string or primitive value to
|
||||
|
||||
@@ -31,7 +31,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
|
||||
[tasks.verify-flow]
|
||||
description = "Provides pre and post hooks for verify"
|
||||
dependencies = ["pre-verify", "verify", "post-verify"]
|
||||
dependencies = ["pre-verify-flow", "verify", "post-verify-flow"]
|
||||
|
||||
[tasks.verify]
|
||||
description = "Run all quality checks and tests"
|
||||
@@ -41,17 +41,16 @@ dependencies = ["check-style", "test-unit-and-web"]
|
||||
description = "Run all unit and web tests"
|
||||
dependencies = ["test-flow", "web-test-flow"]
|
||||
|
||||
[tasks.pre-verify]
|
||||
[tasks.pre-verify-flow]
|
||||
|
||||
[tasks.post-verify]
|
||||
dependencies = ["clean-all"]
|
||||
[tasks.post-verify-flow]
|
||||
|
||||
[tasks.web-test-flow]
|
||||
description = "Provides pre and post hooks for web-test"
|
||||
dependencies = ["pre-web-test", "web-test", "post-web-test"]
|
||||
dependencies = ["pre-web-test-flow", "web-test", "post-web-test-flow"]
|
||||
|
||||
[tasks.pre-web-test]
|
||||
[tasks.pre-web-test-flow]
|
||||
|
||||
[tasks.web-test]
|
||||
|
||||
[tasks.post-web-test]
|
||||
[tasks.post-web-test-flow]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
[env]
|
||||
CARGO_MAKE_CLIPPY_ARGS = "--all-targets -- -D warnings"
|
||||
|
||||
[tasks.check-style]
|
||||
description = "Check for style violations"
|
||||
@@ -12,12 +12,3 @@ dependencies = ["check-style", "test-local"]
|
||||
[tasks.test-local]
|
||||
description = "Run all tests from an example directory"
|
||||
dependencies = ["test", "web-test"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
description = "Runs the trunk clean command."
|
||||
category = "Cleanup"
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean", "clean-trunk"]
|
||||
|
||||
@@ -37,7 +37,7 @@ cfg_if! {
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let addr = conf.leptos_options.site_addr.clone();
|
||||
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
|
||||
|
||||
HttpServer::new(move || {
|
||||
@@ -48,7 +48,7 @@ cfg_if! {
|
||||
.service(counter_events)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <Counters/> })
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", &site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{For, *};
|
||||
use leptos::{For, ForProps, *};
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -11,8 +11,8 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::landing::App;
|
||||
use leptos::{LeptosOptions, Errors, view};
|
||||
use crate::landing::{App, AppProps};
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::{error_template::ErrorTemplate, errors::AppError};
|
||||
use crate::{
|
||||
error_template::{ErrorTemplate, ErrorTemplateProps},
|
||||
errors::AppError,
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
@@ -51,8 +54,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||
let generate_internal_error =
|
||||
create_server_action::<CauseInternalServerError>(cx);
|
||||
let generate_internal_error = create_server_action::<CauseInternalServerError>(cx);
|
||||
|
||||
view! { cx,
|
||||
<p>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
pub mod fallback;
|
||||
@@ -7,7 +8,6 @@ pub mod landing;
|
||||
// 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 leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::landing::*;
|
||||
|
||||
|
||||
@@ -37,8 +37,7 @@ async fn custom_handler(
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Debug)
|
||||
.expect("couldn't initialize logging");
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
crate::landing::register_server_functions();
|
||||
|
||||
@@ -52,11 +51,7 @@ async fn main() {
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(
|
||||
leptos_options.clone(),
|
||||
routes,
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -14,7 +14,7 @@ pub enum FetchError {
|
||||
#[error("Error loading data from serving.")]
|
||||
Request,
|
||||
#[error("Error deserializaing cat data from request.")]
|
||||
Json,
|
||||
Json
|
||||
}
|
||||
|
||||
async fn fetch_cats(count: u32) -> Result<Vec<String>, FetchError> {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -4,7 +4,10 @@ use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
mod routes;
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use routes::nav::*;
|
||||
use routes::stories::*;
|
||||
use routes::story::*;
|
||||
use routes::users::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
@@ -7,7 +7,7 @@ cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use hackernews::{App};
|
||||
use hackernews::{App,AppProps};
|
||||
use leptos_actix::{LeptosRoutes, generate_route_list};
|
||||
|
||||
#[get("/style.css")]
|
||||
@@ -24,7 +24,7 @@ cfg_if! {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let addr = conf.leptos_options.site_addr.clone();
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> });
|
||||
|
||||
@@ -37,7 +37,7 @@ cfg_if! {
|
||||
.service(favicon)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <App/> })
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", &site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
@@ -46,7 +46,7 @@ cfg_if! {
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
use hackernews::{App};
|
||||
use hackernews::{App, AppProps};
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use crate::api;
|
||||
|
||||
fn category(from: &str) -> &'static str {
|
||||
match from {
|
||||
"new" => "newest",
|
||||
@@ -36,10 +37,8 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
);
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
let hide_more_link = move |cx| {
|
||||
pending()
|
||||
|| stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28
|
||||
};
|
||||
let hide_more_link =
|
||||
move |cx| pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
@@ -13,20 +13,11 @@ pub fn Story(cx: Scope) -> impl IntoView {
|
||||
if id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
api::fetch_api::<api::Story>(
|
||||
cx,
|
||||
&api::story(&format!("item/{id}")),
|
||||
)
|
||||
.await
|
||||
api::fetch_api::<api::Story>(cx, &api::story(&format!("item/{id}"))).await
|
||||
}
|
||||
},
|
||||
);
|
||||
let meta_description = move || {
|
||||
story
|
||||
.read(cx)
|
||||
.and_then(|story| story.map(|story| story.title))
|
||||
.unwrap_or_else(|| "Loading story...".to_string())
|
||||
};
|
||||
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
|
||||
|
||||
view! { cx,
|
||||
<>
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn User(cx: Scope) -> impl IntoView {
|
||||
<li>
|
||||
<span class="label">"Karma: "</span> {user.karma}
|
||||
</li>
|
||||
<li inner_html={user.about} class="about"></li>
|
||||
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{Scope, Serializable};
|
||||
use leptos::{on_cleanup, Scope, Serializable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn story(path: &str) -> String {
|
||||
@@ -29,7 +29,7 @@ where
|
||||
|
||||
// abort in-flight requests if the Scope is disposed
|
||||
// i.e., if we've navigated away from this page
|
||||
leptos::on_cleanup(cx, move || {
|
||||
on_cleanup(cx, move || {
|
||||
if let Some(abort_controller) = abort_controller {
|
||||
abort_controller.abort()
|
||||
}
|
||||
@@ -38,7 +38,7 @@ where
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn fetch_api<T>(_cx: Scope, path: &str) -> Option<T>
|
||||
pub async fn fetch_api<T>(cx: Scope, path: &str) -> Option<T>
|
||||
where
|
||||
T: Serializable,
|
||||
{
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use leptos::{view, Errors, For, IntoView, RwSignal, Scope, View};
|
||||
use leptos::{
|
||||
signal_prelude::*, view, Errors, For, ForProps, IntoView, RwSignal, Scope,
|
||||
View,
|
||||
};
|
||||
|
||||
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
||||
// here than just displaying them
|
||||
|
||||
@@ -21,7 +21,7 @@ if #[cfg(feature = "ssr")] {
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
|
||||
handler(req).await.into_response()
|
||||
|
||||
@@ -7,7 +7,10 @@ pub mod error_template;
|
||||
pub mod fallback;
|
||||
pub mod handlers;
|
||||
mod routes;
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use routes::nav::*;
|
||||
use routes::stories::*;
|
||||
use routes::story::*;
|
||||
use routes::users::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
@@ -19,7 +19,7 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let addr = leptos_options.site_addr.clone();
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{component, view, IntoView, Scope};
|
||||
use leptos::{component, Scope, IntoView, view};
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use crate::api;
|
||||
|
||||
fn category(from: &str) -> &'static str {
|
||||
match from {
|
||||
"new" => "newest",
|
||||
@@ -36,10 +37,8 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
);
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
let hide_more_link = move || {
|
||||
pending()
|
||||
|| stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28
|
||||
};
|
||||
let hide_more_link =
|
||||
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
@@ -13,20 +13,11 @@ pub fn Story(cx: Scope) -> impl IntoView {
|
||||
if id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
api::fetch_api::<api::Story>(
|
||||
cx,
|
||||
&api::story(&format!("item/{id}")),
|
||||
)
|
||||
.await
|
||||
api::fetch_api::<api::Story>(cx, &api::story(&format!("item/{id}"))).await
|
||||
}
|
||||
},
|
||||
);
|
||||
let meta_description = move || {
|
||||
story
|
||||
.read(cx)
|
||||
.and_then(|story| story.map(|story| story.title))
|
||||
.unwrap_or_else(|| "Loading story...".to_string())
|
||||
};
|
||||
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
|
||||
|
||||
view! { cx,
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = { path = "../cargo-make/common.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
extend = { path = "../../cargo-make/common.toml" }
|
||||
@@ -1 +0,0 @@
|
||||
extend = { path = "../../cargo-make/common.toml" }
|
||||
@@ -1,8 +1,9 @@
|
||||
use api_boundary::*;
|
||||
use gloo_net::http::{Request, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use thiserror::Error;
|
||||
|
||||
use api_boundary::*;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct UnauthorizedApi {
|
||||
url: &'static str,
|
||||
|
||||
@@ -20,56 +20,54 @@ pub fn CredentialsForm(
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<form on:submit=|ev| ev.prevent_default()>
|
||||
<p>{title}</p>
|
||||
{move || {
|
||||
error
|
||||
.get()
|
||||
.map(|err| {
|
||||
view! { cx, <p style="color:red;">{err}</p> }
|
||||
})
|
||||
}}
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
prop:disabled=move || disabled.get()
|
||||
on:keyup=move |ev: ev::KeyboardEvent| {
|
||||
let val = event_target_value(&ev);
|
||||
set_email.update(|v| *v = val);
|
||||
<form on:submit=|ev|ev.prevent_default()>
|
||||
<p>{ title }</p>
|
||||
{move || error.get().map(|err| view!{ cx,
|
||||
<p style ="color:red;" >{ err }</p>
|
||||
})}
|
||||
<input
|
||||
type = "email"
|
||||
required
|
||||
placeholder = "Email address"
|
||||
prop:disabled = move || disabled.get()
|
||||
on:keyup = move |ev: ev::KeyboardEvent| {
|
||||
let val = event_target_value(&ev);
|
||||
set_email.update(|v|*v = val);
|
||||
}
|
||||
// The `change` event fires when the browser fills the form automatically,
|
||||
on:change = move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_email.update(|v|*v = val);
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type = "password"
|
||||
required
|
||||
placeholder = "Password"
|
||||
prop:disabled = move || disabled.get()
|
||||
on:keyup = move |ev: ev::KeyboardEvent| {
|
||||
match &*ev.key() {
|
||||
"Enter" => {
|
||||
dispatch_action();
|
||||
}
|
||||
on:change=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_email.update(|v| *v = val);
|
||||
_=> {
|
||||
let val = event_target_value(&ev);
|
||||
set_password.update(|p|*p = val);
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
placeholder="Password"
|
||||
prop:disabled=move || disabled.get()
|
||||
on:keyup=move |ev: ev::KeyboardEvent| {
|
||||
match &*ev.key() {
|
||||
"Enter" => {
|
||||
dispatch_action();
|
||||
}
|
||||
_ => {
|
||||
let val = event_target_value(&ev);
|
||||
set_password.update(|p| *p = val);
|
||||
}
|
||||
}
|
||||
}
|
||||
on:change=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_password.update(|p| *p = val);
|
||||
}
|
||||
/>
|
||||
<button
|
||||
prop:disabled=move || button_is_disabled.get()
|
||||
on:click=move |_| dispatch_action()
|
||||
>
|
||||
{action_label}
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
// The `change` event fires when the browser fills the form automatically,
|
||||
on:change = move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_password.update(|p|*p = val);
|
||||
}
|
||||
/>
|
||||
<button
|
||||
prop:disabled = move || button_is_disabled.get()
|
||||
on:click = move |_| dispatch_action()
|
||||
>
|
||||
{ action_label }
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::Page;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use crate::Page;
|
||||
|
||||
#[component]
|
||||
pub fn NavBar<F>(
|
||||
cx: Scope,
|
||||
@@ -12,27 +13,20 @@ where
|
||||
F: Fn() + 'static + Clone,
|
||||
{
|
||||
view! { cx,
|
||||
<nav>
|
||||
<Show
|
||||
when=move || logged_in.get()
|
||||
fallback=|cx| {
|
||||
view! { cx,
|
||||
<A href=Page::Login.path()>"Login"</A>
|
||||
" | "
|
||||
<A href=Page::Register.path()>"Register"</A>
|
||||
}
|
||||
}
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
on:click={
|
||||
let on_logout = on_logout.clone();
|
||||
move |_| on_logout()
|
||||
}
|
||||
>
|
||||
"Logout"
|
||||
</a>
|
||||
</Show>
|
||||
</nav>
|
||||
<nav>
|
||||
<Show
|
||||
when = move || logged_in.get()
|
||||
fallback = |cx| view! { cx,
|
||||
<A href=Page::Login.path() >"Login"</A>
|
||||
" | "
|
||||
<A href=Page::Register.path() >"Register"</A>
|
||||
}
|
||||
>
|
||||
<a href="#" on:click={
|
||||
let on_logout = on_logout.clone();
|
||||
move |_| on_logout()
|
||||
}>"Logout"</a>
|
||||
</Show>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use api_boundary::*;
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use api_boundary::*;
|
||||
|
||||
mod api;
|
||||
mod components;
|
||||
mod pages;
|
||||
@@ -85,51 +86,45 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
.expect("LocalStorage::set");
|
||||
}
|
||||
None => {
|
||||
log::debug!(
|
||||
"API is no longer authorized: delete token from \
|
||||
LocalStorage"
|
||||
);
|
||||
log::debug!("API is no longer authorized: delete token from LocalStorage");
|
||||
LocalStorage::delete(API_TOKEN_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<Router>
|
||||
<NavBar logged_in on_logout/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path=Page::Home.path()
|
||||
view=move |cx| {
|
||||
view! { cx, <Home user_info=user_info.into()/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=Page::Login.path()
|
||||
view=move |cx| {
|
||||
view! { cx,
|
||||
<Login
|
||||
api=unauthorized_api
|
||||
on_success=move |api| {
|
||||
log::info!("Successfully logged in");
|
||||
authorized_api.update(|v| *v = Some(api));
|
||||
let navigate = use_navigate(cx);
|
||||
navigate(Page::Home.path(), Default::default()).expect("Home route");
|
||||
fetch_user_info.dispatch(());
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=Page::Register.path()
|
||||
view=move |cx| {
|
||||
view! { cx, <Register api=unauthorized_api/> }
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
<Router>
|
||||
<NavBar logged_in on_logout />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path=Page::Home.path()
|
||||
view=move |cx| view! { cx,
|
||||
<Home user_info = user_info.into() />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=Page::Login.path()
|
||||
view=move |cx| view! { cx,
|
||||
<Login
|
||||
api = unauthorized_api
|
||||
on_success = move |api| {
|
||||
log::info!("Successfully logged in");
|
||||
authorized_api.update(|v| *v = Some(api));
|
||||
let navigate = use_navigate(cx);
|
||||
navigate(Page::Home.path(), Default::default()).expect("Home route");
|
||||
fetch_user_info.dispatch(());
|
||||
} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=Page::Register.path()
|
||||
view=move |cx| view! { cx,
|
||||
<Register api = unauthorized_api />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use client::*;
|
||||
use leptos::*;
|
||||
|
||||
use client::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <App/> })
|
||||
mount_to_body(|cx| view! { cx, <App /> })
|
||||
}
|
||||
|
||||
@@ -6,19 +6,15 @@ use leptos_router::*;
|
||||
#[component]
|
||||
pub fn Home(cx: Scope, user_info: Signal<Option<UserInfo>>) -> impl IntoView {
|
||||
view! { cx,
|
||||
<h2>"Leptos Login example"</h2>
|
||||
{move || match user_info.get() {
|
||||
Some(info) => {
|
||||
view! { cx, <p>"You are logged in with " {info.email} "."</p> }
|
||||
.into_view(cx)
|
||||
}
|
||||
None => {
|
||||
view! { cx,
|
||||
<p>"You are not logged in."</p>
|
||||
<A href=Page::Login.path()>"Login now."</A>
|
||||
}
|
||||
.into_view(cx)
|
||||
}
|
||||
}}
|
||||
<h2>"Leptos Login example"</h2>
|
||||
{move || match user_info.get() {
|
||||
Some(info) => view!{ cx,
|
||||
<p>"You are logged in with "{ info.email }"."</p>
|
||||
}.into_view(cx),
|
||||
None => view!{ cx,
|
||||
<p>"You are not logged in."</p>
|
||||
<A href=Page::Login.path() >"Login now."</A>
|
||||
}.into_view(cx)
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use api_boundary::*;
|
||||
|
||||
use crate::{
|
||||
api::{self, AuthorizedApi, UnauthorizedApi},
|
||||
components::credentials::*,
|
||||
Page,
|
||||
};
|
||||
use api_boundary::*;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn Login<F>(cx: Scope, api: UnauthorizedApi, on_success: F) -> impl IntoView
|
||||
@@ -51,14 +53,14 @@ where
|
||||
let disabled = Signal::derive(cx, move || wait_for_response.get());
|
||||
|
||||
view! { cx,
|
||||
<CredentialsForm
|
||||
title="Please login to your account"
|
||||
action_label="Login"
|
||||
action=login_action
|
||||
error=login_error.into()
|
||||
disabled
|
||||
/>
|
||||
<p>"Don't have an account?"</p>
|
||||
<A href=Page::Register.path()>"Register"</A>
|
||||
<CredentialsForm
|
||||
title = "Please login to your account"
|
||||
action_label = "Login"
|
||||
action = login_action
|
||||
error = login_error.into()
|
||||
disabled
|
||||
/>
|
||||
<p>"Don't have an account?"</p>
|
||||
<A href=Page::Register.path()>"Register"</A>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use api_boundary::*;
|
||||
|
||||
use crate::{
|
||||
api::{self, UnauthorizedApi},
|
||||
components::credentials::*,
|
||||
Page,
|
||||
};
|
||||
use api_boundary::*;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn Register(cx: Scope, api: UnauthorizedApi) -> impl IntoView {
|
||||
@@ -50,24 +52,26 @@ pub fn Register(cx: Scope, api: UnauthorizedApi) -> impl IntoView {
|
||||
let disabled = Signal::derive(cx, move || wait_for_response.get());
|
||||
|
||||
view! { cx,
|
||||
<Show
|
||||
when=move || register_response.get().is_some()
|
||||
fallback=move |_| {
|
||||
view! { cx,
|
||||
<CredentialsForm
|
||||
title="Please enter the desired credentials"
|
||||
action_label="Register"
|
||||
action=register_action
|
||||
error=register_error.into()
|
||||
disabled
|
||||
/>
|
||||
<p>"Your already have an account?"</p>
|
||||
<A href=Page::Login.path()>"Login"</A>
|
||||
}
|
||||
}
|
||||
>
|
||||
<p>"You have successfully registered."</p>
|
||||
<p>"You can now " <A href=Page::Login.path()>"login"</A> " with your new account."</p>
|
||||
</Show>
|
||||
<Show
|
||||
when = move || register_response.get().is_some()
|
||||
fallback = move |_| view!{ cx,
|
||||
<CredentialsForm
|
||||
title = "Please enter the desired credentials"
|
||||
action_label = "Register"
|
||||
action = register_action
|
||||
error = register_error.into()
|
||||
disabled
|
||||
/>
|
||||
<p>"Your already have an account?"</p>
|
||||
<A href=Page::Login.path()>"Login"</A>
|
||||
}
|
||||
>
|
||||
<p>"You have successfully registered."</p>
|
||||
<p>
|
||||
"You can now "
|
||||
<A href=Page::Login.path()>"login"</A>
|
||||
" with your new account."
|
||||
</p>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
extend = { path = "../../cargo-make/common.toml" }
|
||||
@@ -2,7 +2,8 @@ use crate::{application::*, Error};
|
||||
use api_boundary as json;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Json, Response},
|
||||
response::Json,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use api_boundary as json;
|
||||
use std::{env, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
extract::{State, TypedHeader},
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
@@ -7,9 +8,10 @@ use axum::{
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use std::{env, sync::Arc};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
use api_boundary as json;
|
||||
|
||||
mod adapters;
|
||||
mod application;
|
||||
|
||||
@@ -23,10 +25,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
env::set_var("RUST_LOG", "debug");
|
||||
}
|
||||
env::VarError::NotUnicode(_) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"The value of 'RUST_LOG' does not contain valid unicode \
|
||||
data."
|
||||
));
|
||||
return Err(anyhow::anyhow!("The value of 'RUST_LOG' does not contain valid unicode data."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = { path = "../cargo-make/common.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -107,7 +107,7 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>
|
||||
{move || view! { cx, <ul>{contacts}</ul>}}
|
||||
</Suspense>
|
||||
<AnimatedOutlet
|
||||
<AnimatedOutlet
|
||||
class="outlet"
|
||||
outro="fadeOut"
|
||||
intro="fadeIn"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = { path = "../cargo-make/common.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
@@ -166,9 +168,7 @@ pub async fn login(
|
||||
.ok_or("User does not exist.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
|
||||
match verify(password, &user.password)
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
match verify(password, &user.password).map_err(|e| ServerFnError::ServerError(e.to_string()))? {
|
||||
true => {
|
||||
auth.login_user(user.id);
|
||||
auth.remember_user(remember.is_some());
|
||||
|
||||
@@ -13,9 +13,7 @@ impl TodoAppError {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
TodoAppError::NotFound => StatusCode::NOT_FOUND,
|
||||
TodoAppError::InternalServerError => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
TodoAppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions, Errors, view};
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
|
||||
use crate::errors::TodoAppError;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
@@ -22,7 +22,7 @@ if #[cfg(feature = "ssr")] {
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
res.into_response()
|
||||
} else{
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(TodoAppError::NotFound);
|
||||
|
||||
@@ -5,7 +5,7 @@ cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{Response, IntoResponse},
|
||||
routing::get,
|
||||
routing::{post, get},
|
||||
extract::{Path, Extension, RawQuery},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
@@ -16,7 +16,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use session_auth_axum::*;
|
||||
use session_auth_axum::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
|
||||
use leptos::{log, view, provide_context, LeptosOptions, get_configuration};
|
||||
use leptos::{log, view, provide_context, LeptosOptions, get_configuration, ServerFnError};
|
||||
use std::sync::Arc;
|
||||
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
|
||||
use axum_database_sessions::{SessionConfig, SessionLayer, SessionStore};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{auth::*, error_template::ErrorTemplate};
|
||||
use crate::auth::*;
|
||||
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
@@ -20,15 +21,15 @@ if #[cfg(feature = "ssr")] {
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
pub fn pool(cx: Scope) -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>(cx)
|
||||
Ok(use_context::<SqlitePool>(cx)
|
||||
.ok_or("Pool missing.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn auth(cx: Scope) -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>(cx)
|
||||
Ok(use_context::<AuthSession>(cx)
|
||||
.ok_or("Auth session missing.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn register_server_functions() {
|
||||
@@ -72,8 +73,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
let pool = pool(cx)?;
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
|
||||
let mut rows = sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
|
||||
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
@@ -111,13 +111,11 @@ pub async fn add_todo(cx: Scope, title: String) -> Result<(), ServerFnError> {
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||
|
||||
match sqlx::query(
|
||||
"INSERT INTO todos (title, user_id, completed) VALUES (?, ?, false)",
|
||||
)
|
||||
.bind(title)
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
match sqlx::query("INSERT INTO todos (title, user_id, completed) VALUES (?, ?, false)")
|
||||
.bind(title)
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(_row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
@@ -306,10 +304,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Login(
|
||||
cx: Scope,
|
||||
action: Action<Login, Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
pub fn Login(cx: Scope, action: Action<Login, Result<(), ServerFnError>>) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<ActionForm action=action>
|
||||
@@ -335,10 +330,7 @@ pub fn Login(
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Signup(
|
||||
cx: Scope,
|
||||
action: Action<Signup, Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
pub fn Signup(cx: Scope, action: Action<Signup, Result<(), ServerFnError>>) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<ActionForm action=action>
|
||||
@@ -370,10 +362,7 @@ pub fn Signup(
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Logout(
|
||||
cx: Scope,
|
||||
action: Action<Logout, Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
pub fn Logout(cx: Scope, action: Action<Logout, Result<(), ServerFnError>>) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<div id="loginbox">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -12,8 +12,8 @@ async fn main() -> std::io::Result<()> {
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> });
|
||||
|
||||
let _ = GetPost::register();
|
||||
let _ = ListPostMetadata::register();
|
||||
GetPost::register();
|
||||
ListPostMetadata::register();
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -11,8 +11,8 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::app::App;
|
||||
use leptos::{LeptosOptions, Errors, view};
|
||||
use crate::app::{App, AppProps};
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
@@ -20,7 +20,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{extract::Extension, routing::post, Router};
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
|
||||
@@ -13,8 +17,8 @@ async fn main() {
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
let _ = GetPost::register();
|
||||
let _ = ListPostMetadata::register();
|
||||
GetPost::register();
|
||||
ListPostMetadata::register();
|
||||
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -20,7 +20,7 @@ cfg_if! {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let addr = conf.leptos_options.site_addr.clone();
|
||||
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> });
|
||||
@@ -32,7 +32,7 @@ cfg_if! {
|
||||
App::new()
|
||||
.service(css)
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <App/> })
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", &site_root))
|
||||
.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -5,7 +5,7 @@ This is a template demonstrating how to integrate [TailwindCSS](https://tailwind
|
||||
|
||||
Install Tailwind and build the CSS:
|
||||
|
||||
`Trunk.toml` is configured to build the CSS automatically.
|
||||
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
|
||||
|
||||
Install trunk to client side render this bundle.
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[[hooks]]
|
||||
stage = "pre_build"
|
||||
command = "sh"
|
||||
command_arguments = ["-c", "npx tailwindcss -i input.css -o style/output.css"]
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com
|
||||
! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -31,7 +31,6 @@
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
6. Use the user's configured `sans` font-variation-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
@@ -48,8 +47,6 @@ html {
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
font-variation-settings: normal;
|
||||
/* 6 */
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -436,9 +433,6 @@ video {
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
@@ -486,9 +480,6 @@ video {
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
|
||||
@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6.2", optional = true }
|
||||
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
|
||||
actix-web = { version = "4.2.1", features = ["macros"] }
|
||||
anyhow = "1.0.68"
|
||||
broadcaster = "1.0.0"
|
||||
console_log = "1.0.0"
|
||||
@@ -19,7 +19,7 @@ cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_actix = { path = "../../integrations/actix" }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
@@ -36,10 +36,8 @@ default = ["ssr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_actix",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = { path = "../cargo-make/common.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
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 leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ cfg_if! {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let addr = conf.leptos_options.site_addr.clone();
|
||||
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> });
|
||||
@@ -43,7 +43,7 @@ cfg_if! {
|
||||
.service(css)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <TodoApp/> })
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", &site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(addr)?
|
||||
|
||||
@@ -9,7 +9,7 @@ cfg_if! {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn register_server_functions() {
|
||||
@@ -37,18 +37,18 @@ cfg_if! {
|
||||
#[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
|
||||
let req = use_context::<actix_web::HttpRequest>(cx);
|
||||
|
||||
if let Some(req) = req {
|
||||
println!("req.path = {:#?}", req.path());
|
||||
let req =
|
||||
use_context::<actix_web::HttpRequest>(cx);
|
||||
|
||||
if let Some(req) = req{
|
||||
println!("req.path = {:#?}", req.path());
|
||||
}
|
||||
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);
|
||||
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
@@ -73,7 +73,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(_row) => Ok(()),
|
||||
Ok(row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,9 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
cx,
|
||||
<Todos/>
|
||||
}/>
|
||||
<Api path="bananas" route=web::get().to(|req: HttpRequest| async move {
|
||||
req.path()
|
||||
})
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -130,7 +133,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
cx,
|
||||
<div>
|
||||
<MultiActionForm
|
||||
// we can handle client-side validation in the on:submit event
|
||||
// we can handle client-side validation in the on:submit event
|
||||
// leptos_router implements a `FromFormData` trait that lets you
|
||||
// parse deserializable types from form data and check them
|
||||
on:submit=move |ev| {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = { path = "../cargo-make/common.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -13,7 +13,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions, Errors, view};
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
|
||||
use crate::errors::TodoAppError;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
@@ -22,7 +22,7 @@ if #[cfg(feature = "ssr")] {
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
res.into_response()
|
||||
} else{
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(TodoAppError::NotFound);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
pub mod fallback;
|
||||
@@ -7,7 +8,6 @@ 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 leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::*;
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
routing::{post, get},
|
||||
extract::{Extension, Path},
|
||||
@@ -32,7 +32,7 @@ cfg_if! {
|
||||
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");
|
||||
let conn = db().await.expect("couldn't connect to DB");
|
||||
/* sqlx::migrate!()
|
||||
.run(&mut conn)
|
||||
.await
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = { path = "../cargo-make/common.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use crate::{
|
||||
error_template::ErrorTemplate,
|
||||
error_template::{ErrorTemplate, ErrorTemplateProps},
|
||||
errors::TodoAppError,
|
||||
};
|
||||
use http::Uri;
|
||||
@@ -22,7 +22,7 @@ if #[cfg(feature = "ssr")] {
|
||||
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 resp = get_static_file(uri, &root, headers, route_info).await?;
|
||||
let status = resp.status();
|
||||
|
||||
if status.is_success() || status.is_redirection() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
pub mod fallback;
|
||||
@@ -7,7 +8,6 @@ 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 leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
use leptos::*;
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::*;
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use crate::fallback::file_and_error_handler;
|
||||
use crate::todo::*;
|
||||
use leptos_viz::{generate_route_list, LeptosRoutes};
|
||||
@@ -35,7 +34,7 @@ cfg_if! {
|
||||
simple_logger::init_with_level(log::Level::Debug)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
let _conn = db().await.expect("couldn't connect to DB");
|
||||
let conn = db().await.expect("couldn't connect to DB");
|
||||
/* sqlx::migrate!()
|
||||
.run(&mut conn)
|
||||
.await
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extend = { path = "../cargo-make/common.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -136,7 +136,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
|
||||
|
||||
// Handle the three filter modes: All, Active, and Completed
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener_untyped("hashchange", move |_| {
|
||||
window_event_listener("hashchange", move |_| {
|
||||
let new_mode =
|
||||
location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
@@ -202,15 +202,15 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
});
|
||||
|
||||
// focus the main input on load
|
||||
// focus the main input on load
|
||||
create_effect(cx, move |_| {
|
||||
if let Some(input) = input_ref.get() {
|
||||
// We use request_animation_frame here because the NodeRef
|
||||
// is filled when the element is created, but before it's mounted
|
||||
// We use request_animation_frame here because the NodeRef
|
||||
// is filled when the element is created, but before it's mounted
|
||||
// to the DOM. Calling .focus() before it's mounted does nothing.
|
||||
// So inside, we wait a tick for the browser to mount it, then .focus()
|
||||
request_animation_frame(move || {
|
||||
let _ = input.focus();
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -348,14 +348,19 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
Active,
|
||||
Completed,
|
||||
#[default]
|
||||
All,
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Mode::All
|
||||
}
|
||||
}
|
||||
|
||||
pub fn route(hash: &str) -> Mode {
|
||||
match hash {
|
||||
"/active" => Mode::Active,
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
use crate::Todo;
|
||||
use leptos::{signal_prelude::*, Scope};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use leptos::{
|
||||
signal_prelude::*,
|
||||
Scope,
|
||||
};
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TodoSerialized {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub completed: bool,
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl TodoSerialized {
|
||||
pub fn into_todo(self, cx: Scope) -> Todo {
|
||||
Todo::new_with_completed(cx, self.id, self.title, self.completed)
|
||||
}
|
||||
pub fn into_todo(self, cx: Scope) -> Todo {
|
||||
Todo::new_with_completed(cx, self.id, self.title, self.completed)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Todo> for TodoSerialized {
|
||||
fn from(todo: &Todo) -> Self {
|
||||
Self {
|
||||
id: todo.id,
|
||||
title: todo.title.get(),
|
||||
completed: todo.completed.get(),
|
||||
}
|
||||
fn from(todo: &Todo) -> Self {
|
||||
Self {
|
||||
id: todo.id,
|
||||
title: todo.title.get(),
|
||||
completed: todo.completed.get(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ use actix_web::{
|
||||
use futures::{Stream, StreamExt};
|
||||
use http::StatusCode;
|
||||
use leptos::{
|
||||
leptos_dom::{Transparent, ssr::render_to_stream_with_prefix_undisposed_with_context},
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
server_fn::Encoding,
|
||||
ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement,
|
||||
*,
|
||||
};
|
||||
use leptos_integration_utils::{build_async_response, html_parts_separated};
|
||||
@@ -514,43 +514,6 @@ pub fn render_app_to_stream_with_context<IV>(
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
method: Method,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
options,
|
||||
additional_context,
|
||||
app_fn,
|
||||
method,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// This function allows you to provide additional information to Leptos for your route.
|
||||
/// It could be used to pass in Path Info, Connection Info, or anything your heart desires.
|
||||
///
|
||||
/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read
|
||||
/// from blocking resources should be retrojected into the HTML that's initially served, rather
|
||||
/// than dynamically inserting them with JavaScript on the client. This means you will have
|
||||
/// better support if JavaScript is not enabled, in exchange for a marginally slower response time.
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
|
||||
method: Method,
|
||||
replace_blocks: bool,
|
||||
) -> Route
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
@@ -570,14 +533,7 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
stream_app(
|
||||
&options,
|
||||
app,
|
||||
res_options,
|
||||
additional_context,
|
||||
replace_blocks,
|
||||
)
|
||||
.await
|
||||
stream_app(&options, app, res_options, additional_context).await
|
||||
}
|
||||
};
|
||||
match method {
|
||||
@@ -697,6 +653,103 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
/// rendering it, and includes any meta tags injected using [leptos_meta].
|
||||
///
|
||||
/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
|
||||
/// includes everything described in the documentation for that function.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use leptos_actix::DataResponse;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope, data: &'static str) -> impl IntoView {
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
/// let addr = conf.leptos_options.site_addr.clone();
|
||||
/// HttpServer::new(move || {
|
||||
/// let leptos_options = &conf.leptos_options;
|
||||
///
|
||||
/// App::new()
|
||||
/// // {tail:.*} passes the remainder of the URL as the route
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route(
|
||||
/// "/{tail:.*}",
|
||||
/// leptos_actix::render_preloaded_data_app(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |req| async move {
|
||||
/// Ok(DataResponse::Data(
|
||||
/// "async func that can preload data",
|
||||
/// ))
|
||||
/// },
|
||||
/// |cx, data| view! { cx, <MyApp data/> },
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
/// .bind(&addr)?
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[deprecated = "You can now use `render_app_async` with `create_resource` and \
|
||||
`<Suspense/>` to achieve async rendering without manually \
|
||||
preloading data."]
|
||||
pub fn render_preloaded_data_app<Data, Fut, IV>(
|
||||
options: LeptosOptions,
|
||||
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
|
||||
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
|
||||
) -> Route
|
||||
where
|
||||
Data: 'static,
|
||||
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
web::get().to(move |req: HttpRequest| {
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
let data_fn = data_fn.clone();
|
||||
let res_options = ResponseOptions::default();
|
||||
|
||||
async move {
|
||||
let data = match data_fn(req.clone()).await {
|
||||
Err(e) => return HttpResponse::from_error(e),
|
||||
Ok(DataResponse::Response(r)) => return r.into(),
|
||||
Ok(DataResponse::Data(d)) => d,
|
||||
};
|
||||
|
||||
let app = {
|
||||
let app_fn = app_fn.clone();
|
||||
let res_options = res_options.clone();
|
||||
move |cx| {
|
||||
provide_contexts(cx, &req, res_options);
|
||||
(app_fn)(cx, data).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
stream_app(&options, app, res_options, |_cx| {}).await
|
||||
}
|
||||
})
|
||||
}
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn provide_contexts(
|
||||
cx: leptos::Scope,
|
||||
@@ -728,14 +781,12 @@ async fn stream_app(
|
||||
app: impl FnOnce(leptos::Scope) -> View + 'static,
|
||||
res_options: ResponseOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
replace_blocks: bool,
|
||||
) -> HttpResponse<BoxBody> {
|
||||
let (stream, runtime, scope) =
|
||||
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| generate_head_metadata_separated(cx).1.into(),
|
||||
additional_context,
|
||||
replace_blocks
|
||||
);
|
||||
|
||||
build_stream_response(options, res_options, stream, runtime, scope).await
|
||||
@@ -871,7 +922,7 @@ pub fn generate_route_list_with_exclusions<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut routes = leptos_router::generate_route_list_inner(app_fn);
|
||||
let (mut routes, mut api_routes) = leptos_router::generate_route_list_inner(app_fn);
|
||||
|
||||
// Empty strings screw with Actix pathing, they need to be "/"
|
||||
routes = routes
|
||||
@@ -934,6 +985,22 @@ pub trait LeptosRoutes {
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
|
||||
#[deprecated = "You can now use `leptos_routes` and a `<Route \
|
||||
mode=SsrMode::Async/>`
|
||||
to achieve async rendering without manually preloading \
|
||||
data."]
|
||||
fn leptos_preloaded_data_routes<Data, Fut, IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<String>,
|
||||
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
|
||||
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
Data: 'static,
|
||||
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
|
||||
IV: IntoView + 'static;
|
||||
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
@@ -968,7 +1035,34 @@ where
|
||||
{
|
||||
self.leptos_routes_with_context(options, paths, |_| {}, app_fn)
|
||||
}
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn leptos_preloaded_data_routes<Data, Fut, IV>(
|
||||
self,
|
||||
options: LeptosOptions,
|
||||
paths: Vec<String>,
|
||||
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
|
||||
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
Data: 'static,
|
||||
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
|
||||
for path in paths.iter() {
|
||||
router = router.route(
|
||||
path,
|
||||
#[allow(deprecated)]
|
||||
render_preloaded_data_app(
|
||||
options.clone(),
|
||||
data_fn.clone(),
|
||||
app_fn.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
router
|
||||
}
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
@@ -997,15 +1091,6 @@ where
|
||||
method,
|
||||
)
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
true,
|
||||
)
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
@@ -1028,6 +1113,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines an API route, which mounts the given route handler at this path.
|
||||
#[component(transparent)]
|
||||
pub fn Api<P>(cx: leptos::Scope, path: P, route: actix_web::Route) -> impl IntoView
|
||||
where P: Into<String>
|
||||
{
|
||||
Transparent::new(ApiRouteListing::new(path.into(), route))
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions. This takes
|
||||
/// a handler function as its argument. The handler follows similar rules to an Actix
|
||||
/// [Handler](actix_web::Handler): it is an async function that receives arguments that
|
||||
|
||||
@@ -129,6 +129,23 @@ pub fn redirect(cx: leptos::Scope, path: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Decomposes an HTTP request into its parts, allowing you to read its headers
|
||||
/// and other data without consuming the body.
|
||||
#[deprecated(note = "Replaced with generate_request_and_parts() to allow for \
|
||||
putting LeptosRequest in the Context")]
|
||||
pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
// provide request headers as context in server scope
|
||||
let (parts, body) = req.into_parts();
|
||||
let body = body::to_bytes(body).await.unwrap_or_default();
|
||||
RequestParts {
|
||||
method: parts.method,
|
||||
uri: parts.uri,
|
||||
headers: parts.headers,
|
||||
version: parts.version,
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decomposes an HTTP request into its parts, allowing you to read its headers
|
||||
/// and other data without consuming the body. Creates a new Request from the
|
||||
/// original parts for further processing
|
||||
@@ -605,54 +622,6 @@ pub fn render_app_to_stream_with_context<IV>(
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
options,
|
||||
additional_context,
|
||||
app_fn,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure.
|
||||
///
|
||||
/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read
|
||||
/// from blocking resources should be retrojected into the HTML that's initially served, rather
|
||||
/// than dynamically inserting them with JavaScript on the client. This means you will have
|
||||
/// better support if JavaScript is not enabled, in exchange for a marginally slower response time.
|
||||
///
|
||||
/// Otherwise, this function is identical to [render_app_to_stream_with_context].
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
replace_blocks: bool,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<
|
||||
Box<
|
||||
dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
>,
|
||||
> + Clone
|
||||
+ Send
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
move |req: Request<Body>| {
|
||||
Box::pin({
|
||||
@@ -682,11 +651,10 @@ where
|
||||
}
|
||||
};
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
replace_blocks
|
||||
);
|
||||
|
||||
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
|
||||
@@ -695,7 +663,6 @@ where
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "info", fields(error), skip_all)]
|
||||
async fn generate_response(
|
||||
res_options: ResponseOptions,
|
||||
@@ -1101,9 +1068,9 @@ where
|
||||
let path = listing.path();
|
||||
if path.is_empty() {
|
||||
RouteListing::new(
|
||||
"/".to_string(),
|
||||
listing.mode(),
|
||||
listing.methods(),
|
||||
"/",
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
)
|
||||
} else {
|
||||
listing
|
||||
@@ -1206,21 +1173,6 @@ impl LeptosRoutes for axum::Router {
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s = render_app_to_stream_with_context_and_replace_blocks(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
true
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
|
||||
@@ -506,48 +506,6 @@ pub fn render_app_to_stream_with_context<IV>(
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
options,
|
||||
additional_context,
|
||||
app_fn,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// This version allows us to pass Viz State/Extractor or other infro from Viz or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure.
|
||||
///
|
||||
/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read
|
||||
/// from blocking resources should be retrojected into the HTML that's initially served, rather
|
||||
/// than dynamically inserting them with JavaScript on the client. This means you will have
|
||||
/// better support if JavaScript is not enabled, in exchange for a marginally slower response time.
|
||||
///
|
||||
/// Otherwise, this function is identical to [render_app_to_stream_with_context].
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
options: LeptosOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + Clone + Send + 'static,
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
replace_blocks: bool,
|
||||
) -> impl Fn(
|
||||
Request,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView,
|
||||
{
|
||||
move |req: Request| {
|
||||
Box::pin({
|
||||
@@ -590,11 +548,10 @@ where
|
||||
};
|
||||
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
replace_blocks
|
||||
);
|
||||
|
||||
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
|
||||
@@ -1035,9 +992,9 @@ where
|
||||
let path = listing.path();
|
||||
if path.is_empty() {
|
||||
RouteListing::new(
|
||||
"/".to_string(),
|
||||
listing.mode(),
|
||||
listing.methods(),
|
||||
"/",
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
)
|
||||
} else {
|
||||
listing
|
||||
@@ -1134,22 +1091,6 @@ impl LeptosRoutes for Router {
|
||||
leptos_router::Method::Patch => router.patch(path, s),
|
||||
}
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s =
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
options.clone(),
|
||||
additional_context.clone(),
|
||||
app_fn.clone(),
|
||||
true,
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => router.get(path, s),
|
||||
leptos_router::Method::Post => router.post(path, s),
|
||||
leptos_router::Method::Put => router.put(path, s),
|
||||
leptos_router::Method::Delete => router.delete(path, s),
|
||||
leptos_router::Method::Patch => router.patch(path, s),
|
||||
}
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
options.clone(),
|
||||
|
||||
@@ -152,6 +152,7 @@ pub use leptos_config::{self, get_configuration, LeptosOptions};
|
||||
pub mod ssr {
|
||||
pub use leptos_dom::{ssr::*, ssr_in_order::*};
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
pub use leptos_dom::{
|
||||
self, create_node_ref, debug_warn, document, error, ev,
|
||||
helpers::{
|
||||
@@ -160,10 +161,11 @@ pub use leptos_dom::{
|
||||
request_idle_callback, request_idle_callback_with_handle, set_interval,
|
||||
set_interval_with_handle, set_timeout, set_timeout_with_handle,
|
||||
window_event_listener, window_event_listener_untyped,
|
||||
window_event_listener_with_precast,
|
||||
},
|
||||
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
|
||||
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
|
||||
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
|
||||
IntoClass, IntoProperty, IntoView, NodeRef, Property, View,
|
||||
};
|
||||
pub use leptos_macro::*;
|
||||
pub use leptos_reactive::*;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_dom::{DynChild, Fragment, HydrationCtx, IntoView};
|
||||
use leptos_macro::component;
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
use leptos_reactive::ScopeDisposer;
|
||||
use leptos_reactive::{provide_context, Scope, SuspenseContext};
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// If any [Resources](leptos_reactive::Resource) are read in the `children` of this
|
||||
@@ -56,17 +52,16 @@ use std::rc::Rc;
|
||||
tracing::instrument(level = "info", skip_all)
|
||||
)]
|
||||
#[component(transparent)]
|
||||
pub fn Suspense<F, E, V>(
|
||||
pub fn Suspense<F, E>(
|
||||
cx: Scope,
|
||||
/// Returns a fallback UI that will be shown while `async` [Resources](leptos_reactive::Resource) are still loading.
|
||||
fallback: F,
|
||||
/// Children will be displayed once all `async` [Resources](leptos_reactive::Resource) have resolved.
|
||||
children: Box<dyn Fn(Scope) -> V>,
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> E + 'static,
|
||||
E: IntoView,
|
||||
V: IntoView + 'static,
|
||||
{
|
||||
let context = SuspenseContext::new(cx);
|
||||
|
||||
@@ -75,9 +70,8 @@ where
|
||||
|
||||
let orig_child = Rc::new(children);
|
||||
|
||||
let before_me = HydrationCtx::peek();
|
||||
let current_id = HydrationCtx::next_component();
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
let prev_disposer = Rc::new(RefCell::new(None::<ScopeDisposer>));
|
||||
|
||||
let child = DynChild::new({
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
@@ -85,18 +79,11 @@ where
|
||||
move || {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
if let Some(disposer) = prev_disposer.take() {
|
||||
disposer.dispose();
|
||||
}
|
||||
let (view, disposer) =
|
||||
cx.run_child_scope(|cx| if context.ready() {
|
||||
if context.ready() {
|
||||
Fragment::lazy(Box::new(|| vec![orig_child(cx).into_view(cx)])).into_view(cx)
|
||||
} else {
|
||||
Fragment::lazy(Box::new(|| vec![fallback().into_view(cx)])).into_view(cx)
|
||||
});
|
||||
*prev_disposer.borrow_mut() = Some(disposer);
|
||||
view
|
||||
|
||||
}
|
||||
} else {
|
||||
use leptos_reactive::signal_prelude::*;
|
||||
|
||||
@@ -161,7 +148,7 @@ where
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
HydrationCtx::continue_from(before_me);
|
||||
|
||||
leptos_dom::View::Suspense(current_id, core_component)
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ where
|
||||
F: Fn() -> E + 'static,
|
||||
E: IntoView,
|
||||
{
|
||||
let prev_children = Rc::new(RefCell::new(None::<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);
|
||||
@@ -112,13 +112,13 @@ where
|
||||
}
|
||||
})
|
||||
.children(Box::new(move |cx| {
|
||||
let frag = children(cx).into_view(cx);
|
||||
let frag = children(cx);
|
||||
|
||||
let suspense_context = use_context::<SuspenseContext>(cx)
|
||||
.expect("there to be a SuspenseContext");
|
||||
|
||||
if cfg!(feature = "hydrate") || !first_run.get() {
|
||||
*prev_children.borrow_mut() = Some(frag.clone());
|
||||
*prev_children.borrow_mut() = Some(frag.nodes.clone());
|
||||
}
|
||||
if is_first_run(&first_run, &suspense_context) {
|
||||
let has_local_only = suspense_context.has_local_only()
|
||||
|
||||
@@ -14,13 +14,16 @@ fn simple_ssr_test() {
|
||||
</div>
|
||||
};
|
||||
|
||||
assert!(rendered.into_view(cx).render_to_string(cx).contains(
|
||||
"<div id=\"_0-1\"><button id=\"_0-2\">-1</button><span \
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-8|open--><div \
|
||||
id=\"_0-1\"><button id=\"_0-2\">-1</button><span \
|
||||
id=\"_0-3\">Value: \
|
||||
<!--hk=_0-4o|leptos-dyn-child-start-->0<!\
|
||||
--hk=_0-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-5\">+1</button></div>"
|
||||
));
|
||||
id=\"_0-5\">+1</button></div><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-8|close-->"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,13 +54,28 @@ fn ssr_test_with_components() {
|
||||
</div>
|
||||
};
|
||||
|
||||
assert!(rendered.into_view(cx).render_to_string(cx).contains(
|
||||
"<div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-49|open--><div id=\"_0-1\" \
|
||||
class=\"counters\"><!--hk=_0-1-0o|leptos-counter-start--><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-38|open--><div \
|
||||
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
id=\"_0-1-3\">Value: \
|
||||
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
|
||||
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5\">+1</button></div>"
|
||||
));
|
||||
id=\"_0-1-5\">+1</button></div><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-38|close--><!--hk=_0-1-0c|leptos-counter-end--><!\
|
||||
--hk=_0-1-5-0o|leptos-counter-start--><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-38|open--><div \
|
||||
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
|
||||
id=\"_0-1-5-3\">Value: \
|
||||
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
|
||||
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5-5\">+1</button></div><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-38|close--><!\
|
||||
--hk=_0-1-5-0c|leptos-counter-end--></div><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-49|close-->"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -88,13 +106,29 @@ fn ssr_test_with_snake_case_components() {
|
||||
</div>
|
||||
};
|
||||
|
||||
assert!(rendered.into_view(cx).render_to_string(cx).contains(
|
||||
"<div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-101|open--><div id=\"_0-1\" \
|
||||
class=\"counters\"><!\
|
||||
--hk=_0-1-0o|leptos-snake-case-counter-start--><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-90|open--><div \
|
||||
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
id=\"_0-1-3\">Value: \
|
||||
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
|
||||
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5\">+1</button></div>"
|
||||
));
|
||||
id=\"_0-1-5\">+1</button></div><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-90|close--><!--hk=_0-1-0c|leptos-snake-case-counter-end--><!\
|
||||
--hk=_0-1-5-0o|leptos-snake-case-counter-start--><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-90|open--><div \
|
||||
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
|
||||
id=\"_0-1-5-3\">Value: \
|
||||
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
|
||||
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5-5\">+1</button></div><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-90|close--><!\
|
||||
--hk=_0-1-5-0c|leptos-snake-case-counter-end--></div><!\
|
||||
--leptos-view|leptos-tests-ssr.rs-101|close-->"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,10 +144,12 @@ fn test_classes() {
|
||||
<div class="my big" class:a={move || value.get() > 10} class:red=true class:car={move || value.get() > 1}></div>
|
||||
};
|
||||
|
||||
assert!(rendered
|
||||
.into_view(cx)
|
||||
.render_to_string(cx)
|
||||
.contains("<div id=\"_0-1\" class=\"my big red car\"></div>"));
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-142|open--><div id=\"_0-1\" \
|
||||
class=\"my big red \
|
||||
car\"></div><!--leptos-view|leptos-tests-ssr.rs-142|close-->"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,10 +168,13 @@ fn ssr_with_styles() {
|
||||
</div>
|
||||
};
|
||||
|
||||
assert!(rendered.into_view(cx).render_to_string(cx).contains(
|
||||
"<div id=\"_0-1\" class=\" myclass\"><button id=\"_0-2\" \
|
||||
class=\"btn myclass\">-1</button></div>"
|
||||
));
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-164|open--><div id=\"_0-1\" \
|
||||
class=\" myclass\"><button id=\"_0-2\" class=\"btn \
|
||||
myclass\">-1</button></div><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-164|close-->"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,9 +190,11 @@ fn ssr_option() {
|
||||
<option/>
|
||||
};
|
||||
|
||||
assert!(rendered
|
||||
.into_view(cx)
|
||||
.render_to_string(cx)
|
||||
.contains("<option id=\"_0-1\"></option>"));
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<!--leptos-view|leptos-tests-ssr.rs-188|open--><option \
|
||||
id=\"_0-1\"></option><!--leptos-view|leptos-tests-ssr.\
|
||||
rs-188|close-->"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ features = [
|
||||
"Comment",
|
||||
"Document",
|
||||
"DomTokenList",
|
||||
"CssStyleDeclaration",
|
||||
"Location",
|
||||
"Range",
|
||||
"Text",
|
||||
|
||||
@@ -332,8 +332,32 @@ impl IntervalHandle {
|
||||
any(debug_assertions, features = "ssr"),
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
|
||||
_ = set_interval_with_handle(cb, duration);
|
||||
#[deprecated = "use set_interval_with_handle() instead. In the future, \
|
||||
set_interval() will no longer return a handle, for consistency \
|
||||
with other timer helper functions."]
|
||||
pub fn set_interval(
|
||||
cb: impl Fn() + 'static,
|
||||
duration: Duration,
|
||||
) -> Result<IntervalHandle, JsValue> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
leptos_reactive::SpecialNonReactiveZone::exit();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
let handle = window()
|
||||
.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
)?;
|
||||
Ok(IntervalHandle(handle))
|
||||
}
|
||||
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls,
|
||||
@@ -378,6 +402,24 @@ pub fn set_interval_with_handle(
|
||||
si(Box::new(cb), duration)
|
||||
}
|
||||
|
||||
/// Adds an event listener to the `Window`.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, features = "ssr"),
|
||||
instrument(level = "trace", skip_all, fields(event_name = %event_name))
|
||||
)]
|
||||
#[inline(always)]
|
||||
#[deprecated = "In the next release, `window_event_listener` will become \
|
||||
typed. You can switch now to `window_event_listener_untyped` \
|
||||
for the current behavior or use \
|
||||
`window_event_listener_with_precast`, which will become the \
|
||||
new`window_event_listener`."]
|
||||
pub fn window_event_listener(
|
||||
event_name: &str,
|
||||
cb: impl Fn(web_sys::Event) + 'static,
|
||||
) {
|
||||
window_event_listener_untyped(event_name, cb)
|
||||
}
|
||||
|
||||
/// Adds an event listener to the `Window`, typed as a generic `Event`.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
@@ -414,21 +456,8 @@ pub fn window_event_listener_untyped(
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a window event listener from a typed event.
|
||||
/// ```
|
||||
/// use leptos::{leptos_dom::helpers::window_event_listener, *};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn App(cx: Scope) -> impl IntoView {
|
||||
/// window_event_listener(ev::keypress, |ev| {
|
||||
/// // ev is typed as KeyboardEvent automatically,
|
||||
/// // so .code() can be called
|
||||
/// let code = ev.code();
|
||||
/// log!("code = {code:?}");
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn window_event_listener<E: ev::EventDescriptor + 'static>(
|
||||
/// Creates a window event listener where the event in the callback is already appropriately cast.
|
||||
pub fn window_event_listener_with_precast<E: ev::EventDescriptor + 'static>(
|
||||
event: E,
|
||||
cb: impl Fn(E::EventType) + 'static,
|
||||
) where
|
||||
|
||||
@@ -63,7 +63,7 @@ cfg_if! {
|
||||
use crate::{
|
||||
ev::EventDescriptor,
|
||||
hydration::HydrationCtx,
|
||||
macro_helpers::{IntoAttribute, IntoClass, IntoProperty, IntoStyle},
|
||||
macro_helpers::{IntoAttribute, IntoClass, IntoProperty},
|
||||
Element, Fragment, IntoView, NodeRef, Text, View,
|
||||
};
|
||||
use leptos_reactive::Scope;
|
||||
@@ -382,6 +382,26 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
#[deprecated = "Use HtmlElement::from_chunks() instead."]
|
||||
pub fn from_html(
|
||||
cx: Scope,
|
||||
element: El,
|
||||
html: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cx,
|
||||
attrs: smallvec![],
|
||||
children: ElementChildren::Chunks(vec![StringOrView::String(
|
||||
html.into(),
|
||||
)]),
|
||||
element,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub fn from_chunks(
|
||||
@@ -804,69 +824,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a style on an element.
|
||||
///
|
||||
/// **Note**: In the builder syntax, this will be overwritten by the `style`
|
||||
/// attribute if you use `.attr("class", /* */)`. In the `view` macro, they
|
||||
/// are automatically re-ordered so that this over-writing does not happen.
|
||||
#[track_caller]
|
||||
pub fn style(
|
||||
self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
style: impl IntoStyle,
|
||||
) -> Self {
|
||||
let name = name.into();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
let el = self.element.as_ref();
|
||||
let value = style.into_style(self.cx);
|
||||
style_helper(el, name, value);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
use crate::macro_helpers::Style;
|
||||
|
||||
let mut this = self;
|
||||
|
||||
let style = style.into_style(this.cx);
|
||||
|
||||
let include = match style {
|
||||
Style::Value(value) => Some(value),
|
||||
Style::Option(value) => value,
|
||||
Style::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Style::Fn(_, f) = value {
|
||||
value = f();
|
||||
}
|
||||
match value {
|
||||
Style::Value(value) => Some(value),
|
||||
Style::Option(value) => value,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(style_value) = include {
|
||||
if let Some((_, ref mut value)) =
|
||||
this.attrs.iter_mut().find(|(name, _)| name == "style")
|
||||
{
|
||||
*value = format!("{value} {name}: {style_value};").into();
|
||||
} else {
|
||||
this.attrs.push((
|
||||
"style".into(),
|
||||
format!("{name}: {style_value};").into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a property on an element.
|
||||
#[track_caller]
|
||||
pub fn prop(
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
use leptos_reactive::Scope;
|
||||
use std::{borrow::Cow, rc::Rc};
|
||||
|
||||
/// todo docs
|
||||
#[derive(Clone)]
|
||||
pub enum Style {
|
||||
/// A plain string value.
|
||||
Value(Cow<'static, str>),
|
||||
/// An optional string value, which sets the property to the value if `Some` and removes the property if `None`.
|
||||
Option(Option<Cow<'static, str>>),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to update the style.
|
||||
Fn(Scope, Rc<dyn Fn() -> Style>),
|
||||
}
|
||||
|
||||
impl PartialEq for Style {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Value(l0), Self::Value(r0)) => l0 == r0,
|
||||
(Self::Fn(_, _), Self::Fn(_, _)) => false,
|
||||
(Self::Option(l0), Self::Option(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Style {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Value(arg0) => f.debug_tuple("Value").field(arg0).finish(),
|
||||
Self::Fn(_, _) => f.debug_tuple("Fn").finish(),
|
||||
Self::Option(arg0) => f.debug_tuple("Option").field(arg0).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts some type into a [Style].
|
||||
pub trait IntoStyle {
|
||||
/// Converts the object into a [Style].
|
||||
fn into_style(self, cx: Scope) -> Style;
|
||||
}
|
||||
|
||||
impl IntoStyle for &'static str {
|
||||
#[inline(always)]
|
||||
fn into_style(self, _cx: Scope) -> Style {
|
||||
Style::Value(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for String {
|
||||
#[inline(always)]
|
||||
fn into_style(self, _cx: Scope) -> Style {
|
||||
Style::Value(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<&'static str> {
|
||||
#[inline(always)]
|
||||
fn into_style(self, _cx: Scope) -> Style {
|
||||
Style::Option(self.map(Cow::Borrowed))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<String> {
|
||||
#[inline(always)]
|
||||
fn into_style(self, _cx: Scope) -> Style {
|
||||
Style::Option(self.map(Cow::Owned))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> IntoStyle for T
|
||||
where
|
||||
T: Fn() -> U + 'static,
|
||||
U: IntoStyle,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn into_style(self, cx: Scope) -> Style {
|
||||
let modified_fn = Rc::new(move || (self)().into_style(cx));
|
||||
Style::Fn(cx, modified_fn)
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Converts the style to its HTML value at that moment so it can be rendered on the server.
|
||||
pub fn as_value_string(
|
||||
&self,
|
||||
style_name: &'static str,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
match self {
|
||||
Style::Value(value) => {
|
||||
Some(format!("{style_name}: {value};").into())
|
||||
}
|
||||
Style::Option(value) => value
|
||||
.as_ref()
|
||||
.map(|value| format!("{style_name}: {value};").into()),
|
||||
Style::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Style::Fn(_, f) = value {
|
||||
value = f();
|
||||
}
|
||||
value.as_value_string(style_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoStyle> IntoStyle for (Scope, T) {
|
||||
#[inline(always)]
|
||||
fn into_style(self, _: Scope) -> Style {
|
||||
self.1.into_style(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[doc(hidden)]
|
||||
#[inline(never)]
|
||||
pub fn style_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Style,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
let el = el.unchecked_ref::<web_sys::HtmlElement>();
|
||||
let style_list = el.style();
|
||||
match value {
|
||||
Style::Fn(cx, f) => {
|
||||
create_render_effect(cx, move |old| {
|
||||
let mut new = f();
|
||||
while let Style::Fn(_, f) = new {
|
||||
new = f();
|
||||
}
|
||||
let new = match new {
|
||||
Style::Value(value) => Some(value),
|
||||
Style::Option(value) => value,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if old.as_ref() != Some(&new) {
|
||||
style_expression(&style_list, &name, new.as_ref(), true)
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Style::Value(value) => {
|
||||
style_expression(&style_list, &name, Some(&value), false)
|
||||
}
|
||||
Style::Option(value) => {
|
||||
style_expression(&style_list, &name, value.as_ref(), false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(never)]
|
||||
pub(crate) fn style_expression(
|
||||
style_list: &web_sys::CssStyleDeclaration,
|
||||
style_name: &str,
|
||||
value: Option<&Cow<'static, str>>,
|
||||
force: bool,
|
||||
) {
|
||||
use crate::HydrationCtx;
|
||||
|
||||
if force || !HydrationCtx::is_hydrating() {
|
||||
let style_name = wasm_bindgen::intern(style_name);
|
||||
|
||||
if let Some(value) = value {
|
||||
if let Err(e) = style_list.set_property(style_name, &value) {
|
||||
crate::error!("[HtmlElement::style()] {e:?}");
|
||||
}
|
||||
} else {
|
||||
if let Err(e) = style_list.remove_property(style_name) {
|
||||
crate::error!("[HtmlElement::style()] {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! style_type {
|
||||
($style_type:ty) => {
|
||||
impl IntoStyle for $style_type {
|
||||
fn into_style(self, _: Scope) -> Style {
|
||||
Style::Value(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<$style_type> {
|
||||
fn into_style(self, _: Scope) -> Style {
|
||||
Style::Option(self.map(|n| n.to_string().into()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
style_type!(&String);
|
||||
style_type!(usize);
|
||||
style_type!(u8);
|
||||
style_type!(u16);
|
||||
style_type!(u32);
|
||||
style_type!(u64);
|
||||
style_type!(u128);
|
||||
style_type!(isize);
|
||||
style_type!(i8);
|
||||
style_type!(i16);
|
||||
style_type!(i32);
|
||||
style_type!(i64);
|
||||
style_type!(i128);
|
||||
style_type!(f32);
|
||||
style_type!(f64);
|
||||
style_type!(char);
|
||||
@@ -1,8 +1,6 @@
|
||||
mod into_attribute;
|
||||
mod into_class;
|
||||
mod into_property;
|
||||
mod into_style;
|
||||
pub use into_attribute::*;
|
||||
pub use into_class::*;
|
||||
pub use into_property::*;
|
||||
pub use into_style::*;
|
||||
|
||||
@@ -79,6 +79,12 @@ pub fn create_node_ref<T: ElementDescriptor + 'static>(
|
||||
}
|
||||
|
||||
impl<T: ElementDescriptor + 'static> NodeRef<T> {
|
||||
/// Creates an empty reference.
|
||||
#[deprecated = "Use `create_node_ref` instead of `NodeRef::new()`."]
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
Self(create_rw_signal(cx, None))
|
||||
}
|
||||
|
||||
/// Gets the element that is currently stored in the reference.
|
||||
///
|
||||
/// This tracks reactively, so that node references can be used in effects.
|
||||
|
||||
@@ -146,44 +146,6 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
additional_context: impl FnOnce(Scope) + 'static,
|
||||
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
|
||||
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
view,
|
||||
prefix,
|
||||
additional_context,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings and returns the [Scope] and [RuntimeId] that were created, so
|
||||
/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with
|
||||
/// the same scope. This can be used to generate additional HTML that has access to the same `Scope`.
|
||||
///
|
||||
/// If `replace_blocks` is true, this will wait for any fragments with blocking resources and
|
||||
/// actually replace them in the initial HTML. This is slower to render (as it requires walking
|
||||
/// back over the HTML for string replacement) but has the advantage of never including those fallbacks
|
||||
/// in the HTML.
|
||||
///
|
||||
/// This renders:
|
||||
/// 1) the prefix
|
||||
/// 2) the application shell
|
||||
/// a) HTML for everything that is not under a `<Suspense/>`,
|
||||
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
|
||||
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
|
||||
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
|
||||
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
|
||||
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
|
||||
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", skip_all,)
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
additional_context: impl FnOnce(Scope) + 'static,
|
||||
replace_blocks: bool,
|
||||
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
|
||||
HydrationCtx::reset_id();
|
||||
|
||||
@@ -215,7 +177,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
|
||||
});
|
||||
let cx = Scope { runtime, id: scope };
|
||||
|
||||
let mut blocking_fragments = FuturesUnordered::new();
|
||||
let blocking_fragments = FuturesUnordered::new();
|
||||
let fragments = FuturesUnordered::new();
|
||||
|
||||
for (fragment_id, data) in pending_fragments {
|
||||
@@ -236,46 +198,24 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
|
||||
|
||||
// HTML for the view function and script to store resources
|
||||
let stream = futures::stream::once(async move {
|
||||
let resolvers = format!(
|
||||
"<script>__LEPTOS_PENDING_RESOURCES = \
|
||||
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
|
||||
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
|
||||
);
|
||||
|
||||
if replace_blocks {
|
||||
let mut blocks = Vec::with_capacity(blocking_fragments.len());
|
||||
while let Some((blocked_id, blocked_fragment)) =
|
||||
blocking_fragments.next().await
|
||||
{
|
||||
blocks.push((blocked_id, blocked_fragment));
|
||||
}
|
||||
|
||||
let prefix = prefix(cx);
|
||||
|
||||
let mut shell = shell;
|
||||
|
||||
for (blocked_id, blocked_fragment) in blocks {
|
||||
let open = format!("<!--suspense-open-{blocked_id}-->");
|
||||
let close = format!("<!--suspense-close-{blocked_id}-->");
|
||||
let (first, rest) = shell.split_once(&open).unwrap_or_default();
|
||||
let (_fallback, rest) =
|
||||
rest.split_once(&close).unwrap_or_default();
|
||||
|
||||
shell = format!("{first}{blocked_fragment}{rest}").into();
|
||||
}
|
||||
|
||||
format!("{prefix}{shell}{resolvers}")
|
||||
} else {
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments =
|
||||
fragments_to_chunks(blocking_fragments);
|
||||
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
}
|
||||
let prefix = prefix(cx);
|
||||
format!("{prefix}{shell}{resolvers}{blocking}")
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments = fragments_to_chunks(blocking_fragments);
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
}
|
||||
let prefix = prefix(cx);
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
{shell}
|
||||
<script>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
{blocking}
|
||||
"#
|
||||
)
|
||||
})
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
@@ -289,7 +229,6 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
|
||||
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
|
||||
@@ -201,29 +201,7 @@ mod template;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 8. Individual styles can also be set with `style:` or `style=("property-name", value)` syntax.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let (x, set_x) = create_signal(cx, 0);
|
||||
/// let (y, set_y) = create_signal(cx, 0);
|
||||
/// view! { cx,
|
||||
/// <div
|
||||
/// style="position: absolute"
|
||||
/// style:left=move || format!("{}px", x())
|
||||
/// style:top=move || format!("{}px", y())
|
||||
/// style=("background-color", move || format!("rgb({}, {}, 100)", x(), y()))
|
||||
/// >
|
||||
/// "Moves when coordinates change"
|
||||
/// </div>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 9. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
|
||||
/// 8. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
|
||||
/// [NodeRef](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to use later.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
@@ -240,7 +218,7 @@ mod template;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 10. You can add the same class to every element in the view by passing in a special
|
||||
/// 9. You can add the same class to every element in the view by passing in a special
|
||||
/// `class = {/* ... */},` argument after `cx, `. This is useful for injecting a class
|
||||
/// provided by a scoped styling library.
|
||||
/// ```rust
|
||||
@@ -258,7 +236,7 @@ mod template;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 11. You can set any HTML element’s `innerHTML` with the `inner_html` attribute on an
|
||||
/// 10. You can set any HTML element’s `innerHTML` with the `inner_html` attribute on an
|
||||
/// element. Be careful: this HTML will not be escaped, so you should ensure that it
|
||||
/// only contains trusted input.
|
||||
/// ```rust
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user