Compare commits

..

29 Commits

Author SHA1 Message Date
Greg Johnston
1657107834 missed some 2023-05-18 08:34:51 -04:00
Greg Johnston
a241e5edce tests: fix broken SSR doctests 2023-05-18 08:29:41 -04:00
Greg Johnston
85ad7b0f38 fix: <Suspense/> hydration when no resources are read under it (#1046) 2023-05-16 12:20:23 -04:00
Greg Johnston
f0a9940364 fix: leak in todomvc example (closes #706) 2023-05-15 14:53:39 -04:00
Mark Catley
b472aaf6a0 fix: typo in actix extract documentation (#1043) 2023-05-15 08:57:49 -04:00
Greg Johnston
059c1bf61c cargo fmt 2023-05-14 06:55:05 -04:00
Matt Crane
add13fd6a4 change: migrate Axum integration to use with_state over layer(Extension) (#1032) 2023-05-14 06:37:39 -04:00
Greg Johnston
904c2e8a67 v0.3.0 2023-05-13 19:44:06 -04:00
Greg Johnston
a5c3be586a docs: tweak new slice docs 2023-05-13 19:43:17 -04:00
Markus Kohlhase
9f5139d929 examples: fix trunk config to run tailwind at the right time (#1040) 2023-05-13 19:39:36 -04:00
sjud
bae305340e change: update create_slice to allow different types on getter and setter (#1036) 2023-05-13 19:39:17 -04:00
Greg Johnston
40c1556f29 change: remove APIs that had been marked deprecated (#1037) 2023-05-12 19:45:48 -04:00
Greg Johnston
0db4f5821f fix: avoid extra { escaping (closes #1035) (#1038) 2023-05-12 16:29:33 -04:00
Greg Johnston
12ebc95800 fix: flickering <Transition/> in release mode (closes #960) (#1030) 2023-05-11 14:51:33 -04:00
Greg Johnston
d7b919032e feat: SsrMode::PartiallyBlocked (#1026) 2023-05-10 13:30:01 -04:00
Greg Johnston
be8bf8b0d6 fix: corrects error-deserialization behavior of ActionForm (closes #1024) (#1025) 2023-05-09 06:40:22 -04:00
Greg Johnston
f84f1422f4 fix: maintain insertion order of meta tags (#1021) 2023-05-08 08:36:54 -04:00
Snêu
b01976e3bb examples: fix indentations (#1017) 2023-05-08 08:36:45 -04:00
agilarity
50b48fb272 chore: build CSS with trunk (#1016)
This configures a hook to run the tailwindcss CLI when a build is triggered or retriggered via Trunk watch. It eliminates the need to run the tailwindcss manually.
2023-05-08 08:36:07 -04:00
agilarity
1617e31d69 CI: clean up examples after verification (#1019)
* build: improve task names

* build: add clean-examples task

Make it easy to clean all the cargo and trunk files in the examples.

* build: clean after verify
2023-05-08 08:35:27 -04:00
Chris
51cd082d4c docs: add examples for manual server integration for router (#1015) 2023-05-08 08:34:43 -04:00
Warre Dujardin
72414b7945 docs: fix link to cargo-leptos README in the book (#1012) 2023-05-06 13:42:44 -04:00
FrankReh
1afa14ccbd docs: adjust Dynamic Attributes page (#1011)
Adjust the intro in the Dynamic Attributes page to include
the recent `dynamic style` feature. Also reorder a little given
the order of the page body that follows.
2023-05-06 13:42:16 -04:00
Warre Dujardin
477c29cdf1 docs: close iframe tag (#1013) 2023-05-06 13:41:50 -04:00
Greg Johnston
49a424314a docs: add a serevr fn section (#1014) 2023-05-06 13:14:16 -04:00
Warre Dujardin
598523cd9d fix: relax Debug trait bounds (#1010) 2023-05-06 12:10:48 -04:00
Greg Johnston
1fdb6f1cdf feat: add style: to view (#1009) 2023-05-06 06:23:20 -04:00
agilarity
9997487a9c test: lint examples with --all-features (#1008)
* test: lint all features

* fix(counter_isomorphic): check-style issues

* fix(errors_axum): check-style issues

* fix(hackernews): check-style issues

* fix(hackernews_axum): check-style issues

* fix(session_auth_axum): check-style issues

* build(session_auth_axum): add common tasks

* fix(ssr_modes): check-style issues

* build(ssr_modes_axum): add common tasks

* fix(ssr_modes_axum): check-style issues

* build(tailwind): add common tasks

* fix(tailwind): check-style issues

* fix(todo_app_sqlite_axum): check-style issues

* fix(todo_app_sqlite_viz): check-style issues
2023-05-05 22:25:29 -04:00
Greg Johnston
b5e94e4054 fix: properly dispose of <Suspense/> scopes (closes #834) (#1006) 2023-05-05 19:08:56 -04:00
87 changed files with 1415 additions and 815 deletions

View File

@@ -25,22 +25,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.3.0-alpha"
version = "0.3.0"
[workspace.dependencies]
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" }
leptos = { path = "./leptos", default-features = false, version = "0.3.0" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0" }
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0" }
leptos_router = { path = "./router", version = "0.3.0" }
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0" }
[profile.release]
codegen-units = 1

View File

@@ -69,7 +69,11 @@ 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"
@@ -102,9 +106,15 @@ 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"

View File

@@ -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 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>

View File

@@ -34,9 +34,10 @@
- [`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 Footguns](./ssr/24_hydration_bugs.md)
- [Server Functions]()
- [Request/Response]()
- [Hydration Bugs](./ssr/24_hydration_bugs.md)
- [Working with the Server](./server/README.md)
- [Server Functions](./server/25_server_functions.md)
- [Request/Response]()
- [Extractors]()
- [Axum]()
- [Actix]()
@@ -46,6 +47,6 @@
- [Actions]()
- [Forms]()
- [`<ActionForm/>`s]()
- [Turning off WebAssembly]()
- [Turning off WebAssembly: Progressive Enhancement and Graceful Degradation]()
- [Advanced Reactivity]()
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)

View File

@@ -2,8 +2,8 @@
So far weve 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.
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 theres much more to it than that.
Asynchronous data is notoriously hard to integrate with the synchronous parts of your code.
In this chapter, well see how Leptos helps smooth out that process for you.

View File

@@ -0,0 +1,125 @@
# Server Functions
If youre creating anything beyond a toy app, youll 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 dont 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 thats stored on the server and definitely shouldnt be shipped down to a users 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 users dark/light mode preference across sessions, and be applied during server rendering so theres 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, youll 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 functions 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? Its 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();
// ...
}
```
Youll 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 doesnt have access to client state unless you send it explicitly. (Otherwise wed 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 dont 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 its 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 well 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).)
- Youll 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 well 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 its very important to remember. **Server functions are not magic; theyre syntax sugar for defining a public API.** The _body_ of a server function is never made public; its just part of your server binary. But the server function is a publicly accessible API endpoint, and its 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 Ive 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, well 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.

View File

@@ -0,0 +1,11 @@
# 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 _cant,_ like reading from and writing to a SQL database.
If youre used to building JavaScript frontend apps, youre probably used to calling out to some kind of REST API to do this sort of server work. If youre 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 its 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.

View File

@@ -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/leptos/blob/main/examples/hackernews/src/api.rs).
`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).
But what exactly is happening when you open our browser to `localhost:3000`? Well, read on to find out.

View File

@@ -1,10 +1,10 @@
# `view`: Dynamic Attributes and Classes
# `view`: Dynamic Classes, Styles and Attributes
So far weve 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, well look at how to update attributes and classes dynamically,
In this section, well look at how to update classes, styles and attributes dynamically,
and well introduce the concept of a **derived signal**.
Lets start with a simple component that should be familiar: click a button to
@@ -52,12 +52,42 @@ 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 cant 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 youre 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

View File

@@ -31,7 +31,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
[tasks.verify-flow]
description = "Provides pre and post hooks for verify"
dependencies = ["pre-verify-flow", "verify", "post-verify-flow"]
dependencies = ["pre-verify", "verify", "post-verify"]
[tasks.verify]
description = "Run all quality checks and tests"
@@ -41,16 +41,17 @@ dependencies = ["check-style", "test-unit-and-web"]
description = "Run all unit and web tests"
dependencies = ["test-flow", "web-test-flow"]
[tasks.pre-verify-flow]
[tasks.pre-verify]
[tasks.post-verify-flow]
[tasks.post-verify]
dependencies = ["clean-all"]
[tasks.web-test-flow]
description = "Provides pre and post hooks for web-test"
dependencies = ["pre-web-test-flow", "web-test", "post-web-test-flow"]
dependencies = ["pre-web-test", "web-test", "post-web-test"]
[tasks.pre-web-test-flow]
[tasks.pre-web-test]
[tasks.web-test]
[tasks.post-web-test-flow]
[tasks.post-web-test]

View File

@@ -1,5 +1,5 @@
[env]
CARGO_MAKE_CLIPPY_ARGS = "--all-targets -- -D warnings"
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
[tasks.check-style]
description = "Check for style violations"
@@ -12,3 +12,12 @@ 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"]

View File

@@ -41,7 +41,7 @@ ssr = [
"leptos_meta/ssr",
"leptos_router/ssr",
]
nightly = ["leptos/nightly", "leptos_router/nightly"]
stable = ["leptos/stable", "leptos_router/stable"]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix", "stable"]

View File

@@ -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.clone();
let addr = conf.leptos_options.site_addr;
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)?

View File

@@ -10,12 +10,12 @@ crate-type = ["cdylib", "rlib"]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4.17"
serde = { version = "1", features = ["derive"] }
simple_logger = "4.0.0"

View File

@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -11,10 +11,10 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::landing::{App, AppProps};
use leptos::{LeptosOptions, view};
use crate::landing::App;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();

View File

@@ -5,7 +5,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use crate::landing::*;
use axum::body::Body as AxumBody;
use axum::{
extract::{Extension, Path},
extract::{State, Path},
http::Request,
response::{IntoResponse, Response},
routing::{get, post},
@@ -21,7 +21,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
Extension(options): Extension<Arc<LeptosOptions>>,
State(options): State<Arc<LeptosOptions>>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_app_to_stream_with_context(
@@ -44,7 +44,7 @@ async fn main() {
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let leptos_options = Arc::new(conf.leptos_options);
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
@@ -58,7 +58,7 @@ async fn main() {
|cx| view! { cx, <App/> },
)
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -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.clone();
let addr = conf.leptos_options.site_addr;
// 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)?

View File

@@ -1,4 +1,4 @@
use leptos::{on_cleanup, Scope, Serializable};
use leptos::{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
on_cleanup(cx, move || {
leptos::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,
{

View File

@@ -4,7 +4,7 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -15,13 +15,13 @@ if #[cfg(feature = "ssr")] {
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
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()

View File

@@ -7,7 +7,6 @@ if #[cfg(feature = "ssr")] {
use axum::{
Router,
routing::get,
extract::Extension,
};
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
@@ -18,8 +17,8 @@ if #[cfg(feature = "ssr")] {
use hackernews_axum::*;
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr.clone();
let leptos_options = Arc::new(conf.leptos_options);
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
@@ -29,7 +28,7 @@ if #[cfg(feature = "ssr")] {
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -4,7 +4,7 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -13,16 +13,16 @@ if #[cfg(feature = "ssr")] {
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::error_template::ErrorTemplate;
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
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);

View File

@@ -5,8 +5,8 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
response::{Response, IntoResponse},
routing::{post, get},
extract::{Path, Extension, RawQuery},
routing::get,
extract::{Path, State, Extension, RawQuery},
http::{Request, header::HeaderMap},
body::Body as AxumBody,
Router,
@@ -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, ServerFnError};
use leptos::{log, view, provide_context, LeptosOptions, get_configuration};
use std::sync::Arc;
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
use axum_database_sessions::{SessionConfig, SessionLayer, SessionStore};
@@ -33,7 +33,7 @@ if #[cfg(feature = "ssr")] {
}, request).await
}
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, State(options): State<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
move |cx| {
provide_context(cx, auth_session.clone());
@@ -68,7 +68,7 @@ if #[cfg(feature = "ssr")] {
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let leptos_options = Arc::new(conf.leptos_options);
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
@@ -80,8 +80,8 @@ if #[cfg(feature = "ssr")] {
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
.with_config(auth_config))
.layer(SessionLayer::new(session_store))
.layer(Extension(Arc::new(leptos_options)))
.layer(Extension(pool));
.layer(Extension(pool))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -20,15 +20,15 @@ if #[cfg(feature = "ssr")] {
use sqlx::SqlitePool;
pub fn pool(cx: Scope) -> Result<SqlitePool, ServerFnError> {
Ok(use_context::<SqlitePool>(cx)
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> {
Ok(use_context::<AuthSession>(cx)
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() {

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -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/> });
GetPost::register();
ListPostMetadata::register();
let _ = GetPost::register();
let _ = ListPostMetadata::register();
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -11,16 +11,16 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::app::{App, AppProps};
use leptos::{LeptosOptions, view};
use crate::app::App;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
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(),

View File

@@ -1,11 +1,7 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{
extract::{Extension, Path},
routing::{get, post},
Router,
};
use axum::{routing::post, Router};
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
@@ -13,12 +9,12 @@ async fn main() {
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
let leptos_options = Arc::new(conf.leptos_options);
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
GetPost::register();
ListPostMetadata::register();
let _ = GetPost::register();
let _ = ListPostMetadata::register();
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
@@ -28,7 +24,7 @@ async fn main() {
|cx| view! { cx, <App/> },
)
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -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.clone();
let addr = conf.leptos_options.site_addr;
// 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)?

View File

@@ -5,7 +5,7 @@ This is a template demonstrating how to integrate [TailwindCSS](https://tailwind
Install Tailwind and build the CSS:
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
`Trunk.toml` is configured to build the CSS automatically.
Install trunk to client side render this bundle.

View File

@@ -0,0 +1,4 @@
[[hooks]]
stage = "pre_build"
command = "sh"
command_arguments = ["-c", "npx tailwindcss -i input.css -o style/output.css"]

View File

@@ -1,5 +1,5 @@
/*
! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com
! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com
*/
/*
@@ -31,6 +31,7 @@
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 {
@@ -47,6 +48,8 @@ html {
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
}
/*
@@ -433,6 +436,9 @@ 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: ;
@@ -480,6 +486,9 @@ 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: ;

View File

@@ -4,7 +4,7 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -13,16 +13,16 @@ if #[cfg(feature = "ssr")] {
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::error_template::ErrorTemplate;
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
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);

View File

@@ -5,7 +5,7 @@ cfg_if! {
use leptos::*;
use axum::{
routing::{post, get},
extract::{Extension, Path},
extract::{State, Path},
http::Request,
response::{IntoResponse, Response},
Router,
@@ -18,7 +18,7 @@ cfg_if! {
use std::sync::Arc;
//Define a handler to test extractor with state
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
async fn custom_handler(Path(id): Path<String>, State(options): State<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
move |cx| {
provide_context(cx, id.clone());
@@ -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
@@ -42,7 +42,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 leptos_options = conf.leptos_options;
let leptos_options = Arc::new(conf.leptos_options);
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
@@ -52,7 +52,7 @@ cfg_if! {
.route("/special/:id", get(custom_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <TodoApp/> } )
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use crate::{
error_template::{ErrorTemplate, ErrorTemplateProps},
error_template::ErrorTemplate,
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() {

View File

@@ -35,7 +35,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

View File

@@ -43,7 +43,7 @@ impl Todos {
}
pub fn remove(&mut self, id: Uuid) {
self.0.retain(|todo| todo.id != id);
self.retain(|todo| todo.id != id);
}
pub fn remaining(&self) -> usize {
@@ -76,7 +76,23 @@ impl Todos {
}
fn clear_completed(&mut self) {
self.0.retain(|todo| !todo.completed.get());
self.retain(|todo| !todo.completed.get());
}
fn retain(&mut self, mut f: impl FnMut(&Todo) -> bool) {
self.0.retain(|todo| {
let retain = f(todo);
// because these signals are created at the top level,
// they are owned by the <TodoMVC/> component and not
// by the individual <Todo/> components. This means
// that if they are not manually disposed when removed, they
// will be held onto until the <TodoMVC/> is unmounted.
if !retain {
todo.title.dispose();
todo.completed.dispose();
}
retain
})
}
}
@@ -136,7 +152,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(ev::hashchange, move |_| {
let new_mode =
location_hash().map(|hash| route(&hash)).unwrap_or_default();
set_mode(new_mode);

View File

@@ -16,9 +16,9 @@ use actix_web::{
use futures::{Stream, StreamExt};
use http::StatusCode;
use leptos::{
leptos_dom::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,6 +514,43 @@ 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,
{
@@ -533,7 +570,14 @@ where
}
};
stream_app(&options, app, res_options, additional_context).await
stream_app(
&options,
app,
res_options,
additional_context,
replace_blocks,
)
.await
}
};
match method {
@@ -653,103 +697,6 @@ 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 apps 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,
@@ -781,12 +728,14 @@ 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(
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
app,
move |cx| generate_head_metadata_separated(cx).1.into(),
additional_context,
replace_blocks
);
build_stream_response(options, res_options, stream, runtime, scope).await
@@ -985,22 +934,6 @@ 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,
@@ -1035,34 +968,7 @@ 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,
@@ -1091,6 +997,15 @@ 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(),
@@ -1113,7 +1028,7 @@ where
}
}
/// A helper to make it easier to use Axum extractors in server functions. This takes
/// A helper to make it easier to use Actix 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
/// will be extracted from the request and returns some value.

View File

@@ -129,23 +129,6 @@ 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
@@ -622,6 +605,54 @@ 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({
@@ -651,10 +682,11 @@ where
}
};
let (bundle, runtime, scope) =
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
app,
|cx| generate_head_metadata_separated(cx).1.into(),
add_context,
replace_blocks
);
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
@@ -663,6 +695,7 @@ where
})
}
}
#[tracing::instrument(level = "info", fields(error), skip_all)]
async fn generate_response(
res_options: ResponseOptions,
@@ -1093,12 +1126,39 @@ where
}
}
/// This trait allows one to use your custom struct in Axum's router, provided it can provide the
/// `LeptosOptions` to use for the `LeptosRoutes` trait functions.
pub trait LeptosOptionProvider {
fn options(&self) -> LeptosOptions;
}
/// Implement `LeptosOptionProvider` trait for `LeptosOptions` itself.
impl LeptosOptionProvider for LeptosOptions {
fn options(&self) -> LeptosOptions {
self.clone()
}
}
/// Implement `LeptosOptionProvider` trait for any type wrapped in an Arc, if that type implements
/// `LeptosOptionProvider` as states in axum are often provided wrapped in an Arc.
impl<T> LeptosOptionProvider for Arc<T>
where
T: LeptosOptionProvider,
{
fn options(&self) -> LeptosOptions {
(**self).options()
}
}
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes {
pub trait LeptosRoutes<OP>
where
OP: LeptosOptionProvider,
{
fn leptos_routes<IV>(
self,
options: LeptosOptions,
options: OP,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -1107,7 +1167,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
options: OP,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
@@ -1121,16 +1181,20 @@ pub trait LeptosRoutes {
handler: H,
) -> Self
where
H: axum::handler::Handler<T, (), axum::body::Body>,
H: axum::handler::Handler<T, OP, axum::body::Body>,
T: 'static;
}
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
/// to those paths to Leptos's renderer.
impl LeptosRoutes for axum::Router {
impl<OP> LeptosRoutes<OP> for axum::Router<OP>
where
OP: LeptosOptionProvider + Clone + Send + Sync + 'static,
{
#[tracing::instrument(level = "info", fields(error), skip_all)]
fn leptos_routes<IV>(
self,
options: LeptosOptions,
options: OP,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -1143,7 +1207,7 @@ impl LeptosRoutes for axum::Router {
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
options: OP,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
@@ -1161,7 +1225,7 @@ impl LeptosRoutes for axum::Router {
match listing.mode() {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
options.clone(),
options.options(),
additional_context.clone(),
app_fn.clone(),
);
@@ -1173,9 +1237,24 @@ 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.options(),
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(),
options.options(),
additional_context.clone(),
app_fn.clone(),
);
@@ -1189,7 +1268,7 @@ impl LeptosRoutes for axum::Router {
}
SsrMode::Async => {
let s = render_app_async_with_context(
options.clone(),
options.options(),
additional_context.clone(),
app_fn.clone(),
);
@@ -1215,7 +1294,7 @@ impl LeptosRoutes for axum::Router {
handler: H,
) -> Self
where
H: axum::handler::Handler<T, (), axum::body::Body>,
H: axum::handler::Handler<T, OP, axum::body::Body>,
T: 'static,
{
let mut router = self;
@@ -1238,6 +1317,7 @@ impl LeptosRoutes for axum::Router {
router
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn get_leptos_pool() -> LocalPoolHandle {
static LOCAL_POOL: OnceCell<LocalPoolHandle> = OnceCell::new();

View File

@@ -506,6 +506,48 @@ 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({
@@ -548,10 +590,11 @@ where
};
let (bundle, runtime, scope) =
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
app,
|cx| generate_head_metadata_separated(cx).1.into(),
add_context,
replace_blocks
);
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
@@ -1091,6 +1134,22 @@ 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(),

View File

@@ -23,7 +23,7 @@ server_fn = { workspace = true, default-features = false }
leptos = { path = ".", default-features = false }
[features]
default = ["serde", "nightly"]
default = ["csr", "serde"]
csr = [
"leptos_dom/web",
"leptos_macro/csr",
@@ -44,11 +44,11 @@ ssr = [
"leptos_reactive/ssr",
"leptos_server/ssr",
]
nightly = [
"leptos_dom/nightly",
"leptos_macro/nightly",
"leptos_reactive/nightly",
"leptos_server/nightly",
stable = [
"leptos_dom/stable",
"leptos_macro/stable",
"leptos_reactive/stable",
"leptos_server/stable",
]
serde = ["leptos_reactive/serde"]
serde-lite = ["leptos_reactive/serde-lite"]
@@ -57,10 +57,8 @@ rkyv = ["leptos_reactive/rkyv"]
tracing = ["leptos_macro/tracing"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]
denylist = ["stable", "tracing"]
skip_feature_sets = [
[
],
[
"csr",
"ssr",

View File

@@ -152,7 +152,6 @@ 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::{
@@ -161,11 +160,10 @@ 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, IntoView, NodeRef, Property, View,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
};
pub use leptos_macro::*;
pub use leptos_reactive::*;

View File

@@ -1,7 +1,11 @@
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
@@ -52,16 +56,17 @@ use std::rc::Rc;
tracing::instrument(level = "info", skip_all)
)]
#[component(transparent)]
pub fn Suspense<F, E>(
pub fn Suspense<F, E, V>(
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) -> Fragment>,
children: Box<dyn Fn(Scope) -> V>,
) -> impl IntoView
where
F: Fn() -> E + 'static,
E: IntoView,
V: IntoView + 'static,
{
let context = SuspenseContext::new(cx);
@@ -70,8 +75,9 @@ 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")))]
@@ -79,22 +85,33 @@ where
move || {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
if context.ready() {
if let Some(disposer) = prev_disposer.take() {
disposer.dispose();
}
let (view, disposer) =
cx.run_child_scope(|cx| 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::*;
// run the child; we'll probably throw this away, but it will register resource reads
let child = orig_child(cx).into_view(cx);
let _child = orig_child(cx).into_view(cx);
let after_original_child = HydrationCtx::id();
let initial = {
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
child
let orig_child = Rc::clone(&orig_child);
HydrationCtx::continue_from(current_id.clone());
Fragment::lazy(Box::new(move || {
vec![DynChild::new(move || orig_child(cx)).into_view(cx)]
})).into_view(cx)
}
// show the fallback, but also prepare to stream HTML
else {
@@ -148,7 +165,7 @@ where
_ => unreachable!(),
};
HydrationCtx::continue_from(before_me);
HydrationCtx::continue_from(current_id.clone());
leptos_dom::View::Suspense(current_id, core_component)
}

View File

@@ -78,7 +78,7 @@ where
F: Fn() -> E + 'static,
E: IntoView,
{
let prev_children = Rc::new(RefCell::new(None::<Vec<View>>));
let prev_children = Rc::new(RefCell::new(None::<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);
let frag = children(cx).into_view(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.nodes.clone());
*prev_children.borrow_mut() = Some(frag.clone());
}
if is_first_run(&first_run, &suspense_context) {
let has_local_only = suspense_context.has_local_only()

View File

@@ -14,16 +14,13 @@ fn simple_ssr_test() {
</div>
};
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 \
assert!(rendered.into_view(cx).render_to_string(cx).contains(
"<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><!--leptos-view|leptos-tests-ssr.\
rs-8|close-->"
);
id=\"_0-5\">+1</button></div>"
));
});
}
@@ -54,28 +51,13 @@ fn ssr_test_with_components() {
</div>
};
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 \
assert!(rendered.into_view(cx).render_to_string(cx).contains(
"<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><!--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-->"
);
id=\"_0-1-5\">+1</button></div>"
));
});
}
@@ -106,29 +88,13 @@ fn ssr_test_with_snake_case_components() {
</div>
};
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 \
assert!(rendered.into_view(cx).render_to_string(cx).contains(
"<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><!--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-->"
);
id=\"_0-1-5\">+1</button></div>"
));
});
}
@@ -144,12 +110,10 @@ fn test_classes() {
<div class="my big" class:a={move || value.get() > 10} class:red=true class:car={move || value.get() > 1}></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-->"
);
assert!(rendered
.into_view(cx)
.render_to_string(cx)
.contains("<div id=\"_0-1\" class=\"my big red car\"></div>"));
});
}
@@ -168,13 +132,10 @@ fn ssr_with_styles() {
</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-->"
);
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>"
));
});
}
@@ -190,11 +151,9 @@ fn ssr_option() {
<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-->"
);
assert!(rendered
.into_view(cx)
.render_to_string(cx)
.contains("<option id=\"_0-1\"></option>"));
});
}

View File

@@ -17,7 +17,7 @@ leptos_actix = { path = "../../../../integrations/actix", optional = true }
leptos_router = { path = "../../../../router", default-features = false }
log = "0.4"
simple_logger = "4"
wasm-bindgen = "0.2"
wasm-bindgen = "0.2.85"
serde = "1.0.159"
tokio = { version = "1.27.0", features = ["time"], optional = true }

View File

@@ -56,6 +56,7 @@ pub fn App(cx: Scope) -> impl IntoView {
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
<Route path="none" view=|cx| view! { cx, <None/> }/>
</Route>
// in-order
<Route
@@ -71,6 +72,7 @@ pub fn App(cx: Scope) -> impl IntoView {
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
<Route path="none" view=|cx| view! { cx, <None/> }/>
</Route>
// async
<Route
@@ -86,6 +88,7 @@ pub fn App(cx: Scope) -> impl IntoView {
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
<Route path="none" view=|cx| view! { cx, <None/> }/>
</Route>
</Routes>
</main>
@@ -101,6 +104,7 @@ fn SecondaryNav(cx: Scope) -> impl IntoView {
<A href="single">"Single"</A>
<A href="parallel">"Parallel"</A>
<A href="inside-component">"Inside Component"</A>
<A href="none">"No Resources"</A>
</nav>
}
}
@@ -217,3 +221,25 @@ fn InsideComponentChild(cx: Scope) -> impl IntoView {
</Suspense>
}
}
#[component]
fn None(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<div>
<Suspense fallback=|| "Loading 1...">
<div>"Children inside Suspense should hydrate properly."</div>
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
</Suspense>
<p>"Children following " <code>"<Suspense/>"</code> " should hydrate properly."</p>
<div>
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
</div>
</div>
}
}

View File

@@ -43,6 +43,7 @@ features = [
"Comment",
"Document",
"DomTokenList",
"CssStyleDeclaration",
"Location",
"Range",
"Text",
@@ -157,7 +158,7 @@ features = [
default = []
web = ["leptos_reactive/csr"]
ssr = ["leptos_reactive/ssr"]
nightly = ["leptos_reactive/nightly"]
stable = ["leptos_reactive/stable"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View File

@@ -332,32 +332,8 @@ impl IntervalHandle {
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all, fields(duration = ?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))
pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
_ = set_interval_with_handle(cb, duration);
}
/// Repeatedly calls the given function, with a delay of the given duration between calls,
@@ -402,24 +378,6 @@ 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,
@@ -456,8 +414,21 @@ pub fn window_event_listener_untyped(
}
}
/// 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>(
/// 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>(
event: E,
cb: impl Fn(E::EventType) + 'static,
) where

View File

@@ -63,7 +63,7 @@ cfg_if! {
use crate::{
ev::EventDescriptor,
hydration::HydrationCtx,
macro_helpers::{IntoAttribute, IntoClass, IntoProperty},
macro_helpers::{IntoAttribute, IntoClass, IntoProperty, IntoStyle},
Element, Fragment, IntoView, NodeRef, Text, View,
};
use leptos_reactive::Scope;
@@ -382,26 +382,6 @@ 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(
@@ -824,6 +804,69 @@ 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(

View File

@@ -1,7 +1,7 @@
#![deny(missing_docs)]
#![forbid(unsafe_code)]
#![cfg_attr(feature = "nightly", feature(fn_traits))]
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
#![cfg_attr(not(feature = "stable"), feature(fn_traits))]
#![cfg_attr(not(feature = "stable"), feature(unboxed_closures))]
//! The DOM implementation for `leptos`.
@@ -1125,7 +1125,7 @@ viewable_primitive![
];
cfg_if! {
if #[cfg(feature = "nightly")] {
if #[cfg(not(feature = "stable"))] {
viewable_primitive! {
std::backtrace::Backtrace
}

View File

@@ -0,0 +1,209 @@
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);

View File

@@ -1,6 +1,8 @@
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::*;

View File

@@ -79,12 +79,6 @@ 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.
@@ -160,7 +154,7 @@ impl<T: ElementDescriptor> Clone for NodeRef<T> {
impl<T: ElementDescriptor + 'static> Copy for NodeRef<T> {}
cfg_if::cfg_if! {
if #[cfg(feature = "nightly")] {
if #[cfg(not(feature = "stable"))] {
impl<T: Clone + ElementDescriptor + 'static> FnOnce<()> for NodeRef<T> {
type Output = Option<HtmlElement<T>>;

View File

@@ -146,6 +146,44 @@ 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();
@@ -177,7 +215,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
});
let cx = Scope { runtime, id: scope };
let blocking_fragments = FuturesUnordered::new();
let mut blocking_fragments = FuturesUnordered::new();
let fragments = FuturesUnordered::new();
for (fragment_id, data) in pending_fragments {
@@ -198,24 +236,46 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
// HTML for the view function and script to store resources
let stream = futures::stream::once(async move {
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 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 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
@@ -229,6 +289,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
(stream, runtime, scope)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)

View File

@@ -240,13 +240,20 @@ impl View {
dont_escape_text: bool,
) {
match self {
View::Suspense(id, _) => {
View::Suspense(id, view) => {
let id = id.to_string();
if let Some(data) = cx.take_pending_fragment(&id) {
chunks.push_back(StreamChunk::Async {
chunks: data.in_order,
should_block: data.should_block,
});
} else {
// if not registered, means it was already resolved
View::CoreComponent(view).into_stream_chunks_helper(
cx,
chunks,
dont_escape_text,
);
}
}
View::Text(node) => {

View File

@@ -39,7 +39,7 @@ default = ["ssr"]
csr = []
hydrate = []
ssr = []
nightly = ["server_fn_macro/nightly"]
stable = ["server_fn_macro/stable"]
tracing = []
[package.metadata.cargo-all-features]

View File

@@ -1,4 +1,4 @@
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
#![forbid(unsafe_code)]
#[macro_use]
@@ -201,7 +201,29 @@ mod template;
/// # });
/// ```
///
/// 8. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
/// 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
/// [NodeRef](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to use later.
/// ```rust
/// # use leptos::*;
@@ -218,7 +240,7 @@ mod template;
/// # });
/// ```
///
/// 9. You can add the same class to every element in the view by passing in a special
/// 10. 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
@@ -236,7 +258,7 @@ mod template;
/// # });
/// ```
///
/// 10. You can set any HTML elements `innerHTML` with the `inner_html` attribute on an
/// 11. You can set any HTML elements `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
@@ -353,7 +375,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
cfg_if::cfg_if! {
if #[cfg(all(debug_assertions, feature = "nightly"))] {
if #[cfg(all(debug_assertions, not(feature = "stable")))] {
Some(leptos_hot_reload::span_to_stable_id(
site.source_file().path(),
site.into()

View File

@@ -301,10 +301,12 @@ fn root_element_to_tokens_ssr(
let chunks = chunks.into_iter().map(|chunk| match chunk {
SsrElementChunks::String { template, holes } => {
if holes.is_empty() {
let template = template.replace("\\{", "{").replace("\\}", "}");
quote! {
leptos::leptos_dom::html::StringOrView::String(#template.into())
}
} else {
let template = template.replace("\\{", "{{").replace("\\}", "}}");
quote! {
leptos::leptos_dom::html::StringOrView::String(
format!(
@@ -451,6 +453,7 @@ fn element_to_tokens_ssr(
holes.push(hydration_id);
set_class_attribute_ssr(cx, node, template, holes, global_class);
set_style_attribute_ssr(cx, node, template, holes);
if is_self_closing(node) {
template.push_str("/>");
@@ -489,8 +492,8 @@ fn element_to_tokens_ssr(
};
template.push_str(
&value
.replace('{', "{{")
.replace('}', "}}"),
.replace('{', "\\{")
.replace('}', "\\}"),
);
} else {
template.push_str("{}");
@@ -555,9 +558,10 @@ fn attribute_to_tokens_ssr<'a>(
})
} else if name.strip_prefix("prop:").is_some()
|| name.strip_prefix("class:").is_some()
|| name.strip_prefix("style:").is_some()
{
// ignore props for SSR
// ignore classes: we'll handle these separately
// ignore classes and sdtyles: we'll handle these separately
} else if name == "inner_html" {
return node.value.as_ref();
} else {
@@ -575,7 +579,7 @@ fn attribute_to_tokens_ssr<'a>(
for more information and an example: https://github.com/leptos-rs/leptos/issues/773")
};
if name != "class" {
if name != "class" && name != "style" {
template.push(' ');
if let Some(value) = node.value.as_ref() {
@@ -734,6 +738,113 @@ fn set_class_attribute_ssr(
}
}
fn set_style_attribute_ssr(
cx: &Ident,
node: &NodeElement,
template: &mut String,
holes: &mut Vec<TokenStream>,
) {
let static_style_attr = node
.attributes
.iter()
.filter_map(|a| match a {
Node::Attribute(attr) if attr.key.to_string() == "style" => {
attr.value.as_ref().and_then(value_to_string)
}
_ => None,
})
.next()
.map(|style| format!("{style};"));
let dyn_style_attr = node
.attributes
.iter()
.filter_map(|a| {
if let Node::Attribute(a) = a {
if a.key.to_string() == "style" {
if a.value.as_ref().and_then(value_to_string).is_some()
|| fancy_style_name(&a.key.to_string(), cx, a).is_some()
{
None
} else {
Some((a.key.span(), &a.value))
}
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
let style_attrs = node
.attributes
.iter()
.filter_map(|node| {
if let Node::Attribute(node) = node {
let name = node.key.to_string();
if name == "style" {
return if let Some((_, name, value)) =
fancy_style_name(&name, cx, node)
{
let span = node.key.span();
Some((span, name, value))
} else {
None
};
}
if name.starts_with("style:") || name.starts_with("style-") {
let name = if name.starts_with("style:") {
name.replacen("style:", "", 1)
} else if name.starts_with("style-") {
name.replacen("style-", "", 1)
} else {
name
};
let value = attribute_value(node);
let span = node.key.span();
Some((span, name, value))
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
if static_style_attr.is_some()
|| !dyn_style_attr.is_empty()
|| !style_attrs.is_empty()
{
template.push_str(" style=\"");
template.push_str(&static_style_attr.unwrap_or_default());
for (_span, value) in dyn_style_attr {
if let Some(value) = value {
template.push_str(" {};");
let value = value.as_ref();
holes.push(quote! {
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
.unwrap_or_default()
});
}
}
for (_span, name, value) in &style_attrs {
template.push_str(" {}");
holes.push(quote! {
(#cx, #value).into_style(#cx).as_value_string(#name).unwrap_or_default()
});
}
template.push('"');
}
}
#[allow(clippy::too_many_arguments)]
fn fragment_to_tokens(
cx: &Ident,
@@ -916,8 +1027,11 @@ fn element_to_tokens(
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
let name = node.key.to_string();
if name.trim().starts_with("class:")
|| fancy_class_name(&name, cx, node).is_some()
let name = name.trim();
if name.starts_with("class:")
|| fancy_class_name(name, cx, node).is_some()
|| name.starts_with("style:")
|| fancy_style_name(name, cx, node).is_some()
{
None
} else {
@@ -941,6 +1055,20 @@ fn element_to_tokens(
None
}
});
let style_attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
let name = node.key.to_string();
if let Some((fancy, _, _)) = fancy_style_name(&name, cx, node) {
Some(fancy)
} else if name.trim().starts_with("style:") {
Some(attribute_to_tokens(cx, node, global_class))
} else {
None
}
} else {
None
}
});
let global_class_expr = match global_class {
None => quote! {},
Some(class) => {
@@ -1034,6 +1162,7 @@ fn element_to_tokens(
#name
#(#attrs)*
#(#class_attrs)*
#(#style_attrs)*
#global_class_expr
#(#children)*
#view_marker
@@ -1149,6 +1278,21 @@ fn attribute_to_tokens(
quote! {
#class(#name, (#cx, #[allow(unused_braces)] #value))
}
} else if let Some(name) = name.strip_prefix("style:") {
let value = attribute_value(node);
let style = match &node.key {
NodeName::Punctuated(parts) => &parts[0],
_ => unreachable!(),
};
let style = {
let span = style.span();
quote_spanned! {
span => .style
}
};
quote! {
#style(#name, (#cx, #[allow(unused_braces)] #value))
}
} else {
let name = name.replacen("attr:", "", 1);
@@ -1800,3 +1944,51 @@ fn fancy_class_name<'a>(
}
None
}
fn fancy_style_name<'a>(
name: &str,
cx: &Ident,
node: &'a NodeAttribute,
) -> Option<(TokenStream, String, &'a Expr)> {
// special case for complex dynamic style names:
if name == "style" {
if let Some(expr) = node.value.as_ref() {
if let syn::Expr::Tuple(tuple) = expr.as_ref() {
if tuple.elems.len() == 2 {
let span = node.key.span();
let style = quote_spanned! {
span => .style
};
let style_name = &tuple.elems[0];
let style_name = if let Expr::Lit(ExprLit {
lit: Lit::Str(s),
..
}) = style_name
{
s.value()
} else {
proc_macro_error::emit_error!(
style_name.span(),
"style name must be a string literal"
);
Default::default()
};
let value = &tuple.elems[1];
return Some((
quote! {
#style(#style_name, (#cx, #value))
},
style_name,
value,
));
} else {
proc_macro_error::emit_error!(
tuple.span(),
"style tuples must have two elements."
)
}
}
}
}
None
}

View File

@@ -68,16 +68,15 @@ hydrate = [
"dep:web-sys",
]
ssr = ["dep:tokio"]
nightly = []
stable = []
serde = []
serde-lite = ["dep:serde-lite"]
miniserde = ["dep:miniserde"]
rkyv = ["dep:rkyv", "dep:bytecheck"]
[package.metadata.cargo-all-features]
denylist = ["stable"]
skip_feature_sets = [
[
],
[
"csr",
"ssr",

View File

@@ -1,7 +1,7 @@
#![deny(missing_docs)]
#![cfg_attr(feature = "nightly", feature(fn_traits))]
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
#![cfg_attr(not(feature = "stable"), feature(fn_traits))]
#![cfg_attr(not(feature = "stable"), feature(unboxed_closures))]
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
//! The reactive system for the [Leptos](https://docs.rs/leptos/latest/leptos/) Web framework.
//!

View File

@@ -81,7 +81,7 @@ pub fn create_resource<S, T, Fu>(
fetcher: impl Fn(S) -> Fu + 'static,
) -> Resource<S, T>
where
S: PartialEq + Debug + Clone + 'static,
S: PartialEq + Clone + 'static,
T: Serializable + 'static,
Fu: Future<Output = T> + 'static,
{
@@ -119,7 +119,7 @@ pub fn create_resource_with_initial_value<S, T, Fu>(
initial_value: Option<T>,
) -> Resource<S, T>
where
S: PartialEq + Debug + Clone + 'static,
S: PartialEq + Clone + 'static,
T: Serializable + 'static,
Fu: Future<Output = T> + 'static,
{
@@ -165,7 +165,7 @@ pub fn create_blocking_resource<S, T, Fu>(
fetcher: impl Fn(S) -> Fu + 'static,
) -> Resource<S, T>
where
S: PartialEq + Debug + Clone + 'static,
S: PartialEq + Clone + 'static,
T: Serializable + 'static,
Fu: Future<Output = T> + 'static,
{
@@ -186,7 +186,7 @@ fn create_resource_helper<S, T, Fu>(
serializable: ResourceSerialization,
) -> Resource<S, T>
where
S: PartialEq + Debug + Clone + 'static,
S: PartialEq + Clone + 'static,
T: Serializable + 'static,
Fu: Future<Output = T> + 'static,
{
@@ -290,7 +290,7 @@ pub fn create_local_resource<S, T, Fu>(
fetcher: impl Fn(S) -> Fu + 'static,
) -> Resource<S, T>
where
S: PartialEq + Debug + Clone + 'static,
S: PartialEq + Clone + 'static,
T: 'static,
Fu: Future<Output = T> + 'static,
{
@@ -324,7 +324,7 @@ pub fn create_local_resource_with_initial_value<S, T, Fu>(
initial_value: Option<T>,
) -> Resource<S, T>
where
S: PartialEq + Debug + Clone + 'static,
S: PartialEq + Clone + 'static,
T: 'static,
Fu: Future<Output = T> + 'static,
{
@@ -380,7 +380,7 @@ where
#[cfg(not(feature = "hydrate"))]
fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Rc<ResourceState<S, T>>)
where
S: PartialEq + Debug + Clone + 'static,
S: PartialEq + Clone + 'static,
T: 'static,
{
SUPPRESS_RESOURCE_LOAD.with(|s| {
@@ -393,7 +393,7 @@ where
#[cfg(feature = "hydrate")]
fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Rc<ResourceState<S, T>>)
where
S: PartialEq + Debug + Clone + 'static,
S: PartialEq + Clone + 'static,
T: Serializable + 'static,
{
use wasm_bindgen::{JsCast, UnwrapThrowExt};

View File

@@ -3,9 +3,7 @@ use crate::{
create_isomorphic_effect, create_signal, ReadSignal, Scope, SignalUpdate,
WriteSignal,
};
use std::{
cell::RefCell, collections::HashMap, fmt::Debug, hash::Hash, rc::Rc,
};
use std::{cell::RefCell, collections::HashMap, hash::Hash, rc::Rc};
/// Creates a conditional signal that only notifies subscribers when a change
/// in the source signals value changes whether it is equal to the key value
@@ -52,7 +50,7 @@ pub fn create_selector<T>(
source: impl Fn() -> T + Clone + 'static,
) -> impl Fn(T) -> bool + Clone
where
T: PartialEq + Eq + Debug + Clone + Hash + 'static,
T: PartialEq + Eq + Clone + Hash + 'static,
{
create_selector_with_fn(cx, source, PartialEq::eq)
}
@@ -69,7 +67,7 @@ pub fn create_selector_with_fn<T>(
f: impl Fn(&T, &T) -> bool + Clone + 'static,
) -> impl Fn(T) -> bool + Clone
where
T: PartialEq + Eq + Debug + Clone + Hash + 'static,
T: PartialEq + Eq + Clone + Hash + 'static,
{
#[allow(clippy::type_complexity)]
let subs: Rc<

View File

@@ -17,7 +17,7 @@ use thiserror::Error;
macro_rules! impl_get_fn_traits {
($($ty:ident $(($method_name:ident))?),*) => {
$(
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl<T: Clone> FnOnce<()> for $ty<T> {
type Output = T;
@@ -27,7 +27,7 @@ macro_rules! impl_get_fn_traits {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl<T: Clone> FnMut<()> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
@@ -35,7 +35,7 @@ macro_rules! impl_get_fn_traits {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl<T: Clone> Fn<()> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
@@ -55,7 +55,7 @@ macro_rules! impl_get_fn_traits {
macro_rules! impl_set_fn_traits {
($($ty:ident $($method_name:ident)?),*) => {
$(
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl<T> FnOnce<(T,)> for $ty<T> {
type Output = ();
@@ -65,7 +65,7 @@ macro_rules! impl_set_fn_traits {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl<T> FnMut<(T,)> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, args: (T,)) -> Self::Output {
@@ -73,7 +73,7 @@ macro_rules! impl_set_fn_traits {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl<T> Fn<(T,)> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call(&self, args: (T,)) -> Self::Output {
@@ -167,18 +167,6 @@ pub trait SignalUpdate<T> {
#[track_caller]
fn update(&self, f: impl FnOnce(&mut T));
/// Applies a function to the current value to mutate it in place
/// and notifies subscribers that the signal has changed. Returns
/// [`Some(O)`] if the signal is still valid, [`None`] otherwise.
///
/// **Note:** `update()` does not auto-memoize, i.e., it will notify subscribers
/// even if the value has not actually changed.
#[deprecated = "Please use `try_update` instead. This method will be \
removed in a future version of this crate"]
fn update_returning<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
self.try_update(f)
}
/// Applies a function to the current value to mutate it in place
/// and notifies subscribers that the signal has changed. Returns
/// [`Some(O)`] if the signal is still valid, [`None`] otherwise.
@@ -249,19 +237,6 @@ pub trait SignalUpdateUntracked<T> {
#[track_caller]
fn update_untracked(&self, f: impl FnOnce(&mut T));
/// Runs the provided closure with a mutable reference to the current
/// value without notifying dependents and returns
/// the value the closure returned.
#[deprecated = "Please use `try_update_untracked` instead. This method \
will be removed in a future version of `leptos`"]
#[inline(always)]
fn update_returning_untracked<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
self.try_update_untracked(f)
}
/// Runs the provided closure with a mutable reference to the current
/// value without notifying dependents and returns
/// the value the closure returned.
@@ -930,27 +905,6 @@ impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
self.id.update_with_no_effect(self.runtime, f);
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "WriteSignal::update_returning_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn update_returning_untracked<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
self.id.update_with_no_effect(self.runtime, f)
}
#[inline(always)]
fn try_update_untracked<O>(
&self,
@@ -1345,27 +1299,6 @@ impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
self.id.update_with_no_effect(self.runtime, f);
}
#[cfg_attr(
any(debug_assertions, features="ssr"),
instrument(
level = "trace",
name = "RwSignal::update_returning_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn update_returning_untracked<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
self.id.update_with_no_effect(self.runtime, f)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(

View File

@@ -68,16 +68,41 @@ use crate::{
/// // setting name only causes name to log, not count
/// set_name("Bob".into());
/// ```
pub fn create_slice<T, O>(
pub fn create_slice<T, O, S>(
cx: Scope,
signal: RwSignal<T>,
getter: impl Fn(&T) -> O + Clone + Copy + 'static,
setter: impl Fn(&mut T, O) + Clone + Copy + 'static,
) -> (Signal<O>, SignalSetter<O>)
setter: impl Fn(&mut T, S) + Clone + Copy + 'static,
) -> (Signal<O>, SignalSetter<S>)
where
O: PartialEq,
{
let getter = create_memo(cx, move |_| signal.with(getter));
let setter = move |value| signal.update(|x| setter(x, value));
(getter.into(), setter.mapped_signal_setter(cx))
(
create_read_slice(cx, signal, getter),
create_write_slice(cx, signal, setter),
)
}
/// Takes a memoized, read-only slice of a signal. This is equivalent to the
/// read-only half of [`create_slice`].
pub fn create_read_slice<T, O>(
cx: Scope,
signal: RwSignal<T>,
getter: impl Fn(&T) -> O + Clone + Copy + 'static,
) -> Signal<O>
where
O: PartialEq,
{
create_memo(cx, move |_| signal.with(getter)).into()
}
/// Creates a setter to access one slice of a signal. This is equivalent to the
/// write-only half of [`create_slice`].
pub fn create_write_slice<T, O>(
cx: Scope,
signal: RwSignal<T>,
setter: impl Fn(&mut T, O) + Clone + Copy + 'static,
) -> SignalSetter<O> {
let setter = move |value| signal.update(|x| setter(x, value));
setter.mapped_signal_setter(cx)
}

View File

@@ -56,42 +56,8 @@ impl<T> StoredValue<T> {
/// }
/// let data = store_value(cx, MyCloneableData { value: "a".into() });
///
/// // calling .get() clones and returns the value
/// assert_eq!(data.get().value, "a");
/// // there's a short-hand getter form
/// assert_eq!(data().value, "a");
/// # });
/// ```
#[track_caller]
#[deprecated = "Please use `get_value` instead, as this method does not \
track the stored value. This method will also be removed \
in a future version of `leptos`"]
pub fn get(&self) -> T
where
T: Clone,
{
self.get_value()
}
/// Returns a clone of the signals current value, subscribing the effect
/// to this signal.
///
/// # Panics
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// #[derive(Clone)]
/// pub struct MyCloneableData {
/// pub value: String,
/// }
/// let data = store_value(cx, MyCloneableData { value: "a".into() });
///
/// // calling .get() clones and returns the value
/// assert_eq!(data.get().value, "a");
/// // calling .get_value() clones and returns the value
/// assert_eq!(data.get_value().value, "a");
/// // there's a short-hand getter form
/// assert_eq!(data().value, "a");
/// # });
@@ -104,18 +70,6 @@ impl<T> StoredValue<T> {
self.try_get_value().expect("could not get stored value")
}
/// Same as [`StoredValue::get`] but will not panic by default.
#[track_caller]
#[deprecated = "Please use `try_get_value` instead, as this method does \
not track the stored value. This method will also be \
removed in a future version of `leptos`"]
pub fn try_get(&self) -> Option<T>
where
T: Clone,
{
self.try_get_value()
}
/// Same as [`StoredValue::get`] but will not panic by default.
#[track_caller]
pub fn try_get_value(&self) -> Option<T>
@@ -125,33 +79,6 @@ impl<T> StoredValue<T> {
self.try_with_value(T::clone)
}
/// Applies a function to the current stored value.
///
/// # Panics
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
///
/// // calling .with() to extract the value
/// assert_eq!(data.with(|data| data.value.clone()), "a");
/// });
/// ```
#[track_caller]
#[deprecated = "Please use `with_value` instead, as this method does not \
track the stored value. This method will also be removed \
in a future version of `leptos`"]
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
self.with_value(f)
}
/// Applies a function to the current stored value.
///
/// # Panics
@@ -167,8 +94,8 @@ impl<T> StoredValue<T> {
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
///
/// // calling .with() to extract the value
/// assert_eq!(data.with(|data| data.value.clone()), "a");
/// // calling .with_value() to extract the value
/// assert_eq!(data.with_value(|data| data.value.clone()), "a");
/// # });
/// ```
#[track_caller]
@@ -178,15 +105,6 @@ impl<T> StoredValue<T> {
self.try_with_value(f).expect("could not get stored value")
}
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
/// the signal is still valid. [`None`] otherwise.
#[deprecated = "Please use `try_with_value` instead, as this method does \
not track the stored value. This method will also be \
removed in a future version of `leptos`"]
pub fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
self.try_with_value(f)
}
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
/// the signal is still valid. [`None`] otherwise.
pub fn try_with_value<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
@@ -214,8 +132,8 @@ impl<T> StoredValue<T> {
/// pub value: String
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// data.update(|data| data.value = "b".into());
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// data.update_value(|data| data.value = "b".into());
/// assert_eq!(data.with_value(|data| data.value.clone()), "b");
/// });
/// ```
///
@@ -228,54 +146,12 @@ impl<T> StoredValue<T> {
/// }
///
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// let updated = data.update_returning(|data| {
/// let updated = data.try_update_value(|data| {
/// data.value = "b".into();
/// data.value.clone()
/// });
///
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// assert_eq!(updated, Some(String::from("b")));
/// # });
/// ```
#[track_caller]
#[deprecated = "Please use `update_value` instead, as this method does not \
track the stored value. This method will also be removed \
in a future version of `leptos`"]
pub fn update(&self, f: impl FnOnce(&mut T)) {
self.update_value(f);
}
/// Updates the stored value.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// data.update(|data| data.value = "b".into());
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// });
/// ```
///
/// ```
/// use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String,
/// }
///
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// let updated = data.update_returning(|data| {
/// data.value = "b".into();
/// data.value.clone()
/// });
///
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// assert_eq!(data.with_value(|data| data.value.clone()), "b");
/// assert_eq!(updated, Some(String::from("b")));
/// # });
/// ```
@@ -285,18 +161,6 @@ impl<T> StoredValue<T> {
.expect("could not set stored value");
}
/// Updates the stored value.
#[track_caller]
#[deprecated = "Please use `try_update_value` instead, as this method does \
not track the stored value. This method will also be \
removed in a future version of `leptos`"]
pub fn update_returning<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
self.try_update_value(f)
}
/// Same as [`Self::update`], but returns [`Some(O)`] if the
/// signal is still valid, [`None`] otherwise.
pub fn try_update_value<O>(self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
@@ -311,29 +175,6 @@ impl<T> StoredValue<T> {
.flatten()
}
/// Sets the stored value.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// data.set(MyUncloneableData { value: "b".into() });
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// });
/// ```
#[track_caller]
#[deprecated = "Please use `set_value` instead, as this method does not \
track the stored value. This method will also be removed \
in a future version of `leptos`"]
pub fn set(&self, value: T) {
self.set_value(value);
}
/// Sets the stored value.
///
/// # Examples
@@ -345,8 +186,8 @@ impl<T> StoredValue<T> {
/// pub value: String,
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// data.set(MyUncloneableData { value: "b".into() });
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// data.set_value(MyUncloneableData { value: "b".into() });
/// assert_eq!(data.with_value(|data| data.value.clone()), "b");
/// # });
/// ```
#[track_caller]
@@ -403,10 +244,10 @@ impl<T> StoredValue<T> {
/// pub value: String,
/// }
///
/// // ✅ you can move the `StoredValue` and access it with .with()
/// // ✅ you can move the `StoredValue` and access it with .with_value()
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// let callback_a = move || data.with(|data| data.value == "a");
/// let callback_b = move || data.with(|data| data.value == "b");
/// let callback_a = move || data.with_value(|data| data.value == "a");
/// let callback_b = move || data.with_value(|data| data.value == "b");
/// # }).dispose();
/// ```
#[track_caller]

View File

@@ -225,7 +225,7 @@ impl SignalSet<()> for Trigger {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl FnOnce<()> for Trigger {
type Output = ();
@@ -235,7 +235,7 @@ impl FnOnce<()> for Trigger {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl FnMut<()> for Trigger {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
@@ -243,7 +243,7 @@ impl FnMut<()> for Trigger {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl Fn<()> for Trigger {
#[inline(always)]
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {

View File

@@ -1,10 +1,10 @@
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_isomorphic_effect, create_memo, create_runtime, create_rw_signal,
create_scope, create_signal, SignalSet,
};
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn effect_runs() {
use std::{cell::RefCell, rc::Rc};
@@ -32,7 +32,7 @@ fn effect_runs() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn effect_tracks_memo() {
use std::{cell::RefCell, rc::Rc};
@@ -62,7 +62,7 @@ fn effect_tracks_memo() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn untrack_mutes_effect() {
use std::{cell::RefCell, rc::Rc};
@@ -92,7 +92,7 @@ fn untrack_mutes_effect() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn batching_actually_batches() {
use std::{cell::Cell, rc::Rc};

View File

@@ -1,9 +1,9 @@
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_memo, create_runtime, create_scope, create_signal,
};
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn basic_memo() {
create_scope(create_runtime(), |cx| {
@@ -13,7 +13,7 @@ fn basic_memo() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn memo_with_computed_value() {
create_scope(create_runtime(), |cx| {
@@ -29,7 +29,7 @@ fn memo_with_computed_value() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn nested_memos() {
create_scope(create_runtime(), |cx| {
@@ -51,7 +51,7 @@ fn nested_memos() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn memo_runs_only_when_inputs_change() {
use std::{cell::Cell, rc::Rc};
@@ -94,7 +94,7 @@ fn memo_runs_only_when_inputs_change() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn diamond_problem() {
use std::{cell::Cell, rc::Rc};
@@ -131,7 +131,7 @@ fn diamond_problem() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn dynamic_dependencies() {
use leptos_reactive::create_isomorphic_effect;

View File

@@ -1,7 +1,7 @@
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
use leptos_reactive::{create_runtime, create_scope, create_signal};
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn basic_signal() {
create_scope(create_runtime(), |cx| {
@@ -13,7 +13,7 @@ fn basic_signal() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn derived_signals() {
create_scope(create_runtime(), |cx| {

View File

@@ -1,10 +1,10 @@
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_isomorphic_effect, create_runtime, create_scope, create_signal,
signal_prelude::*, SignalGetUntracked, SignalSetUntracked,
};
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn untracked_set_doesnt_trigger_effect() {
use std::{cell::RefCell, rc::Rc};
@@ -36,7 +36,7 @@ fn untracked_set_doesnt_trigger_effect() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn untracked_get_doesnt_trigger_effect() {
use std::{cell::RefCell, rc::Rc};

View File

@@ -26,7 +26,7 @@ default-tls = ["server_fn/default-tls"]
hydrate = ["leptos_reactive/hydrate"]
rustls = ["server_fn/rustls"]
ssr = ["leptos_reactive/ssr", "server_fn/ssr"]
nightly = ["leptos_reactive/nightly", "server_fn/nightly"]
stable = ["leptos_reactive/stable", "server_fn/stable"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.3.0-alpha"
version = "0.3.0"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -12,6 +12,7 @@ cfg-if = "1"
leptos = { workspace = true }
tracing = "0.1"
wasm-bindgen = "0.2"
indexmap = "1"
[dependencies.web-sys]
version = "0.3"
@@ -22,7 +23,7 @@ default = []
csr = ["leptos/csr", "leptos/tracing"]
hydrate = ["leptos/hydrate", "leptos/tracing"]
ssr = ["leptos/ssr", "leptos/tracing"]
nightly = ["leptos/nightly", "leptos/tracing"]
stable = ["leptos/stable", "leptos/tracing"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View File

@@ -45,6 +45,7 @@
//! which mode your app is operating in.
use cfg_if::cfg_if;
use indexmap::IndexMap;
use leptos::{
leptos_dom::{debug_warn, html::AnyElement},
*,
@@ -52,7 +53,6 @@ use leptos::{
use std::{
borrow::Cow,
cell::{Cell, RefCell},
collections::HashMap,
fmt::Debug,
rc::Rc,
};
@@ -99,7 +99,7 @@ pub struct MetaTagsContext {
#[allow(clippy::type_complexity)]
els: Rc<
RefCell<
HashMap<
IndexMap<
Cow<'static, str>,
(HtmlElement<AnyElement>, Scope, Option<web_sys::Element>),
>,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.3.0-alpha"
version = "0.3.0"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -28,6 +28,7 @@ js-sys = { version = "0.3" }
wasm-bindgen = { version = "0.2" }
wasm-bindgen-futures = { version = "0.4" }
lru = { version = "0.10", optional = true }
serde_json = "1.0.96"
[dependencies.web-sys]
version = "0.3"
@@ -59,7 +60,7 @@ default = []
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr", "dep:cached", "dep:lru", "dep:url", "dep:regex"]
nightly = ["leptos/nightly"]
stable = ["leptos/stable"]
[package.metadata.cargo-all-features]
# No need to test optional dependencies as they are enabled by the ssr feature

View File

@@ -1,5 +1,6 @@
use crate::{use_navigate, use_resolved_path, ToHref, Url};
use leptos::{html::form, *};
use serde::{de::DeserializeOwned, Serialize};
use std::{error::Error, rc::Rc};
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
@@ -240,7 +241,7 @@ pub fn ActionForm<I, O>(
) -> impl IntoView
where
I: Clone + ServerFn + 'static,
O: Clone + Serializable + 'static,
O: Clone + Serialize + DeserializeOwned + 'static,
{
let action_url = if let Some(url) = action.url() {
url
@@ -268,7 +269,6 @@ where
let on_response = Rc::new(move |resp: &web_sys::Response| {
let resp = resp.clone().expect("couldn't get Response");
let status = resp.status();
spawn_local(async move {
let redirected = resp.redirected();
@@ -277,23 +277,33 @@ where
resp.text().expect("couldn't get .text() from Response"),
)
.await;
let status = resp.status();
match body {
Ok(json) => {
// 500 just returns text of error, not JSON
if status == 500 {
let err = ServerFnError::ServerError(
json.as_string().unwrap_or_default(),
);
if let Some(error) = error {
error.try_set(Some(Box::new(err.clone())));
let json = json
.as_string()
.expect("couldn't get String from JsString");
if (500..=599).contains(&status) {
match serde_json::from_str::<ServerFnError>(&json) {
Ok(res) => {
value.try_set(Some(Err(res)));
if let Some(error) = error {
error.try_set(None);
}
}
Err(e) => {
value.try_set(Some(Err(
ServerFnError::Deserialization(
e.to_string(),
),
)));
if let Some(error) = error {
error.try_set(Some(Box::new(e)));
}
}
}
value.try_set(Some(Err(err)));
} else {
match O::de(
&json.as_string().expect(
"couldn't get String from JsString",
),
) {
match serde_json::from_str::<O>(&json) {
Ok(res) => {
value.try_set(Some(Ok(res)));
if let Some(error) = error {

View File

@@ -135,14 +135,14 @@ impl History for BrowserIntegration {
/// The wrapper type that the [Router](crate::Router) uses to interact with a [History].
/// This is automatically provided in the browser. For the server, it should be provided
/// as a context.
/// as a context. Be sure that it can survive conversion to a URL in the browser.
///
/// ```
/// # use leptos_router::*;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// let integration = ServerIntegration {
/// path: "insert/current/path/here".to_string(),
/// path: "http://leptos.rs/".to_string(),
/// };
/// provide_context(cx, RouterIntegrationContext::new(integration));
/// # });
@@ -167,7 +167,24 @@ impl History for RouterIntegrationContext {
}
}
/// A generic router integration for the server side. All its need is the current path.
/// A generic router integration for the server side.
///
/// This should match what the browser history will show.
///
/// Generally, this will already be provided if you are using the leptos
/// server integrations.
///
/// ```
/// # use leptos_router::*;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// let integration = ServerIntegration {
/// // Swap out with your URL if integrating manually.
/// path: "http://leptos.rs/".to_string(),
/// };
/// provide_context(cx, RouterIntegrationContext::new(integration));
/// # });
/// ```
#[derive(Clone, Debug)]
pub struct ServerIntegration {
pub path: String,

View File

@@ -140,7 +140,7 @@ where
}
cfg_if::cfg_if! {
if #[cfg(feature = "nightly")] {
if #[cfg(not(feature = "stable"))] {
auto trait NotOption {}
impl<T> !NotOption for Option<T> {}

View File

@@ -36,7 +36,7 @@ pub fn use_params_map(cx: Scope) -> Memo<ParamsMap> {
/// Returns the current route params, parsed into the given type, or an error.
pub fn use_params<T: Params>(cx: Scope) -> Memo<Result<T, ParamsError>>
where
T: PartialEq + std::fmt::Debug,
T: PartialEq,
{
let route = use_route(cx);
create_memo(cx, move |_| route.params().with(T::from_map))
@@ -50,7 +50,7 @@ pub fn use_query_map(cx: Scope) -> Memo<ParamsMap> {
/// Returns the current URL search query, parsed into the given type, or an error.
pub fn use_query<T: Params>(cx: Scope) -> Memo<Result<T, ParamsError>>
where
T: PartialEq + std::fmt::Debug,
T: PartialEq,
{
let router = use_router(cx);
create_memo(cx, move |_| {

View File

@@ -183,9 +183,9 @@
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
#![cfg_attr(feature = "nightly", feature(auto_traits))]
#![cfg_attr(feature = "nightly", feature(negative_impls))]
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
#![cfg_attr(not(feature = "stable"), feature(auto_traits))]
#![cfg_attr(not(feature = "stable"), feature(negative_impls))]
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
mod animation;
mod components;

View File

@@ -8,11 +8,14 @@
/// 2. **Out-of-order streaming**: Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `Suspense` nodes.
/// - *Pros*: Combines the best of **synchronous** and **`async`**, with a very fast shell and resources that begin loading on the server.
/// - *Cons*: Requires JS for suspended fragments to appear in correct order. Weaker meta tag support when it depends on data that's under suspense (has already streamed down `<head>`)
/// 3. **In-order streaming**: Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
/// 3. **Partially-blocked out-of-order streaming**: Using `create_blocking_resource` with out-of-order streaming still sends fallbacks and relies on JavaScript to fill them in with the fragments. Partially-blocked streaming does this replacement on the server, making for a slower response but requiring no JavaScript to show blocking resources.
/// - *Pros*: Works better if JS is disabled.
/// - *Cons*: Slower initial response because of additional string manipulation on server.
/// 4. **In-order streaming**: Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
/// - *Pros*: Does not require JS for HTML to appear in correct order.
/// - *Cons*: Loads the shell more slowly than out-of-order streaming or synchronous rendering because it needs to pause at every `Suspense`. Cannot begin hydration until the entire page has loaded, so earlier pieces
/// of the page will not be interactive until the suspended chunks have loaded.
/// 4. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
/// 5. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
/// - *Pros*: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
/// - *Cons*: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
///
@@ -23,6 +26,7 @@
pub enum SsrMode {
#[default]
OutOfOrder,
PartiallyBlocked,
InOrder,
Async,
}

View File

@@ -34,4 +34,4 @@ default = ["default-tls"]
default-tls = ["reqwest/default-tls"]
rustls = ["reqwest/rustls-tls"]
ssr = []
nightly = ["server_fn_macro_default/nightly"]
stable = ["server_fn_macro_default/stable"]

View File

@@ -19,4 +19,4 @@ server_fn = { version = "0.2" }
serde = "1"
[features]
nightly = ["server_fn_macro/nightly"]
stable = ["server_fn_macro/stable"]

View File

@@ -1,4 +1,4 @@
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
//! This crate contains the default implementation of the #[macro@crate::server] macro without a context from the server. See the [server_fn_macro] crate for more information.
#![forbid(unsafe_code)]

View File

@@ -18,4 +18,4 @@ xxhash-rust = { version = "0.8.6", features = ["const_xxh64"] }
const_format = "0.2.30"
[features]
nightly = []
stable = []

View File

@@ -1,4 +1,4 @@
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
//! Implementation of the server_fn macro.