mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 10:11:56 -05:00
Compare commits
10 Commits
server-fn-
...
optional-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95eb6b1bd9 | ||
|
|
e81f14a794 | ||
|
|
0e8125abde | ||
|
|
8bb2ee4569 | ||
|
|
5dab35447a | ||
|
|
63be819533 | ||
|
|
af8afb1204 | ||
|
|
2170be8e01 | ||
|
|
1187a506dd | ||
|
|
ff5ceddbe2 |
2
.github/workflows/check-stable.yml
vendored
2
.github/workflows/check-stable.yml
vendored
@@ -42,4 +42,4 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all examples
|
||||
run: cargo make check-stable
|
||||
run: cargo make --profile=github-actions check-stable
|
||||
|
||||
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
@@ -42,4 +42,4 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all libraries
|
||||
run: cargo make check
|
||||
run: cargo make --profile=github-actions check
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -42,4 +42,4 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run tests with all features
|
||||
run: cargo make test
|
||||
run: cargo make --profile=github-actions test
|
||||
|
||||
@@ -74,3 +74,9 @@ dependencies = ["test-all"]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[env]
|
||||
RUSTFLAGS=""
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS="-D warnings"
|
||||
29
README.md
29
README.md
@@ -24,8 +24,7 @@ pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
// create user interfaces with the declarative `view!` macro
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<button on:click=clear>"Clear"</button>
|
||||
<button on:click=decrement>"-1"</button>
|
||||
@@ -48,11 +47,11 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
|
||||
|
||||
## What does that mean?
|
||||
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (_client-side rendering_), on the server (_server-side rendering_), or by rendering HTML on the server and then adding interactivity in the browser (_hydration_). This includes support for _HTTP streaming_ of both data (`Resource`s) and HTML (out-of-order streaming of `<Suspense/>` components.)
|
||||
- **Isomorphic**: Leptos provides primitives to write isomorphic server functions, i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The router is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) components.)
|
||||
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The [router](https://docs.rs/leptos_router/latest/leptos_router/) is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
|
||||
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signal’s value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (_So, no virtual DOM!_)
|
||||
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signal’s value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (So, no virtual DOM overhead!)
|
||||
- **Declarative**: Tell Leptos how you want the page to look, and let the framework tell the browser how to do it.
|
||||
|
||||
## Learn more
|
||||
@@ -86,7 +85,7 @@ If you’re on `stable`, note the following:
|
||||
|
||||
## `cargo-leptos`
|
||||
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our [starter template](https://github.com/leptos-rs/start).
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
@@ -95,13 +94,13 @@ cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
Open browser to [http://localhost:3000/](http://localhost:3000/).
|
||||
|
||||
## FAQs
|
||||
|
||||
### What’s up with the name?
|
||||
|
||||
*Leptos* (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
_Leptos_ (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
|
||||
### Is it production ready?
|
||||
|
||||
@@ -109,7 +108,7 @@ People usually mean one of three things by this question.
|
||||
|
||||
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
|
||||
|
||||
With 0.1 the APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to, for example, a 0.2.0 release.
|
||||
The APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases. The sorts of breaking changes that we discuss are things like “Oh yeah, that function should probably take `cx` as its argument...” not major changes to the way you write your application.
|
||||
|
||||
2. **Are there bugs?**
|
||||
|
||||
@@ -119,7 +118,7 @@ Yes, I’m sure there are. You can see from the state of our issue tracker over
|
||||
|
||||
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) don’t have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
|
||||
|
||||
There are several people in this community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
There are several people in the community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
|
||||
### Can I use this for native GUI?
|
||||
|
||||
@@ -137,8 +136,8 @@ I've put together a [very simple GTK example](https://github.com/leptos-rs/lepto
|
||||
On the surface level, these libraries may seem similar. Yew is, of course, the most mature Rust library for web UI development and has a huge ecosystem. Dioxus is similar in many ways, being heavily inspired by React. Here are some conceptual differences between Leptos and these frameworks:
|
||||
|
||||
- **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply _much_ faster at both creating and updating the UI than Yew is.
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. Your app can be divided into components based on what makes sense for your app, because they have no performance implications.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is. (Dioxus has made huge advances in performance with its recent 0.3 release, and is now roughly on par with Leptos.)
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. You can call functions, create timeouts, etc. within the body of your component functions because they won’t be re-run. You don’t need to think about manual dependency tracking for effects; fine-grained reactivity tracks dependencies automatically.
|
||||
|
||||
### How is this different from Sycamore?
|
||||
|
||||
@@ -146,9 +145,9 @@ Conceptually, these two frameworks are very similar: because both are built on f
|
||||
|
||||
There are some practical differences that make a significant difference:
|
||||
|
||||
- **Maturity:** Sycamore is obviously a much more mature and stable library with a larger ecosystem.
|
||||
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)_
|
||||
- **Server integration:** Leptos provides primitives that encourage HTML streaming and allow for easy async integration and RPC calls, even without WASM enabled, making it easy to opt into integrations between your frontend and backend code without pushing you toward any particular metaframework patterns.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
|
||||
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
|
||||
|
||||
```rust
|
||||
|
||||
@@ -17,15 +17,11 @@ lazy_static = "1"
|
||||
log = "0.4"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
serde = { version = "1", features = ["derive", "rc"]}
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1"
|
||||
tera = "1"
|
||||
reactive-signals = "0.1.0-alpha.4"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Window",
|
||||
"Document",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement"
|
||||
]
|
||||
features = ["Window", "Document", "HtmlElement", "HtmlInputElement"]
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
extern crate test;
|
||||
|
||||
mod reactive;
|
||||
//åmod reactive;
|
||||
//mod ssr;
|
||||
//mod todomvc;
|
||||
mod todomvc;
|
||||
|
||||
@@ -162,6 +162,77 @@ fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn rs_deep_update(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
|
||||
let sc = ClientRuntime::new_root_scope();
|
||||
b.iter(|| {
|
||||
let signal = signal!(sc, 0);
|
||||
let mut memos = Vec::<Signal<Func<i32>, ClientRuntime>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(signal!(sc, move || prev.get() + 1))
|
||||
} else {
|
||||
memos.push(signal!(sc, move || signal.get() + 1))
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn rs_fanning_out(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
let cx = ClientRuntime::new_root_scope();
|
||||
|
||||
b.iter(|| {
|
||||
let sig = signal!(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| signal!(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn rs_narrowing_update(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
let cx = ClientRuntime::new_root_scope();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
|
||||
let memo = signal!(cx, {
|
||||
let sigs = sigs.clone();
|
||||
move || {
|
||||
sigs.iter().map(|r| r.get()).sum::<i32>()
|
||||
}
|
||||
});
|
||||
assert_eq!(memo.get(), 499500);
|
||||
signal!(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move || {
|
||||
acc.set(memo.get());
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(acc.get(), 499500);
|
||||
|
||||
sigs[1].update(|n| *n += 1);
|
||||
sigs[10].update(|n| *n += 1);
|
||||
sigs[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo.get(), 499503);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_deep_creation(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
|
||||
@@ -4,7 +4,7 @@ use test::Bencher;
|
||||
fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
use leptos::*;
|
||||
HydrationCtx::reset_id();
|
||||
leptos_dom::HydrationCtx::reset_id();
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
#[component]
|
||||
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
|
||||
@@ -32,7 +32,8 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>" );
|
||||
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub use leptos::*;
|
||||
use miniserde::*;
|
||||
use web_sys::HtmlInputElement;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Todos(pub Vec<Todo>);
|
||||
@@ -110,10 +111,6 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
provide_context(cx, set_todos);
|
||||
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener("hashchange", move |_| {
|
||||
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
});
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
@@ -167,57 +164,79 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus="" on:keydown=add_todo />
|
||||
</header>
|
||||
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox"
|
||||
prop:checked={move || todos.with(|t| t.remaining() > 0)}
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |todo: Todo| view! { cx, <Todo todo=todo.clone() /> }
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 {
|
||||
" item"
|
||||
} else {
|
||||
" items"
|
||||
}}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
|
||||
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
|
||||
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden={move || todos.with(|t| t.completed() == 0)}
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}.into_view(cx)
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autofocus=""
|
||||
on:keydown=add_todo
|
||||
/>
|
||||
</header>
|
||||
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<input
|
||||
id="toggle-all"
|
||||
class="toggle-all"
|
||||
type="checkbox"
|
||||
prop:checked=move || todos.with(|t| t.remaining() > 0)
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |cx, todo: Todo| {
|
||||
view! { cx, <Todo todo=todo.clone()/> }
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 { " item" } else { " items" }}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li>
|
||||
<a
|
||||
href="#/"
|
||||
class="selected"
|
||||
class:selected=move || mode() == Mode::All
|
||||
>
|
||||
"All"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/active" class:selected=move || mode() == Mode::Active>
|
||||
"Active"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/completed" class:selected=move || mode() == Mode::Completed>
|
||||
"Completed"
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden=move || todos.with(|t| t.completed() == 0)
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by " <a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}.into_view(cx)
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -237,41 +256,36 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li
|
||||
class="todo"
|
||||
class:editing={editing}
|
||||
class:completed={move || (todo.completed)()}
|
||||
//_ref=input
|
||||
>
|
||||
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
|
||||
<div class="view">
|
||||
<input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked={move || (todo.completed)()}
|
||||
|
||||
/>
|
||||
<label on:dblclick=move |_| set_editing(true)>
|
||||
{move || todo.title.get()}
|
||||
</label>
|
||||
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
|
||||
<input class="toggle" type="checkbox" prop:checked=move || (todo.completed)()/>
|
||||
<label on:dblclick=move |_| set_editing(true)>{move || todo.title.get()}</label>
|
||||
<button
|
||||
class="destroy"
|
||||
on:click=move |_| set_todos.update(|t| t.remove(todo.id))
|
||||
></button>
|
||||
</div>
|
||||
{move || editing().then(|| view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden={move || !(editing)()}
|
||||
prop:value={move || todo.title.get()}
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup={move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
{move || {
|
||||
editing()
|
||||
.then(|| {
|
||||
view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden=move || !(editing)()
|
||||
prop:value=move || todo.title.get()
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup=move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,15 @@ mod yew;
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new(cx)/>
|
||||
}
|
||||
.into_view(cx)
|
||||
.render_to_string(cx);
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
let html = ::leptos::ssr::render_to_string(|cx| {
|
||||
view! { cx, <TodoMVC todos=Todos::new(cx)/> }
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,21 +53,20 @@ fn yew_todomvc_ssr(b: &mut Bencher) {
|
||||
});
|
||||
});
|
||||
}
|
||||
/*
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
use self::leptos::*;
|
||||
use ::leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
let html = ::leptos::ssr::render_to_string(|cx| {
|
||||
view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new_with_1000(cx)/>
|
||||
}.into_view(cx).render_to_string(cx);
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
}
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,5 +103,4 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
assert!(rendered.len() > 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -174,4 +174,4 @@ fn tera_todomvc_1000(b: &mut Bencher) {
|
||||
|
||||
let _ = TERA.render("template.html", &ctx).unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,9 @@ in as if it were an HTML element attribute. Simple.
|
||||
> sure you include this `ComponentProps` type:
|
||||
>
|
||||
> `use progress_bar::{ProgressBar, ProgressBarProps};`
|
||||
>
|
||||
> **Note**: This is still true as of `0.2.5`, but the requirement has been removed on `main`
|
||||
> and will not apply to later versions.
|
||||
|
||||
### Reactive and Static Props
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use counter::*;
|
||||
use counter::SimpleCounter;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use counters::{Counters, CountersProps};
|
||||
use counters::Counters;
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -14,5 +14,6 @@ leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
serde_json = "1"
|
||||
parking_lot = "0.12.1"
|
||||
regex = "1.7.0"
|
||||
|
||||
@@ -264,8 +264,10 @@ pub fn handle_server_fns_with_context(
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError()
|
||||
.body(e.to_string()),
|
||||
Err(e) => HttpResponse::InternalServerError().body(
|
||||
serde_json::to_string(&e)
|
||||
.unwrap_or_else(|_| e.to_string()),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
HttpResponse::BadRequest().body(format!(
|
||||
|
||||
@@ -16,5 +16,7 @@ leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
tokio-util = {version = "0.7.7", features = ["rt"] }
|
||||
@@ -32,8 +32,14 @@ use leptos_integration_utils::{build_async_response, html_parts_separated};
|
||||
use leptos_meta::{generate_head_metadata_separated, MetaContext};
|
||||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
use std::{io, pin::Pin, sync::Arc};
|
||||
use tokio::task::{spawn_blocking, LocalSet};
|
||||
use std::{
|
||||
io,
|
||||
pin::Pin,
|
||||
sync::{Arc, OnceLock},
|
||||
thread::available_parallelism,
|
||||
};
|
||||
use tokio::task::LocalSet;
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
|
||||
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
|
||||
/// to construct this for Leptos to use in Axum
|
||||
@@ -294,141 +300,116 @@ async fn handle_server_fns_inner(
|
||||
.unwrap_or(fn_name);
|
||||
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
spawn_blocking({
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
async move {
|
||||
let res = if let Some(server_fn) =
|
||||
server_fn_by_path(fn_name.as_str())
|
||||
let pool_handle = get_leptos_pool();
|
||||
pool_handle.spawn_pinned(move || {
|
||||
async move {
|
||||
let res = if let Some(server_fn) =
|
||||
server_fn_by_path(fn_name.as_str())
|
||||
{
|
||||
let runtime = create_runtime();
|
||||
let (cx, disposer) = raw_scope_and_disposer(runtime);
|
||||
|
||||
additional_context(cx);
|
||||
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await; // Add this so we can get details about the Request
|
||||
provide_context(cx, req_parts.clone());
|
||||
provide_context(cx, leptos_req);
|
||||
// Add this so that we can set headers and status of the response
|
||||
provide_context(cx, ResponseOptions::default());
|
||||
|
||||
let query: &Bytes = &query.unwrap_or("".to_string()).into();
|
||||
let data = match &server_fn.encoding {
|
||||
Encoding::Url | Encoding::Cbor => &req_parts.body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
};
|
||||
match (server_fn.trait_obj)(cx, data).await {
|
||||
Ok(serialized) => {
|
||||
// If ResponseOptions are set, add the headers and status to the request
|
||||
let res_options = use_context::<ResponseOptions>(cx);
|
||||
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
let accept_header = headers
|
||||
.get("Accept")
|
||||
.and_then(|value| value.to_str().ok());
|
||||
let mut res = Response::builder();
|
||||
|
||||
// Add headers from ResponseParts if they exist. These should be added as long
|
||||
// as the server function returns an OK response
|
||||
let res_options_outer = res_options.unwrap().0;
|
||||
let res_options_inner = res_options_outer.read();
|
||||
let (status, mut res_headers) = (
|
||||
res_options_inner.status,
|
||||
res_options_inner.headers.clone(),
|
||||
);
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
let runtime = create_runtime();
|
||||
let (cx, disposer) =
|
||||
raw_scope_and_disposer(runtime);
|
||||
|
||||
additional_context(cx);
|
||||
|
||||
let (req, req_parts) =
|
||||
generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await; // Add this so we can get details about the Request
|
||||
provide_context(cx, req_parts.clone());
|
||||
provide_context(cx, leptos_req);
|
||||
// Add this so that we can set headers and status of the response
|
||||
provide_context(cx, ResponseOptions::default());
|
||||
|
||||
let query: &Bytes =
|
||||
&query.unwrap_or("".to_string()).into();
|
||||
let data = match &server_fn.encoding {
|
||||
Encoding::Url | Encoding::Cbor => {
|
||||
&req_parts.body
|
||||
}
|
||||
Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
};
|
||||
match (server_fn.trait_obj)(cx, data).await {
|
||||
Ok(serialized) => {
|
||||
// If ResponseOptions are set, add the headers and status to the request
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx);
|
||||
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
let accept_header = headers
|
||||
.get("Accept")
|
||||
.and_then(|value| value.to_str().ok());
|
||||
let mut res = Response::builder();
|
||||
|
||||
// Add headers from ResponseParts if they exist. These should be added as long
|
||||
// as the server function returns an OK response
|
||||
let res_options_outer =
|
||||
res_options.unwrap().0;
|
||||
let res_options_inner =
|
||||
res_options_outer.read();
|
||||
let (status, mut res_headers) = (
|
||||
res_options_inner.status,
|
||||
res_options_inner.headers.clone(),
|
||||
);
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some(
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
)
|
||||
|| accept_header
|
||||
== Some("application/cbor")
|
||||
{
|
||||
res = res.status(StatusCode::OK);
|
||||
}
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
else {
|
||||
let referer = headers
|
||||
.get("Referer")
|
||||
.and_then(|value| {
|
||||
value.to_str().ok()
|
||||
})
|
||||
.unwrap_or("/");
|
||||
|
||||
res = res
|
||||
.status(StatusCode::SEE_OTHER)
|
||||
.header("Location", referer);
|
||||
}
|
||||
// Override StatusCode if it was set in a Resource or Element
|
||||
res = match status {
|
||||
Some(status) => res.status(status),
|
||||
None => res,
|
||||
};
|
||||
// This must be after the default referrer
|
||||
// redirect so that it overwrites the one above
|
||||
if let Some(header_ref) = res.headers_mut()
|
||||
{
|
||||
header_ref.extend(res_headers.drain());
|
||||
};
|
||||
match serialized {
|
||||
Payload::Binary(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/cbor",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
Payload::Url(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
Payload::Json(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/json",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
}
|
||||
}
|
||||
Err(e) => Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Full::from(e.to_string())),
|
||||
}
|
||||
} else {
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(Full::from(format!(
|
||||
"Could not find a server function at the \
|
||||
route {fn_name}. \n\nIt's likely that \
|
||||
you need to call ServerFn::register() on \
|
||||
the server function type, somewhere in \
|
||||
your `main` function."
|
||||
)))
|
||||
res = res.status(StatusCode::OK);
|
||||
}
|
||||
.expect("could not build Response");
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
else {
|
||||
let referer = headers
|
||||
.get("Referer")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("/");
|
||||
|
||||
_ = tx.send(res);
|
||||
res = res
|
||||
.status(StatusCode::SEE_OTHER)
|
||||
.header("Location", referer);
|
||||
}
|
||||
// Override StatusCode if it was set in a Resource or Element
|
||||
res = match status {
|
||||
Some(status) => res.status(status),
|
||||
None => res,
|
||||
};
|
||||
// This must be after the default referrer
|
||||
// redirect so that it overwrites the one above
|
||||
if let Some(header_ref) = res.headers_mut() {
|
||||
header_ref.extend(res_headers.drain());
|
||||
};
|
||||
match serialized {
|
||||
Payload::Binary(data) => res
|
||||
.header("Content-Type", "application/cbor")
|
||||
.body(Full::from(data)),
|
||||
Payload::Url(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/x-www-form-urlencoded",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
Payload::Json(data) => res
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Full::from(data)),
|
||||
}
|
||||
}
|
||||
})
|
||||
Err(e) => Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Full::from(
|
||||
serde_json::to_string(&e)
|
||||
.unwrap_or_else(|_| e.to_string()),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Response::builder().status(StatusCode::BAD_REQUEST).body(
|
||||
Full::from(format!(
|
||||
"Could not find a server function at the route \
|
||||
{fn_name}. \n\nIt's likely that you need to call \
|
||||
ServerFn::register() on the server function type, \
|
||||
somewhere in your `main` function."
|
||||
)),
|
||||
)
|
||||
}
|
||||
.expect("could not build Response");
|
||||
|
||||
_ = tx.send(res);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -632,56 +613,33 @@ where
|
||||
let default_res_options = ResponseOptions::default();
|
||||
let res_options2 = default_res_options.clone();
|
||||
let res_options3 = default_res_options.clone();
|
||||
let local_pool = get_leptos_pool();
|
||||
let (tx, rx) = futures::channel::mpsc::channel(8);
|
||||
local_pool.spawn_pinned(move || async move {
|
||||
let app = {
|
||||
// Need to get the path and query string of the Request
|
||||
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
|
||||
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
|
||||
let path = req.uri().path_and_query().unwrap().as_str();
|
||||
|
||||
async move {
|
||||
// Need to get the path and query string of the Request
|
||||
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
|
||||
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
|
||||
let path = req.uri().path_and_query().unwrap().as_str();
|
||||
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::channel(8);
|
||||
|
||||
spawn_blocking({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
async move {
|
||||
tokio::task::LocalSet::new()
|
||||
.run_until(async {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
});
|
||||
};
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
generate_response(res_options3, rx).await
|
||||
}
|
||||
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
|
||||
});
|
||||
async move { generate_response(res_options3, rx).await }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -811,42 +769,26 @@ where
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::channel(8);
|
||||
let local_pool = get_leptos_pool();
|
||||
local_pool.spawn_pinned(move || async move {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
spawn_blocking({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
async move {
|
||||
tokio::task::LocalSet::new()
|
||||
.run_until(async {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
|
||||
});
|
||||
|
||||
generate_response(res_options3, rx).await
|
||||
@@ -994,53 +936,39 @@ where
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
let local_pool = get_leptos_pool();
|
||||
local_pool.spawn_pinned(move || {
|
||||
async move {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
spawn_blocking({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
async move {
|
||||
tokio::task::LocalSet::new()
|
||||
.run_until(async {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
let (stream, runtime, scope) =
|
||||
render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|_| "".into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
let (stream, runtime, scope) =
|
||||
render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|_| "".into(),
|
||||
add_context,
|
||||
);
|
||||
// Extract the value of ResponseOptions from here
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
|
||||
// Extract the value of ResponseOptions from here
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
let html = build_async_response(stream, &options, runtime, scope).await;
|
||||
|
||||
let html = build_async_response(stream, &options, runtime, scope).await;
|
||||
let new_res_parts = res_options.0.read().clone();
|
||||
|
||||
let new_res_parts = res_options.0.read().clone();
|
||||
let mut writable = res_options2.0.write();
|
||||
*writable = new_res_parts;
|
||||
|
||||
let mut writable = res_options2.0.write();
|
||||
*writable = new_res_parts;
|
||||
|
||||
_ = tx.send(html);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
_ = tx.send(html);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1214,3 +1142,14 @@ impl LeptosRoutes for axum::Router {
|
||||
router
|
||||
}
|
||||
}
|
||||
|
||||
fn get_leptos_pool() -> LocalPoolHandle {
|
||||
static LOCAL_POOL: OnceLock<LocalPoolHandle> = OnceLock::new();
|
||||
LOCAL_POOL
|
||||
.get_or_init(|| {
|
||||
tokio_util::task::LocalPoolHandle::new(
|
||||
available_parallelism().map(Into::into).unwrap_or(1),
|
||||
)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
@@ -16,5 +16,6 @@ leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
@@ -301,7 +301,10 @@ async fn handle_server_fns_inner(
|
||||
}
|
||||
Err(e) => Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Body::from(e.to_string())),
|
||||
.body(Body::from(
|
||||
serde_json::to_string(&e)
|
||||
.unwrap_or_else(|_| e.to_string()),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Response::builder()
|
||||
|
||||
@@ -218,3 +218,21 @@ pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
|
||||
/// }
|
||||
/// ```
|
||||
pub type AttributeValue = Box<dyn IntoAttribute>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait Component<P> {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait Props {
|
||||
type Builder;
|
||||
fn builder() -> Self::Builder;
|
||||
}
|
||||
|
||||
impl<P, F, R> Component<P> for F where F: FnOnce(::leptos::Scope, P) -> R {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn component_props_builder<P: Props>(
|
||||
_f: &impl Component<P>,
|
||||
) -> <P as Props>::Builder {
|
||||
<P as Props>::builder()
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ pub fn set_timeout_with_handle(
|
||||
pub fn debounce<T: 'static>(
|
||||
cx: Scope,
|
||||
delay: Duration,
|
||||
mut cb: impl FnMut(T) + 'static,
|
||||
#[allow(unused_mut)] mut cb: impl FnMut(T) + 'static,
|
||||
) -> impl FnMut(T) {
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
|
||||
@@ -137,6 +137,7 @@ impl ToTokens for Model {
|
||||
let lifetimes = body.sig.generics.lifetimes();
|
||||
|
||||
let props_name = format_ident!("{name}Props");
|
||||
let props_builder_name = format_ident!("{name}PropsBuilder");
|
||||
let trace_name = format!("<{name} />");
|
||||
|
||||
let prop_builder_fields = prop_builder_fields(vis, props);
|
||||
@@ -200,6 +201,13 @@ impl ToTokens for Model {
|
||||
#prop_builder_fields
|
||||
}
|
||||
|
||||
impl #generics ::leptos::Props for #props_name #generics #where_clause {
|
||||
type Builder = #props_builder_name #generics;
|
||||
fn builder() -> Self::Builder {
|
||||
#props_name::builder()
|
||||
}
|
||||
}
|
||||
|
||||
#docs
|
||||
#component_fn_prop_docs
|
||||
#[allow(non_snake_case, clippy::too_many_arguments)]
|
||||
|
||||
@@ -1062,7 +1062,6 @@ pub(crate) fn component_to_tokens(
|
||||
let name = &node.name;
|
||||
let component_name = ident_from_tag_name(&node.name);
|
||||
let span = node.name.span();
|
||||
let component_props_name = format_ident!("{component_name}Props");
|
||||
|
||||
let attrs = node.attributes.iter().filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
@@ -1154,7 +1153,7 @@ pub(crate) fn component_to_tokens(
|
||||
let component = quote! {
|
||||
#name(
|
||||
#cx,
|
||||
#component_props_name::builder()
|
||||
::leptos::component_props_builder(&#name)
|
||||
#(#props)*
|
||||
#children
|
||||
.build()
|
||||
|
||||
@@ -45,6 +45,10 @@ indexmap = "1"
|
||||
ouroboros = { version = "0.15.6", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.4.0", features = ["html_reports"] }
|
||||
reactive-signals = { version = "0.1.0-alpha.4", features = ["profile"] }
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
log = "0.4"
|
||||
tokio-test = "0.4"
|
||||
leptos = { path = "../leptos" }
|
||||
@@ -110,3 +114,15 @@ skip_feature_sets = [
|
||||
"rkyv",
|
||||
],
|
||||
]
|
||||
|
||||
[[bench]]
|
||||
name = "deep_update"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "fan_out"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "narrow_down"
|
||||
harness = false
|
||||
|
||||
110
leptos_reactive/benches/deep_update.rs
Normal file
110
leptos_reactive/benches/deep_update.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
fn rs_deep_update(c: &mut Criterion) {
|
||||
use reactive_signals::{
|
||||
runtimes::ClientRuntime, signal, types::Func, Signal,
|
||||
};
|
||||
|
||||
c.bench_function("rs_deep_update", |b| {
|
||||
b.iter(|| {
|
||||
let sc = ClientRuntime::bench_root_scope();
|
||||
let signal = signal!(sc, 0);
|
||||
let mut memos = Vec::<Signal<Func<i32>, ClientRuntime>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(signal!(sc, move || prev.get() + 1))
|
||||
} else {
|
||||
memos.push(signal!(sc, move || signal.get() + 1))
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn l021_deep_update(c: &mut Criterion) {
|
||||
use l021::*;
|
||||
|
||||
c.bench_function("l021_deep_update", |b| {
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
runtime.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
fn sycamore_deep_update(c: &mut Criterion) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
c.bench_function("sycamore_deep_update", |b| {
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(*memos[999].get(), 1001);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn leptos_deep_update(c: &mut Criterion) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
c.bench_function("leptos_deep_update", |b| {
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
deep,
|
||||
rs_deep_update,
|
||||
l021_deep_update,
|
||||
sycamore_deep_update,
|
||||
leptos_deep_update
|
||||
);
|
||||
criterion_main!(deep);
|
||||
93
leptos_reactive/benches/fan_out.rs
Normal file
93
leptos_reactive/benches/fan_out.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
fn rs_fan_out(c: &mut Criterion) {
|
||||
use reactive_signals::{
|
||||
runtimes::ClientRuntime, signal, types::Func, Signal,
|
||||
};
|
||||
|
||||
c.bench_function("rs_fan_out", |b| {
|
||||
b.iter(|| {
|
||||
let cx = ClientRuntime::bench_root_scope();
|
||||
let sig = signal!(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| signal!(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn l021_fan_out(c: &mut Criterion) {
|
||||
use l021::*;
|
||||
|
||||
c.bench_function("l021_fan_out", |b| {
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
runtime.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
fn sycamore_fan_out(c: &mut Criterion) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
c.bench_function("sycamore_fan_out", |b| {
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let sig = create_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(
|
||||
memos.iter().map(|m| *(*m.get())).sum::<i32>(),
|
||||
1000
|
||||
);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn leptos_fan_out(c: &mut Criterion) {
|
||||
use leptos_reactive::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
c.bench_function("leptos_fan_out", |b| {
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
fan_out,
|
||||
rs_fan_out,
|
||||
l021_fan_out,
|
||||
sycamore_fan_out,
|
||||
leptos_fan_out
|
||||
);
|
||||
criterion_main!(fan_out);
|
||||
98
leptos_reactive/benches/narrow_down.rs
Normal file
98
leptos_reactive/benches/narrow_down.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
fn rs_narrow_down(c: &mut Criterion) {
|
||||
use reactive_signals::{
|
||||
runtimes::ClientRuntime, signal, types::Func, Signal,
|
||||
};
|
||||
|
||||
c.bench_function("rs_narrow_down", |b| {
|
||||
b.iter(|| {
|
||||
let cx = ClientRuntime::bench_root_scope();
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
Rc::new((0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>());
|
||||
let memo = signal!(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo.get(), 499500);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn l021_narrow_down(c: &mut Criterion) {
|
||||
use l021::*;
|
||||
|
||||
c.bench_function("l021_narrow_down", |b| {
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
runtime.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
fn sycamore_narrow_down(c: &mut Criterion) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
c.bench_function("sycamore_narrow_down", |b| {
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(*memo.get(), 499500);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn leptos_narrow_down(c: &mut Criterion) {
|
||||
use leptos_reactive::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
c.bench_function("leptos_narrow_down", |b| {
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
narrow_down,
|
||||
rs_narrow_down,
|
||||
l021_narrow_down,
|
||||
sycamore_narrow_down,
|
||||
leptos_narrow_down
|
||||
);
|
||||
criterion_main!(narrow_down);
|
||||
@@ -94,6 +94,7 @@ use std::{
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
type ServerFnTraitObj = server_fn::ServerFnTraitObj<Scope>;
|
||||
|
||||
#[allow(unused)]
|
||||
type ServerFunction = server_fn::ServerFunction<Scope>;
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Link, LinkProps};
|
||||
use crate::Link;
|
||||
use leptos::*;
|
||||
|
||||
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
|
||||
|
||||
@@ -21,6 +21,7 @@ pub fn Redirect<P>(
|
||||
path: P,
|
||||
/// Navigation options to be used on the client side.
|
||||
#[prop(optional)]
|
||||
#[allow(unused)]
|
||||
options: Option<NavigateOptions>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
@@ -36,6 +37,7 @@ where
|
||||
}
|
||||
// redirect on the client
|
||||
else {
|
||||
#[allow(unused)]
|
||||
let navigate = use_navigate(cx);
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
leptos::request_animation_frame(move || {
|
||||
|
||||
@@ -72,7 +72,7 @@ where
|
||||
P: std::fmt::Display + 'static,
|
||||
C: Fn(Scope) -> bool + 'static,
|
||||
{
|
||||
use crate::{Redirect, RedirectProps};
|
||||
use crate::Redirect;
|
||||
let redirect_path = redirect_path.to_string();
|
||||
|
||||
define_route(
|
||||
|
||||
@@ -515,11 +515,13 @@ where
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let status = status.as_u16();
|
||||
if (500..=599).contains(&status) {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let status_text = resp.status_text();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let status_text = status.to_string();
|
||||
return Err(ServerFnError::ServerError(status_text));
|
||||
return Err(serde_json::from_str(&text)
|
||||
.unwrap_or(ServerFnError::ServerError(status_text)));
|
||||
}
|
||||
|
||||
// Decoding the body of the request
|
||||
|
||||
Reference in New Issue
Block a user