Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
0821de8a3a invert stable and nightly features 2023-05-05 17:27:49 -04:00
62 changed files with 141 additions and 806 deletions

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

View File

@@ -34,10 +34,9 @@
- [`cargo-leptos`](./ssr/21_cargo_leptos.md)
- [The Life of a Page Load](./ssr/22_life_cycle.md)
- [Async Rendering and SSR “Modes”](./ssr/23_ssr_modes.md)
- [Hydration Bugs](./ssr/24_hydration_bugs.md)
- [Working with the Server](./server/README.md)
- [Server Functions](./server/25_server_functions.md)
- [Request/Response]()
- [Hydration Footguns](./ssr/24_hydration_bugs.md)
- [Server Functions]()
- [Request/Response]()
- [Extractors]()
- [Axum]()
- [Actix]()
@@ -47,6 +46,6 @@
- [Actions]()
- [Forms]()
- [`<ActionForm/>`s]()
- [Turning off WebAssembly: Progressive Enhancement and Graceful Degradation]()
- [Turning off WebAssembly]()
- [Advanced Reactivity]()
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)

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.
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.
subset of what web applications do. In particular, most web apps have to deal with some kind
of asynchronous data loading, usually loading something from an API.
Asynchronous data is notoriously hard to integrate with the synchronous parts of your code.
In this chapter, well see how Leptos helps smooth out that process for you.

View File

@@ -1,125 +0,0 @@
# 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

@@ -1,11 +0,0 @@
# Working with the Server
The previous section described the process of server-side rendering, using the server to generate an HTML version of the page that will become interactive in the browser. So far, everything has been “isomorphic” or “universal”; in other words, your app has had the “same (_iso_) shape (_morphe_)” on the client and the server.
But a server can do a lot more than just render HTML! In fact, a server can do a whole bunch of things your browser _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/cargo-leptos/blob/main/README.md).
`cargo-leptos` has lots of additional features and built in tools. You can learn more [in its `README`](https://github.com/leptos-rs/leptos/blob/main/examples/hackernews/src/api.rs).
But what exactly is happening when you open our browser to `localhost:3000`? Well, read on to find out.

View File

@@ -1,10 +1,10 @@
# `view`: Dynamic Classes, Styles and Attributes
# `view`: Dynamic Attributes and Classes
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 classes, styles and attributes dynamically,
In this section, well look at how to update attributes and classes 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,42 +52,12 @@ reactively update when the signal changes.
Now every time I click the button, the text should toggle between red and black as
the number switches between even and odd.
Some CSS class names 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

@@ -1,5 +1,5 @@
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
[env]
CARGO_MAKE_CLIPPY_ARGS = "--all-targets -- -D warnings"
[tasks.check-style]
description = "Check for style violations"

View File

@@ -41,7 +41,7 @@ ssr = [
"leptos_meta/ssr",
"leptos_router/ssr",
]
stable = ["leptos/stable", "leptos_router/stable"]
nightly = ["leptos/nightly", "leptos_router/nightly"]
[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;
let addr = conf.leptos_options.site_addr.clone();
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
HttpServer::new(move || {
@@ -48,7 +48,7 @@ cfg_if! {
.service(counter_events)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <Counters/> })
.service(Files::new("/", site_root))
.service(Files::new("/", &site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?

View File

@@ -11,8 +11,8 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, view};
use crate::landing::App;
use leptos::{LeptosOptions, Errors, view};
use crate::landing::{App, AppProps};
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;

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;
let addr = conf.leptos_options.site_addr.clone();
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> });
@@ -37,7 +37,7 @@ cfg_if! {
.service(favicon)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <App/> })
.service(Files::new("/", site_root))
.service(Files::new("/", &site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?

View File

@@ -1,4 +1,4 @@
use leptos::{Scope, Serializable};
use leptos::{on_cleanup, Scope, Serializable};
use serde::{Deserialize, Serialize};
pub fn story(path: &str) -> String {
@@ -29,7 +29,7 @@ where
// abort in-flight requests if the Scope is disposed
// i.e., if we've navigated away from this page
leptos::on_cleanup(cx, move || {
on_cleanup(cx, move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
@@ -38,7 +38,7 @@ where
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(_cx: Scope, path: &str) -> Option<T>
pub async fn fetch_api<T>(cx: Scope, path: &str) -> Option<T>
where
T: Serializable,
{

View File

@@ -19,7 +19,7 @@ if #[cfg(feature = "ssr")] {
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let addr = leptos_options.site_addr.clone();
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");

View File

@@ -13,7 +13,7 @@ if #[cfg(feature = "ssr")] {
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::ErrorTemplate;
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {

View File

@@ -5,7 +5,7 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
response::{Response, IntoResponse},
routing::get,
routing::{post, get},
extract::{Path, Extension, RawQuery},
http::{Request, header::HeaderMap},
body::Body as AxumBody,
@@ -16,7 +16,7 @@ if #[cfg(feature = "ssr")] {
use session_auth_axum::*;
use session_auth_axum::fallback::file_and_error_handler;
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
use leptos::{log, view, provide_context, LeptosOptions, get_configuration};
use leptos::{log, view, provide_context, LeptosOptions, get_configuration, ServerFnError};
use std::sync::Arc;
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
use axum_database_sessions::{SessionConfig, SessionLayer, SessionStore};

View File

@@ -20,15 +20,15 @@ if #[cfg(feature = "ssr")] {
use sqlx::SqlitePool;
pub fn pool(cx: Scope) -> Result<SqlitePool, ServerFnError> {
use_context::<SqlitePool>(cx)
Ok(use_context::<SqlitePool>(cx)
.ok_or("Pool missing.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))
.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
}
pub fn auth(cx: Scope) -> Result<AuthSession, ServerFnError> {
use_context::<AuthSession>(cx)
Ok(use_context::<AuthSession>(cx)
.ok_or("Auth session missing.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))
.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
}
pub fn register_server_functions() {

View File

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

View File

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

View File

@@ -11,8 +11,8 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, view};
use crate::app::App;
use leptos::{LeptosOptions, Errors, view};
use crate::app::{App, AppProps};
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;

View File

@@ -1,7 +1,11 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{extract::Extension, routing::post, Router};
use axum::{
extract::{Extension, Path},
routing::{get, post},
Router,
};
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
@@ -13,8 +17,8 @@ async fn main() {
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
let _ = GetPost::register();
let _ = ListPostMetadata::register();
GetPost::register();
ListPostMetadata::register();
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))

View File

@@ -1,5 +1,3 @@
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;
let addr = conf.leptos_options.site_addr.clone();
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> });
@@ -32,7 +32,7 @@ cfg_if! {
App::new()
.service(css)
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <App/> })
.service(Files::new("/", site_root))
.service(Files::new("/", &site_root))
.wrap(middleware::Compress::default())
})
.bind(&addr)?

View File

@@ -13,7 +13,7 @@ if #[cfg(feature = "ssr")] {
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::ErrorTemplate;
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {

View File

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

View File

@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use crate::{
error_template::ErrorTemplate,
error_template::{ErrorTemplate, ErrorTemplateProps},
errors::TodoAppError,
};
use http::Uri;
@@ -22,7 +22,7 @@ if #[cfg(feature = "ssr")] {
Error::Responder(Response::text("missing state type LeptosOptions")),
)?;
let root = &options.site_root;
let resp = get_static_file(uri, root, headers, route_info).await?;
let resp = get_static_file(uri, &root, headers, route_info).await?;
let status = resp.status();
if status.is_success() || status.is_redirection() {

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

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

View File

@@ -165,7 +165,7 @@ pub use leptos_dom::{
},
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
IntoClass, IntoProperty, IntoView, NodeRef, Property, View,
};
pub use leptos_macro::*;
pub use leptos_reactive::*;

View File

@@ -1,11 +1,7 @@
use cfg_if::cfg_if;
use leptos_dom::{DynChild, Fragment, HydrationCtx, IntoView};
use leptos_macro::component;
#[cfg(any(feature = "csr", feature = "hydrate"))]
use leptos_reactive::ScopeDisposer;
use leptos_reactive::{provide_context, Scope, SuspenseContext};
#[cfg(any(feature = "csr", feature = "hydrate"))]
use std::cell::RefCell;
use std::rc::Rc;
/// If any [Resources](leptos_reactive::Resource) are read in the `children` of this
@@ -76,8 +72,6 @@ where
let before_me = HydrationCtx::peek();
let current_id = HydrationCtx::next_component();
#[cfg(any(feature = "csr", feature = "hydrate"))]
let prev_disposer = Rc::new(RefCell::new(None::<ScopeDisposer>));
let child = DynChild::new({
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
@@ -85,18 +79,11 @@ where
move || {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
if let Some(disposer) = prev_disposer.take() {
disposer.dispose();
}
let (view, disposer) =
cx.run_child_scope(|cx| if context.ready() {
if context.ready() {
Fragment::lazy(Box::new(|| vec![orig_child(cx).into_view(cx)])).into_view(cx)
} else {
Fragment::lazy(Box::new(|| vec![fallback().into_view(cx)])).into_view(cx)
});
*prev_disposer.borrow_mut() = Some(disposer);
view
}
} else {
use leptos_reactive::signal_prelude::*;

View File

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

View File

@@ -63,7 +63,7 @@ cfg_if! {
use crate::{
ev::EventDescriptor,
hydration::HydrationCtx,
macro_helpers::{IntoAttribute, IntoClass, IntoProperty, IntoStyle},
macro_helpers::{IntoAttribute, IntoClass, IntoProperty},
Element, Fragment, IntoView, NodeRef, Text, View,
};
use leptos_reactive::Scope;
@@ -824,69 +824,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
}
/// Sets a style on an element.
///
/// **Note**: In the builder syntax, this will be overwritten by the `style`
/// attribute if you use `.attr("class", /* */)`. In the `view` macro, they
/// are automatically re-ordered so that this over-writing does not happen.
#[track_caller]
pub fn style(
self,
name: impl Into<Cow<'static, str>>,
style: impl IntoStyle,
) -> Self {
let name = name.into();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
let el = self.element.as_ref();
let value = style.into_style(self.cx);
style_helper(el, name, value);
self
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
use crate::macro_helpers::Style;
let mut this = self;
let style = style.into_style(this.cx);
let include = match style {
Style::Value(value) => Some(value),
Style::Option(value) => value,
Style::Fn(_, f) => {
let mut value = f();
while let Style::Fn(_, f) = value {
value = f();
}
match value {
Style::Value(value) => Some(value),
Style::Option(value) => value,
_ => unreachable!(),
}
}
};
if let Some(style_value) = include {
if let Some((_, ref mut value)) =
this.attrs.iter_mut().find(|(name, _)| name == "style")
{
*value = format!("{value} {name}: {style_value};").into();
} else {
this.attrs.push((
"style".into(),
format!("{name}: {style_value};").into(),
));
}
}
this
}
}
/// Sets a property on an element.
#[track_caller]
pub fn prop(

View File

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

View File

@@ -1,209 +0,0 @@
use leptos_reactive::Scope;
use std::{borrow::Cow, rc::Rc};
/// todo docs
#[derive(Clone)]
pub enum Style {
/// A plain string value.
Value(Cow<'static, str>),
/// An optional string value, which sets the property to the value if `Some` and removes the property if `None`.
Option(Option<Cow<'static, str>>),
/// A (presumably reactive) function, which will be run inside an effect to update the style.
Fn(Scope, Rc<dyn Fn() -> Style>),
}
impl PartialEq for Style {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Value(l0), Self::Value(r0)) => l0 == r0,
(Self::Fn(_, _), Self::Fn(_, _)) => false,
(Self::Option(l0), Self::Option(r0)) => l0 == r0,
_ => false,
}
}
}
impl std::fmt::Debug for Style {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Value(arg0) => f.debug_tuple("Value").field(arg0).finish(),
Self::Fn(_, _) => f.debug_tuple("Fn").finish(),
Self::Option(arg0) => f.debug_tuple("Option").field(arg0).finish(),
}
}
}
/// Converts some type into a [Style].
pub trait IntoStyle {
/// Converts the object into a [Style].
fn into_style(self, cx: Scope) -> Style;
}
impl IntoStyle for &'static str {
#[inline(always)]
fn into_style(self, _cx: Scope) -> Style {
Style::Value(self.into())
}
}
impl IntoStyle for String {
#[inline(always)]
fn into_style(self, _cx: Scope) -> Style {
Style::Value(self.into())
}
}
impl IntoStyle for Option<&'static str> {
#[inline(always)]
fn into_style(self, _cx: Scope) -> Style {
Style::Option(self.map(Cow::Borrowed))
}
}
impl IntoStyle for Option<String> {
#[inline(always)]
fn into_style(self, _cx: Scope) -> Style {
Style::Option(self.map(Cow::Owned))
}
}
impl<T, U> IntoStyle for T
where
T: Fn() -> U + 'static,
U: IntoStyle,
{
#[inline(always)]
fn into_style(self, cx: Scope) -> Style {
let modified_fn = Rc::new(move || (self)().into_style(cx));
Style::Fn(cx, modified_fn)
}
}
impl Style {
/// Converts the style to its HTML value at that moment so it can be rendered on the server.
pub fn as_value_string(
&self,
style_name: &'static str,
) -> Option<Cow<'static, str>> {
match self {
Style::Value(value) => {
Some(format!("{style_name}: {value};").into())
}
Style::Option(value) => value
.as_ref()
.map(|value| format!("{style_name}: {value};").into()),
Style::Fn(_, f) => {
let mut value = f();
while let Style::Fn(_, f) = value {
value = f();
}
value.as_value_string(style_name)
}
}
}
}
impl<T: IntoStyle> IntoStyle for (Scope, T) {
#[inline(always)]
fn into_style(self, _: Scope) -> Style {
self.1.into_style(self.0)
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]
pub fn style_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Style,
) {
use leptos_reactive::create_render_effect;
use wasm_bindgen::JsCast;
let el = el.unchecked_ref::<web_sys::HtmlElement>();
let style_list = el.style();
match value {
Style::Fn(cx, f) => {
create_render_effect(cx, move |old| {
let mut new = f();
while let Style::Fn(_, f) = new {
new = f();
}
let new = match new {
Style::Value(value) => Some(value),
Style::Option(value) => value,
_ => unreachable!(),
};
if old.as_ref() != Some(&new) {
style_expression(&style_list, &name, new.as_ref(), true)
}
new
});
}
Style::Value(value) => {
style_expression(&style_list, &name, Some(&value), false)
}
Style::Option(value) => {
style_expression(&style_list, &name, value.as_ref(), false)
}
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn style_expression(
style_list: &web_sys::CssStyleDeclaration,
style_name: &str,
value: Option<&Cow<'static, str>>,
force: bool,
) {
use crate::HydrationCtx;
if force || !HydrationCtx::is_hydrating() {
let style_name = wasm_bindgen::intern(style_name);
if let Some(value) = value {
if let Err(e) = style_list.set_property(style_name, &value) {
crate::error!("[HtmlElement::style()] {e:?}");
}
} else {
if let Err(e) = style_list.remove_property(style_name) {
crate::error!("[HtmlElement::style()] {e:?}");
}
}
}
}
macro_rules! style_type {
($style_type:ty) => {
impl IntoStyle for $style_type {
fn into_style(self, _: Scope) -> Style {
Style::Value(self.to_string().into())
}
}
impl IntoStyle for Option<$style_type> {
fn into_style(self, _: Scope) -> Style {
Style::Option(self.map(|n| n.to_string().into()))
}
}
};
}
style_type!(&String);
style_type!(usize);
style_type!(u8);
style_type!(u16);
style_type!(u32);
style_type!(u64);
style_type!(u128);
style_type!(isize);
style_type!(i8);
style_type!(i16);
style_type!(i32);
style_type!(i64);
style_type!(i128);
style_type!(f32);
style_type!(f64);
style_type!(char);

View File

@@ -1,8 +1,6 @@
mod into_attribute;
mod into_class;
mod into_property;
mod into_style;
pub use into_attribute::*;
pub use into_class::*;
pub use into_property::*;
pub use into_style::*;

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
#![forbid(unsafe_code)]
#[macro_use]
@@ -201,29 +201,7 @@ mod template;
/// # });
/// ```
///
/// 8. Individual styles can also be set with `style:` or `style=("property-name", value)` syntax.
/// ```rust
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (x, set_x) = create_signal(cx, 0);
/// let (y, set_y) = create_signal(cx, 0);
/// view! { cx,
/// <div
/// style="position: absolute"
/// style:left=move || format!("{}px", x())
/// style:top=move || format!("{}px", y())
/// style=("background-color", move || format!("rgb({}, {}, 100)", x(), y()))
/// >
/// "Moves when coordinates change"
/// </div>
/// }
/// # ;
/// # }
/// # });
/// ```
///
/// 9. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
/// 8. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
/// [NodeRef](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to use later.
/// ```rust
/// # use leptos::*;
@@ -240,7 +218,7 @@ mod template;
/// # });
/// ```
///
/// 10. You can add the same class to every element in the view by passing in a special
/// 9. You can add the same class to every element in the view by passing in a special
/// `class = {/* ... */},` argument after `cx, `. This is useful for injecting a class
/// provided by a scoped styling library.
/// ```rust
@@ -258,7 +236,7 @@ mod template;
/// # });
/// ```
///
/// 11. You can set any HTML elements `innerHTML` with the `inner_html` attribute on an
/// 10. 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
@@ -375,7 +353,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, not(feature = "stable")))] {
if #[cfg(all(debug_assertions, feature = "nightly"))] {
Some(leptos_hot_reload::span_to_stable_id(
site.source_file().path(),
site.into()

View File

@@ -451,7 +451,6 @@ 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("/>");
@@ -556,10 +555,9 @@ 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 and sdtyles: we'll handle these separately
// ignore classes: we'll handle these separately
} else if name == "inner_html" {
return node.value.as_ref();
} else {
@@ -577,7 +575,7 @@ fn attribute_to_tokens_ssr<'a>(
for more information and an example: https://github.com/leptos-rs/leptos/issues/773")
};
if name != "class" && name != "style" {
if name != "class" {
template.push(' ');
if let Some(value) = node.value.as_ref() {
@@ -736,113 +734,6 @@ 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,
@@ -1025,11 +916,8 @@ fn element_to_tokens(
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
let name = node.key.to_string();
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()
if name.trim().starts_with("class:")
|| fancy_class_name(&name, cx, node).is_some()
{
None
} else {
@@ -1053,20 +941,6 @@ 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) => {
@@ -1160,7 +1034,6 @@ fn element_to_tokens(
#name
#(#attrs)*
#(#class_attrs)*
#(#style_attrs)*
#global_class_expr
#(#children)*
#view_marker
@@ -1276,21 +1149,6 @@ 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);
@@ -1942,51 +1800,3 @@ 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,15 +68,16 @@ hydrate = [
"dep:web-sys",
]
ssr = ["dep:tokio"]
stable = []
nightly = []
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(not(feature = "stable"), feature(fn_traits))]
#![cfg_attr(not(feature = "stable"), feature(unboxed_closures))]
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
#![cfg_attr(feature = "nightly", feature(fn_traits))]
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
#![cfg_attr(feature = "nightly", 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 + Clone + 'static,
S: PartialEq + Debug + 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 + Clone + 'static,
S: PartialEq + Debug + 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 + Clone + 'static,
S: PartialEq + Debug + 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 + Clone + 'static,
S: PartialEq + Debug + 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 + Clone + 'static,
S: PartialEq + Debug + 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 + Clone + 'static,
S: PartialEq + Debug + 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 + Clone + 'static,
S: PartialEq + Debug + 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 + Clone + 'static,
S: PartialEq + Debug + Clone + 'static,
T: Serializable + 'static,
{
use wasm_bindgen::{JsCast, UnwrapThrowExt};

View File

@@ -3,7 +3,9 @@ use crate::{
create_isomorphic_effect, create_signal, ReadSignal, Scope, SignalUpdate,
WriteSignal,
};
use std::{cell::RefCell, collections::HashMap, hash::Hash, rc::Rc};
use std::{
cell::RefCell, collections::HashMap, fmt::Debug, 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
@@ -50,7 +52,7 @@ pub fn create_selector<T>(
source: impl Fn() -> T + Clone + 'static,
) -> impl Fn(T) -> bool + Clone
where
T: PartialEq + Eq + Clone + Hash + 'static,
T: PartialEq + Eq + Debug + Clone + Hash + 'static,
{
create_selector_with_fn(cx, source, PartialEq::eq)
}
@@ -67,7 +69,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 + Clone + Hash + 'static,
T: PartialEq + Eq + Debug + 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(not(feature = "stable"))]
#[cfg(feature = "nightly")]
impl<T: Clone> FnOnce<()> for $ty<T> {
type Output = T;
@@ -27,7 +27,7 @@ macro_rules! impl_get_fn_traits {
}
}
#[cfg(not(feature = "stable"))]
#[cfg(feature = "nightly")]
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(not(feature = "stable"))]
#[cfg(feature = "nightly")]
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(not(feature = "stable"))]
#[cfg(feature = "nightly")]
impl<T> FnOnce<(T,)> for $ty<T> {
type Output = ();
@@ -65,7 +65,7 @@ macro_rules! impl_set_fn_traits {
}
}
#[cfg(not(feature = "stable"))]
#[cfg(feature = "nightly")]
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(not(feature = "stable"))]
#[cfg(feature = "nightly")]
impl<T> Fn<(T,)> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call(&self, args: (T,)) -> Self::Output {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
#[cfg(not(feature = "stable"))]
#[cfg(feature = "nightly")]
use leptos_reactive::{
create_isomorphic_effect, create_runtime, create_scope, create_signal,
signal_prelude::*, SignalGetUntracked, SignalSetUntracked,
};
#[cfg(not(feature = "stable"))]
#[cfg(feature = "nightly")]
#[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(not(feature = "stable"))]
#[cfg(feature = "nightly")]
#[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"]
stable = ["leptos_reactive/stable", "server_fn/stable"]
nightly = ["leptos_reactive/nightly", "server_fn/nightly"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View File

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

View File

@@ -45,7 +45,6 @@
//! which mode your app is operating in.
use cfg_if::cfg_if;
use indexmap::IndexMap;
use leptos::{
leptos_dom::{debug_warn, html::AnyElement},
*,
@@ -53,6 +52,7 @@ 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<
IndexMap<
HashMap<
Cow<'static, str>,
(HtmlElement<AnyElement>, Scope, Option<web_sys::Element>),
>,

View File

@@ -59,7 +59,7 @@ default = []
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr", "dep:cached", "dep:lru", "dep:url", "dep:regex"]
stable = ["leptos/stable"]
nightly = ["leptos/nightly"]
[package.metadata.cargo-all-features]
# No need to test optional dependencies as they are enabled by the ssr feature

View File

@@ -140,7 +140,7 @@ where
}
cfg_if::cfg_if! {
if #[cfg(not(feature = "stable"))] {
if #[cfg(feature = "nightly")] {
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,
T: PartialEq + std::fmt::Debug,
{
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,
T: PartialEq + std::fmt::Debug,
{
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(not(feature = "stable"), feature(auto_traits))]
#![cfg_attr(not(feature = "stable"), feature(negative_impls))]
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
#![cfg_attr(feature = "nightly", feature(auto_traits))]
#![cfg_attr(feature = "nightly", feature(negative_impls))]
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
mod animation;
mod components;

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
#![cfg_attr(feature = "nightly", 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]
stable = []
nightly = []

View File

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