mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 11:21:55 -05:00
Compare commits
18 Commits
server-fn-
...
api-routes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
beea04a050 | ||
|
|
ea153e4f26 | ||
|
|
59b8626277 | ||
|
|
d8e03773f0 | ||
|
|
6c763a83cb | ||
|
|
9cf337309d | ||
|
|
1af35cdd3b | ||
|
|
fcb98474b8 | ||
|
|
54f7e9366a | ||
|
|
ddf9df2b5e | ||
|
|
7fe9f82d89 | ||
|
|
661adc4027 | ||
|
|
1011c464dc | ||
|
|
4b498a3b42 | ||
|
|
3c90b47e77 | ||
|
|
671b1e4a8f | ||
|
|
52021be806 | ||
|
|
75a7bd610a |
28
Cargo.toml
28
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-alpha"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.5" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.5" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.5" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.5" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.5" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.5" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.5" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.5" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.5" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.5" }
|
||||
leptos_router = { path = "./router", version = "0.2.5" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.2.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.5" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0-alpha" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0-alpha" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0-alpha" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0-alpha" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_router = { path = "./router", version = "0.3.0-alpha" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0-alpha" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0-alpha" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -69,13 +69,27 @@ dependencies = [
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = ["test-all"]
|
||||
dependencies = ["test-all", "test-leptos_macro-example", "doc-leptos_macro-example"]
|
||||
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.test-leptos_macro-example]
|
||||
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test", "--doc"]
|
||||
cwd = "leptos_macro/example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.doc-leptos_macro-example]
|
||||
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "doc"]
|
||||
cwd = "leptos_macro/example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.test-examples]
|
||||
description = "Run all unit and web tests for examples"
|
||||
cwd = "examples"
|
||||
|
||||
@@ -31,7 +31,7 @@ where
|
||||
|
||||
This is pretty straightforward: when the user is logged in, we want to show `children`. Until if the user is not logged in, we want to show `fallback`. And while we’re waiting to find out, we just render `()`, i.e., nothing.
|
||||
|
||||
In other words, we want to pass the children of `<WhenLoaded>/` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
|
||||
In other words, we want to pass the children of `<WhenLoaded/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
|
||||
|
||||
This won’t compile.
|
||||
|
||||
@@ -40,7 +40,7 @@ error[E0507]: cannot move out of `fallback`, a captured variable in an `Fn` clos
|
||||
error[E0507]: cannot move out of `children`, a captured variable in an `Fn` closure
|
||||
```
|
||||
|
||||
The problem here is that both `<Suspense/>` and `<Show/>` need to be able to construct their `children` multiple names. The first time you construct `<Suspense/>`’s children, it would take ownership of `fallback` and `children` to move them into the invocation of `<Show/>`, but then they're not available for future `<Suspense/>` children construction.
|
||||
The problem here is that both `<Suspense/>` and `<Show/>` need to be able to construct their `children` multiple times. The first time you construct `<Suspense/>`’s children, it would take ownership of `fallback` and `children` to move them into the invocation of `<Show/>`, but then they're not available for future `<Suspense/>` children construction.
|
||||
|
||||
## The Details
|
||||
|
||||
@@ -80,13 +80,13 @@ Suspense(
|
||||
)
|
||||
```
|
||||
|
||||
All components own their props; so the `<Show/>` in this case can’t be called, because it only has captured references to `fallback` and `children`.
|
||||
All components own their props; so the `<Show/>` in this case can’t be called because it only has captured references to `fallback` and `children`.
|
||||
|
||||
## Solution
|
||||
|
||||
However, both `<Suspense/>` and `<Show/>` take `ChildrenFn`, i.e., their `children` should implement the `Fn` type so they can be called multiple times with only an immutable reference. This means we don’t need to own `children` or `fallback`; we just need to be able to pass `'static` references to them.
|
||||
|
||||
We can solve this problem by using the [`store_value`](https://docs.rs/leptos/latest/leptos/fn.store_value.html) primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, `Copy` and `'static`, and which we can access or modify through certain methods.
|
||||
We can solve this problem by using the [`store_value`](https://docs.rs/leptos/latest/leptos/fn.store_value.html) primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, `Copy` and `'static`, which we can access or modify through certain methods.
|
||||
|
||||
In this case, it’s really simple:
|
||||
|
||||
@@ -113,7 +113,7 @@ where
|
||||
}
|
||||
```
|
||||
|
||||
At the top level, we store both `fallback` and `children` in the reactive scope owned by `LoggedIn`. Now we can simply move those references down through the other layers into the `<Show/>` component, and call them there.
|
||||
At the top level, we store both `fallback` and `children` in the reactive scope owned by `LoggedIn`. Now we can simply move those references down through the other layers into the `<Show/>` component and call them there.
|
||||
|
||||
## A Final Note
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
};
|
||||
data.into_iter()
|
||||
.map(|value| view! { cx, <span>{value}</span> })
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -31,6 +31,22 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
Leptos also provides a `.collect_view(cx)` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
|
||||
|
||||
```rust
|
||||
let values = vec![0, 1, 2];
|
||||
view! { cx,
|
||||
// this will just render "012"
|
||||
<p>{values.clone()}</p>
|
||||
// or we can wrap them in <li>
|
||||
<ul>
|
||||
{values.into_iter()
|
||||
.map(|n| view! { cx, <li>{n}</li>})
|
||||
.collect_view(cx)}
|
||||
</ul>
|
||||
}
|
||||
```
|
||||
|
||||
The fact that the _list_ is static doesn’t mean the interface needs to be static.
|
||||
You can render dynamic items as part of a static list.
|
||||
|
||||
@@ -52,7 +68,7 @@ let counter_buttons = counters
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect_view(cx);
|
||||
|
||||
view! { cx,
|
||||
<ul>{counter_buttons}</ul>
|
||||
|
||||
@@ -80,7 +80,7 @@ fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -103,7 +103,7 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
.nodes
|
||||
.into_iter()
|
||||
.map(|child| view! { cx, <li>{child}</li> })
|
||||
.collect::<Vec<_>>();
|
||||
.collect_view(cx);
|
||||
|
||||
view! { cx,
|
||||
<ul>{children}</ul>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
extend = [{ path = "./cargo-make/common.toml" }]
|
||||
|
||||
[env]
|
||||
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = ""
|
||||
|
||||
# Emulate workspace
|
||||
CARGO_MAKE_WORKSPACE_EMULATION = true
|
||||
|
||||
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
"counter",
|
||||
"counter_isomorphic",
|
||||
@@ -24,7 +23,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
"ssr_modes_axum",
|
||||
"tailwind",
|
||||
"tailwind_csr_trunk",
|
||||
"timer",
|
||||
"todo_app_sqlite",
|
||||
"todo_app_sqlite_axum",
|
||||
"todo_app_sqlite_viz",
|
||||
@@ -43,10 +41,6 @@ dependencies = ["check-style", "test-unit-and-web"]
|
||||
description = "Run all unit and web tests"
|
||||
dependencies = ["test-flow", "web-test-flow"]
|
||||
|
||||
[tasks.check-style]
|
||||
description = "Check for style violations"
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
[tasks.pre-verify-flow]
|
||||
|
||||
[tasks.post-verify-flow]
|
||||
|
||||
14
examples/cargo-make/common.toml
Normal file
14
examples/cargo-make/common.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[env]
|
||||
CARGO_MAKE_CLIPPY_ARGS = "--all-targets -- -D warnings"
|
||||
|
||||
[tasks.check-style]
|
||||
description = "Check for style violations"
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
[tasks.verify-local]
|
||||
description = "Run all quality checks and tests from an example directory"
|
||||
dependencies = ["check-style", "test-local"]
|
||||
|
||||
[tasks.test-local]
|
||||
description = "Run all tests from an example directory"
|
||||
dependencies = ["test", "web-test"]
|
||||
4
examples/cargo-make/wasm-web-test.toml
Normal file
4
examples/cargo-make/wasm-web-test.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[tasks.web-test]
|
||||
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
@@ -1,3 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/wasm-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use leptos::*;
|
||||
|
||||
/// A simple counter component.
|
||||
///
|
||||
///
|
||||
/// You can use doc comments like this to document your component.
|
||||
#[component]
|
||||
pub fn SimpleCounter(
|
||||
@@ -9,7 +9,7 @@ pub fn SimpleCounter(
|
||||
/// The starting value for the counter
|
||||
initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
step: i32
|
||||
step: i32,
|
||||
) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ use leptos::*;
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx,
|
||||
<SimpleCounter
|
||||
initial_value=0
|
||||
step=1
|
||||
/>
|
||||
mount_to_body(|cx| {
|
||||
view! { cx,
|
||||
<SimpleCounter
|
||||
initial_value=0
|
||||
step=1
|
||||
/>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ wasm_bindgen_test_configure!(run_in_browser);
|
||||
fn clear() {
|
||||
let document = leptos::document();
|
||||
let test_wrapper = document.create_element("section").unwrap();
|
||||
document.body().unwrap().append_child(&test_wrapper);
|
||||
let _ = document.body().unwrap().append_child(&test_wrapper);
|
||||
|
||||
// start by rendering our counter and mounting it to the DOM
|
||||
// note that we start at the initial value of 10
|
||||
@@ -38,7 +38,7 @@ fn clear() {
|
||||
// test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let (value, _set_value) = create_signal(cx, 0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
@@ -71,7 +71,7 @@ fn clear() {
|
||||
fn inc() {
|
||||
let document = leptos::document();
|
||||
let test_wrapper = document.create_element("section").unwrap();
|
||||
document.body().unwrap().append_child(&test_wrapper);
|
||||
let _ = document.body().unwrap().append_child(&test_wrapper);
|
||||
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
@@ -79,7 +79,7 @@ fn inc() {
|
||||
);
|
||||
|
||||
// You can do testing with vanilla DOM operations
|
||||
let document = leptos::document();
|
||||
let _document = leptos::document();
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = div
|
||||
.first_child()
|
||||
@@ -1,3 +1,5 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use broadcaster::BroadcastChannel;
|
||||
static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use broadcaster::BroadcastChannel;
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn register_server_functions() {
|
||||
_ = GetServerCount::register();
|
||||
_ = AdjustServerCount::register();
|
||||
_ = ClearServerCount::register();
|
||||
pub fn register_server_functions() {
|
||||
_ = GetServerCount::register();
|
||||
_ = AdjustServerCount::register();
|
||||
_ = ClearServerCount::register();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
|
||||
#[server(GetServerCount, "/api")]
|
||||
pub async fn get_server_count() -> Result<i32, ServerFnError> {
|
||||
@@ -29,7 +29,10 @@ pub async fn get_server_count() -> Result<i32, ServerFnError> {
|
||||
}
|
||||
|
||||
#[server(AdjustServerCount, "/api")]
|
||||
pub async fn adjust_server_count(delta: i32, msg: String) -> Result<i32, ServerFnError> {
|
||||
pub async fn adjust_server_count(
|
||||
delta: i32,
|
||||
msg: String,
|
||||
) -> Result<i32, ServerFnError> {
|
||||
let new = COUNT.load(Ordering::Relaxed) + delta;
|
||||
COUNT.store(new, Ordering::Relaxed);
|
||||
_ = COUNT_CHANNEL.send(&new).await;
|
||||
@@ -46,36 +49,49 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"Server-Side Counters"</h1>
|
||||
<p>"Each of these counters stores its data in the same variable on the server."</p>
|
||||
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
|
||||
<p>
|
||||
"The value is shared across connections. Try opening this is another browser tab to see what I mean."
|
||||
</p>
|
||||
</header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><A href="">"Simple"</A></li>
|
||||
<li><A href="form">"Form-Based"</A></li>
|
||||
<li><A href="multi">"Multi-User"</A></li>
|
||||
<li>
|
||||
<A href="">"Simple"</A>
|
||||
</li>
|
||||
<li>
|
||||
<A href="form">"Form-Based"</A>
|
||||
</li>
|
||||
<li>
|
||||
<A href="multi">"Multi-User"</A>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<Counter/>
|
||||
}/>
|
||||
<Route path="form" view=|cx| view! {
|
||||
cx,
|
||||
<FormCounter/>
|
||||
}/>
|
||||
<Route path="multi" view=|cx| view! {
|
||||
cx,
|
||||
<MultiuserCounter/>
|
||||
}/>
|
||||
<Route
|
||||
path=""
|
||||
view=|cx| {
|
||||
view! { cx, <Counter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="form"
|
||||
view=|cx| {
|
||||
view! { cx, <FormCounter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="multi"
|
||||
view=|cx| {
|
||||
view! { cx, <MultiuserCounter/> }
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -93,33 +109,47 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
let counter = create_resource(
|
||||
cx,
|
||||
move || (dec.version().get(), inc.version().get(), clear.version().get()),
|
||||
move || {
|
||||
(
|
||||
dec.version().get(),
|
||||
inc.version().get(),
|
||||
clear.version().get(),
|
||||
)
|
||||
},
|
||||
|_| get_server_count(),
|
||||
);
|
||||
|
||||
let value = move || counter.read(cx).map(|count| count.unwrap_or(0)).unwrap_or(0);
|
||||
let error_msg = move || {
|
||||
let value = move || {
|
||||
counter
|
||||
.read(cx)
|
||||
.map(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
})
|
||||
.flatten()
|
||||
.map(|count| count.unwrap_or(0))
|
||||
.unwrap_or(0)
|
||||
};
|
||||
let error_msg = move || {
|
||||
counter.read(cx).and_then(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<h2>"Simple Counter"</h2>
|
||||
<p>"This counter sets the value on the server and automatically reloads the new value."</p>
|
||||
<p>
|
||||
"This counter sets the value on the server and automatically reloads the new value."
|
||||
</p>
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
{move || error_msg().map(|msg| view! { cx, <p>"Error: " {msg.to_string()}</p>})}
|
||||
{move || {
|
||||
error_msg()
|
||||
.map(|msg| {
|
||||
view! { cx, <p>"Error: " {msg.to_string()}</p> }
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -142,19 +172,15 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
);
|
||||
let value = move || {
|
||||
log::debug!("FormCounter looking for value");
|
||||
counter
|
||||
.read(cx)
|
||||
.map(|n| n.ok())
|
||||
.flatten()
|
||||
.map(|n| n)
|
||||
.unwrap_or(0)
|
||||
counter.read(cx).and_then(|n| n.ok()).unwrap_or(0)
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<h2>"Form Counter"</h2>
|
||||
<p>"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"</p>
|
||||
<p>
|
||||
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
|
||||
</p>
|
||||
<div>
|
||||
// calling a server function is the same as POSTing to its API URL
|
||||
// so we can just do that with a form and button
|
||||
@@ -185,26 +211,32 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
// This is the primitive pattern for live chat, collaborative editing, etc.
|
||||
#[component]
|
||||
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
let dec = create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc = create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let dec =
|
||||
create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc =
|
||||
create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let multiplayer_value = {
|
||||
use futures::StreamExt;
|
||||
|
||||
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
|
||||
.expect("couldn't connect to SSE stream");
|
||||
let mut source =
|
||||
gloo_net::eventsource::futures::EventSource::new("/api/events")
|
||||
.expect("couldn't connect to SSE stream");
|
||||
let s = create_signal_from_stream(
|
||||
cx,
|
||||
source.subscribe("message").unwrap().map(|value| {
|
||||
match value {
|
||||
Ok(value) => {
|
||||
value.1.data().as_string().expect("expected string value")
|
||||
},
|
||||
source
|
||||
.subscribe("message")
|
||||
.unwrap()
|
||||
.map(|value| match value {
|
||||
Ok(value) => value
|
||||
.1
|
||||
.data()
|
||||
.as_string()
|
||||
.expect("expected string value"),
|
||||
Err(_) => "0".to_string(),
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
on_cleanup(cx, move || source.close());
|
||||
@@ -212,18 +244,20 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let (multiplayer_value, _) =
|
||||
create_signal(cx, None::<i32>);
|
||||
let (multiplayer_value, _) = create_signal(cx, None::<i32>);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<h2>"Multi-User Counter"</h2>
|
||||
<p>"This one uses server-sent events (SSE) to live-update when other users make changes."</p>
|
||||
<p>
|
||||
"This one uses server-sent events (SSE) to live-update when other users make changes."
|
||||
</p>
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default().to_string()}</span>
|
||||
<span>
|
||||
"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default()}
|
||||
</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod counters;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::counters::*;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
mod counters;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::*;
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::counters::*;
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
[env]
|
||||
CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome"
|
||||
|
||||
[tasks.test-all]
|
||||
dependencies = ["test", "web-test"]
|
||||
|
||||
[tasks.web-test]
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/wasm-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/wasm-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
use wasm_bindgen_test::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
use counters::Counters;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| view! { cx, <Counters/> });
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let add_counter = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
// add 3 counters
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
|
||||
// check HTML
|
||||
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span><!-- <DynChild> -->0<!-- </DynChild> --></span> from <span><!-- <DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- <Each> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->0<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->0<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->0<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- </Each> --></ul>");
|
||||
|
||||
let counters = div
|
||||
.query_selector("ul")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.children();
|
||||
|
||||
// click first counter once, second counter twice, etc.
|
||||
// `NodeList` isn't a `Vec` so we iterate over it in this slightly awkward way
|
||||
for idx in 0..counters.length() {
|
||||
let counter = counters.item(idx).unwrap();
|
||||
let inc_button = counter
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>();
|
||||
for _ in 0..=idx {
|
||||
inc_button.click();
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span><!-- <DynChild> -->6<!-- </DynChild> --></span> from <span><!-- <DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- <Each> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->1<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->2<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->3<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- </Each> --></ul>");
|
||||
|
||||
// remove the first counter
|
||||
counters
|
||||
.item(0)
|
||||
.unwrap()
|
||||
.last_child()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.click();
|
||||
|
||||
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span><!-- <DynChild> -->5<!-- </DynChild> --></span> from <span><!-- <DynChild> -->2<!-- </DynChild> --></span> counters.</p><ul><!-- <Each> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->2<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->3<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- </Each> --></ul>");
|
||||
}
|
||||
120
examples/counters/tests/web.rs
Normal file
120
examples/counters/tests/web.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counters::Counters;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| view! { cx, <Counters/> });
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let add_counter = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
// add 3 counters
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
|
||||
// check HTML
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
|
||||
<DynChild> -->0<!-- </DynChild> --></span> from <span><!-- \
|
||||
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
|
||||
<Each> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->0<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->0<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->0<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- </Each> --></ul>"
|
||||
);
|
||||
|
||||
let counters = div
|
||||
.query_selector("ul")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.children();
|
||||
|
||||
// click first counter once, second counter twice, etc.
|
||||
// `NodeList` isn't a `Vec` so we iterate over it in this slightly awkward way
|
||||
for idx in 0..counters.length() {
|
||||
let counter = counters.item(idx).unwrap();
|
||||
let inc_button = counter
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>();
|
||||
for _ in 0..=idx {
|
||||
inc_button.click();
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
|
||||
<DynChild> -->6<!-- </DynChild> --></span> from <span><!-- \
|
||||
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
|
||||
<Each> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->1<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->2<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->3<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- </Each> --></ul>"
|
||||
);
|
||||
|
||||
// remove the first counter
|
||||
counters
|
||||
.item(0)
|
||||
.unwrap()
|
||||
.last_child()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.click();
|
||||
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
|
||||
<DynChild> -->5<!-- </DynChild> --></span> from <span><!-- \
|
||||
<DynChild> -->2<!-- </DynChild> --></span> counters.</p><ul><!-- \
|
||||
<Each> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->2<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->3<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- </Each> --></ul>"
|
||||
);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
errors
|
||||
.iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li> })
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
})
|
||||
};
|
||||
|
||||
@@ -76,7 +76,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
data.map(|data| {
|
||||
data.iter()
|
||||
.map(|s| view! { cx, <span>{s}</span> })
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
@@ -97,7 +97,7 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
<li><A href=contact.id.to_string()><span>{&contact.first_name} " " {&contact.last_name}</span></A></li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -241,11 +241,11 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
vec![view! { cx, <p>"No tasks were found."</p> }.into_any()]
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
@@ -266,9 +266,8 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
.into_any()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -287,7 +286,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
|
||||
@@ -44,7 +44,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
|
||||
.map(|posts| {
|
||||
posts.iter()
|
||||
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a></li>})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
})
|
||||
)
|
||||
};
|
||||
@@ -109,7 +109,7 @@ fn Post(cx: Scope) -> impl IntoView {
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, error)| view! { cx, <li>{error.to_string()} </li> })
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
|
||||
.map(|posts| {
|
||||
posts.iter()
|
||||
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a> "|" <a href=format!("/post_in_order/{}", post.id)>{&post.title}"(in order)"</a></li>})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
})
|
||||
)
|
||||
};
|
||||
@@ -114,7 +114,7 @@ fn Post(cx: Scope) -> impl IntoView {
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, error)| view! { cx, <li>{error.to_string()} </li> })
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6.2", optional = true }
|
||||
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
|
||||
actix-web = { version = "4.2.1", features = ["macros"] }
|
||||
anyhow = "1.0.68"
|
||||
broadcaster = "1.0.0"
|
||||
console_log = "1.0.0"
|
||||
@@ -19,7 +19,7 @@ cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_actix = { path = "../../integrations/actix" }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
@@ -36,10 +36,8 @@ default = ["ssr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_actix",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
@@ -107,6 +107,9 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
cx,
|
||||
<Todos/>
|
||||
}/>
|
||||
<Api path="bananas" route=web::get().to(|req: HttpRequest| async move {
|
||||
req.path()
|
||||
})
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -176,8 +179,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_view(cx)
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -196,7 +198,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
|
||||
@@ -229,8 +229,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_view(cx)
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -249,7 +248,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
|
||||
@@ -183,8 +183,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_view(cx)
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -203,7 +202,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
|
||||
@@ -16,7 +16,7 @@ use actix_web::{
|
||||
use futures::{Stream, StreamExt};
|
||||
use http::StatusCode;
|
||||
use leptos::{
|
||||
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
|
||||
leptos_dom::{Transparent, ssr::render_to_stream_with_prefix_undisposed_with_context},
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
server_fn::Encoding,
|
||||
*,
|
||||
@@ -922,7 +922,7 @@ pub fn generate_route_list_with_exclusions<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut routes = leptos_router::generate_route_list_inner(app_fn);
|
||||
let (mut routes, mut api_routes) = leptos_router::generate_route_list_inner(app_fn);
|
||||
|
||||
// Empty strings screw with Actix pathing, they need to be "/"
|
||||
routes = routes
|
||||
@@ -1113,6 +1113,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines an API route, which mounts the given route handler at this path.
|
||||
#[component(transparent)]
|
||||
pub fn Api<P>(cx: leptos::Scope, path: P, route: actix_web::Route) -> impl IntoView
|
||||
where P: Into<String>
|
||||
{
|
||||
Transparent::new(ApiRouteListing::new(path.into(), route))
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions. This takes
|
||||
/// a handler function as its argument. The handler follows similar rules to an Actix
|
||||
/// [Handler](actix_web::Handler): it is an async function that receives arguments that
|
||||
|
||||
@@ -164,8 +164,8 @@ pub use leptos_dom::{
|
||||
window_event_listener_with_precast,
|
||||
},
|
||||
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
|
||||
Class, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass,
|
||||
IntoProperty, IntoView, NodeRef, Property, View,
|
||||
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
|
||||
IntoClass, IntoProperty, IntoView, NodeRef, Property, View,
|
||||
};
|
||||
pub use leptos_macro::*;
|
||||
pub use leptos_reactive::*;
|
||||
@@ -240,24 +240,3 @@ pub fn component_props_builder<P: Props>(
|
||||
) -> <P as Props>::Builder {
|
||||
<P as Props>::builder()
|
||||
}
|
||||
|
||||
#[cfg(all(not(doc), feature = "csr", feature = "ssr"))]
|
||||
compile_error!(
|
||||
"You have both `csr` and `ssr` enabled as features, which may cause \
|
||||
issues like <Suspense/>` failing to work silently. `csr` is enabled by \
|
||||
default on `leptos`, and can be disabled by adding `default-features = \
|
||||
false` to your `leptos` dependency."
|
||||
);
|
||||
|
||||
#[cfg(all(not(doc), feature = "hydrate", feature = "ssr"))]
|
||||
compile_error!(
|
||||
"You have both `hydrate` and `ssr` enabled as features, which may cause \
|
||||
issues like <Suspense/>` failing to work silently."
|
||||
);
|
||||
|
||||
#[cfg(all(not(doc), feature = "hydrate", feature = "csr"))]
|
||||
compile_error!(
|
||||
"You have both `hydrate` and `csr` enabled as features, which may cause \
|
||||
issues. `csr` is enabled by default on `leptos`, and can be disabled by \
|
||||
adding `default-features = false` to your `leptos` dependency."
|
||||
);
|
||||
|
||||
@@ -29,18 +29,15 @@ use std::rc::Rc;
|
||||
/// <Suspense fallback=move || view! { cx, <p>"Loading (Suspense Fallback)..."</p> }>
|
||||
/// {move || {
|
||||
/// cats.read(cx).map(|data| match data {
|
||||
/// None => view! { cx, <pre>"Error"</pre> }.into_any(),
|
||||
/// Some(cats) => view! { cx,
|
||||
/// <div>{
|
||||
/// cats.iter()
|
||||
/// .map(|src| {
|
||||
/// None => view! { cx, <pre>"Error"</pre> }.into_view(cx),
|
||||
/// Some(cats) => cats
|
||||
/// .iter()
|
||||
/// .map(|src| {
|
||||
/// view! { cx,
|
||||
/// <img src={src}/>
|
||||
/// }
|
||||
/// })
|
||||
/// .collect::<Vec<_>>()
|
||||
/// }</div>
|
||||
/// }.into_any(),
|
||||
/// })
|
||||
/// .collect_view(cx),
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
|
||||
@@ -39,18 +39,15 @@ use std::{
|
||||
/// >
|
||||
/// {move || {
|
||||
/// cats.read(cx).map(|data| match data {
|
||||
/// None => view! { cx, <pre>"Error"</pre> }.into_any(),
|
||||
/// Some(cats) => view! { cx,
|
||||
/// <div>{
|
||||
/// cats.iter()
|
||||
/// .map(|src| {
|
||||
/// None => view! { cx, <pre>"Error"</pre> }.into_view(cx),
|
||||
/// Some(cats) => cats
|
||||
/// .iter()
|
||||
/// .map(|src| {
|
||||
/// view! { cx,
|
||||
/// <img src={src}/>
|
||||
/// }
|
||||
/// })
|
||||
/// .collect::<Vec<_>>()
|
||||
/// }</div>
|
||||
/// }.into_any(),
|
||||
/// })
|
||||
/// .collect_view(cx),
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
|
||||
@@ -904,6 +904,40 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Optionally adds an event listener to this element.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// #[component]
|
||||
/// pub fn Input(
|
||||
/// cx: Scope,
|
||||
/// #[prop(optional)] value: Option<RwSignal<String>>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view! { cx, <input/> }
|
||||
/// // only add event if `value` is `Some(signal)`
|
||||
/// .optional_event(
|
||||
/// ev::input,
|
||||
/// value.map(|value| move |ev| value.set(event_target_value(&ev))),
|
||||
/// )
|
||||
/// }
|
||||
/// #
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn optional_event<E: EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
#[allow(unused_mut)] // used for tracing in debug
|
||||
mut event_handler: Option<impl FnMut(E::EventType) + 'static>,
|
||||
) -> Self {
|
||||
if let Some(event_handler) = event_handler {
|
||||
self.on(event, event_handler)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a child to this element.
|
||||
#[track_caller]
|
||||
pub fn child(self, child: impl IntoView) -> Self {
|
||||
|
||||
@@ -209,6 +209,25 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects an iterator or collection into a [`View`].
|
||||
pub trait CollectView {
|
||||
/// Collects an iterator or collection into a [`View`].
|
||||
fn collect_view(self, cx: Scope) -> View;
|
||||
}
|
||||
|
||||
impl<I: IntoIterator<Item = T>, T: IntoView> CollectView for I {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", name = "#text", skip_all)
|
||||
)]
|
||||
fn collect_view(self, cx: Scope) -> View {
|
||||
self.into_iter()
|
||||
.map(|v| v.into_view(cx))
|
||||
.collect::<Fragment>()
|
||||
.into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
/// HTML element.
|
||||
@@ -815,6 +834,15 @@ where
|
||||
F: FnOnce(Scope) -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
#[cfg(all(feature = "web", feature = "ssr"))]
|
||||
crate::console_warn(
|
||||
"You have both `csr` and `ssr` or `hydrate` and `ssr` enabled as \
|
||||
features, which may cause issues like <Suspense/>` failing to work \
|
||||
silently. `csr` is enabled by default on `leptos`, and can be \
|
||||
disabled by adding `default-features = false` to your `leptos` \
|
||||
dependency.",
|
||||
);
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
mount_to(crate::document().body().expect("body element to exist"), f)
|
||||
|
||||
@@ -271,6 +271,15 @@ impl View {
|
||||
instrument(level = "info", skip_all,)
|
||||
)]
|
||||
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
|
||||
#[cfg(all(feature = "web", feature = "ssr"))]
|
||||
crate::console_error(
|
||||
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
|
||||
enabled as features, which may cause issues like <Suspense/>` \
|
||||
failing to work silently. `csr` is enabled by default on \
|
||||
`leptos`, and can be disabled by adding `default-features = \
|
||||
false` to your `leptos` dependency.\n",
|
||||
);
|
||||
|
||||
self.render_to_string_helper(false)
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,15 @@ pub fn render_to_stream_in_order_with_prefix(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
#[cfg(all(feature = "web", feature = "ssr"))]
|
||||
crate::console_error(
|
||||
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
|
||||
enabled as features, which may cause issues like <Suspense/>` \
|
||||
failing to work silently. `csr` is enabled by default on `leptos`, \
|
||||
and can be disabled by adding `default-features = false` to your \
|
||||
`leptos` dependency.\n",
|
||||
);
|
||||
|
||||
let (stream, runtime, _) =
|
||||
render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
view,
|
||||
|
||||
11
leptos_macro/example/Cargo.toml
Normal file
11
leptos_macro/example/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
leptos.path = "../../leptos"
|
||||
|
||||
[workspace]
|
||||
41
leptos_macro/example/src/lib.rs
Normal file
41
leptos_macro/example/src/lib.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn TestComponent(
|
||||
_cx: Scope,
|
||||
/// Rust code
|
||||
/// ```
|
||||
/// assert_eq!("hello", stringify!(hello));
|
||||
/// ```
|
||||
/// View containing rust code
|
||||
/// ```view
|
||||
/// assert!(true);
|
||||
/// ```
|
||||
/// View containing rsx
|
||||
/// ```view
|
||||
/// # use example::TestComponent;
|
||||
/// <TestComponent key="hello"/>
|
||||
/// ```
|
||||
/// View containing rsx
|
||||
/// ```view compile_fail
|
||||
/// # use example::TestComponent;
|
||||
/// <TestComponent/>
|
||||
/// ```
|
||||
#[prop(into)]
|
||||
key: String,
|
||||
/// rsx unclosed
|
||||
/// ```view
|
||||
/// # use example::TestComponent;
|
||||
/// <TestComponent key="hello"/>
|
||||
#[prop(optional)]
|
||||
another:usize,
|
||||
/// rust unclosed
|
||||
/// ```view
|
||||
/// use example::TestComponent;
|
||||
#[prop(optional)]
|
||||
and_another: usize,
|
||||
) -> impl IntoView {
|
||||
_ = (key, another, and_another);
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ use convert_case::{
|
||||
Casing,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::{format_ident, ToTokens, TokenStreamExt};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote_spanned, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute,
|
||||
FnArg, GenericArgument, ItemFn, LitStr, Meta, MetaNameValue, Pat, PatIdent,
|
||||
Path, PathArguments, ReturnType, Type, TypePath, Visibility,
|
||||
parse::Parse, parse_quote, spanned::Spanned,
|
||||
AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument, Item,
|
||||
ItemFn, Lit, LitStr, Meta, MetaNameValue, Pat, PatIdent, Path,
|
||||
PathArguments, ReturnType, Stmt, Type, TypePath, Visibility,
|
||||
};
|
||||
|
||||
pub struct Model {
|
||||
@@ -129,6 +130,25 @@ impl ToTokens for Model {
|
||||
|
||||
let mut body = body.to_owned();
|
||||
|
||||
// check for components that end ;
|
||||
if !is_transparent {
|
||||
let ends_semi =
|
||||
body.block.stmts.iter().last().and_then(|stmt| match stmt {
|
||||
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(semi) = ends_semi {
|
||||
proc_macro_error::emit_error!(
|
||||
semi.span(),
|
||||
"A component that ends with a `view!` macro followed by a \
|
||||
semicolon will return (), an empty view. This is usually \
|
||||
an accident, not intentional, so we prevent it. If you’d \
|
||||
like to return (), you can do it it explicitly by \
|
||||
returning () as the last item from the component."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
body.sig.ident = format_ident!("__{}", body.sig.ident);
|
||||
#[allow(clippy::redundant_clone)] // false positive
|
||||
let body_name = body.sig.ident.clone();
|
||||
@@ -291,14 +311,14 @@ impl Prop {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Docs(Vec<Attribute>);
|
||||
pub struct Docs(Vec<(String, Span)>);
|
||||
|
||||
impl ToTokens for Docs {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let s = self
|
||||
.0
|
||||
.iter()
|
||||
.map(|attr| attr.to_token_stream())
|
||||
.map(|(doc, span)| quote_spanned!(*span=> #[doc = #doc]))
|
||||
.collect::<TokenStream>();
|
||||
|
||||
tokens.append_all(s);
|
||||
@@ -307,11 +327,96 @@ impl ToTokens for Docs {
|
||||
|
||||
impl Docs {
|
||||
pub fn new(attrs: &[Attribute]) -> Self {
|
||||
let attrs = attrs
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum ViewCodeFenceState {
|
||||
Outside,
|
||||
Rust,
|
||||
Rsx,
|
||||
}
|
||||
let mut quotes = "```".to_string();
|
||||
let mut quote_ws = "".to_string();
|
||||
let mut view_code_fence_state = ViewCodeFenceState::Outside;
|
||||
const RUST_START: &str =
|
||||
"# ::leptos::create_scope(::leptos::create_runtime(), |cx| {";
|
||||
const RUST_END: &str = "# }).dispose();";
|
||||
const RSX_START: &str = "# ::leptos::view! {cx,";
|
||||
const RSX_END: &str = "# };}).dispose();";
|
||||
|
||||
// Seperated out of chain to allow rustfmt to work
|
||||
let map = |(doc, span): (String, Span)| {
|
||||
doc.lines()
|
||||
.flat_map(|doc| {
|
||||
let trimmed_doc = doc.trim_start();
|
||||
let leading_ws = &doc[..doc.len() - trimmed_doc.len()];
|
||||
let trimmed_doc = trimmed_doc.trim_end();
|
||||
match view_code_fence_state {
|
||||
ViewCodeFenceState::Outside
|
||||
if trimmed_doc.starts_with("```")
|
||||
&& trimmed_doc
|
||||
.trim_start_matches('`')
|
||||
.starts_with("view") =>
|
||||
{
|
||||
view_code_fence_state = ViewCodeFenceState::Rust;
|
||||
let view = trimmed_doc.find('v').unwrap();
|
||||
quotes = trimmed_doc[..view].to_owned();
|
||||
quote_ws = leading_ws.to_owned();
|
||||
let rust_options = &trimmed_doc
|
||||
[view + "view".len()..]
|
||||
.trim_start();
|
||||
vec![
|
||||
format!("{leading_ws}{quotes}{rust_options}"),
|
||||
format!("{leading_ws}{RUST_START}"),
|
||||
]
|
||||
}
|
||||
ViewCodeFenceState::Rust if trimmed_doc == quotes => {
|
||||
view_code_fence_state = ViewCodeFenceState::Outside;
|
||||
vec![
|
||||
format!("{leading_ws}{RUST_END}"),
|
||||
doc.to_owned(),
|
||||
]
|
||||
}
|
||||
ViewCodeFenceState::Rust
|
||||
if trimmed_doc.starts_with('<') =>
|
||||
{
|
||||
view_code_fence_state = ViewCodeFenceState::Rsx;
|
||||
vec![
|
||||
format!("{leading_ws}{RSX_START}"),
|
||||
doc.to_owned(),
|
||||
]
|
||||
}
|
||||
ViewCodeFenceState::Rsx if trimmed_doc == quotes => {
|
||||
view_code_fence_state = ViewCodeFenceState::Outside;
|
||||
vec![
|
||||
format!("{leading_ws}{RSX_END}"),
|
||||
doc.to_owned(),
|
||||
]
|
||||
}
|
||||
_ => vec![doc.to_string()],
|
||||
}
|
||||
})
|
||||
.map(|l| (l, span))
|
||||
.collect_vec()
|
||||
};
|
||||
|
||||
let mut attrs = attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.path == parse_quote!(doc))
|
||||
.cloned()
|
||||
.collect();
|
||||
.filter_map(|attr| attr.path.is_ident("doc").then(|| {
|
||||
let Ok(Meta::NameValue(MetaNameValue { lit: Lit::Str(doc), .. })) = attr.parse_meta() else {
|
||||
abort!(attr, "expected doc comment to be string literal");
|
||||
};
|
||||
(doc.value(), doc.span())
|
||||
}))
|
||||
.flat_map(map)
|
||||
.collect_vec();
|
||||
|
||||
if view_code_fence_state != ViewCodeFenceState::Outside {
|
||||
if view_code_fence_state == ViewCodeFenceState::Rust {
|
||||
attrs.push((format!("{quote_ws}{RUST_END}"), Span::call_site()))
|
||||
} else {
|
||||
attrs.push((format!("{quote_ws}{RSX_END}"), Span::call_site()))
|
||||
}
|
||||
attrs.push((format!("{quote_ws}{quotes}"), Span::call_site()))
|
||||
}
|
||||
|
||||
Self(attrs)
|
||||
}
|
||||
@@ -320,57 +425,22 @@ impl Docs {
|
||||
self.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, attr)| {
|
||||
match attr.parse_meta() {
|
||||
Ok(Meta::NameValue(MetaNameValue { lit: doc, .. })) => {
|
||||
let doc_str = quote!(#doc);
|
||||
.map(|(idx, (doc, span))| {
|
||||
let doc = if idx == 0 {
|
||||
format!(" - {doc}")
|
||||
} else {
|
||||
format!(" {doc}")
|
||||
};
|
||||
|
||||
// We need to remove the leading and trailing `"`"
|
||||
let mut doc_str = doc_str.to_string();
|
||||
doc_str.pop();
|
||||
doc_str.remove(0);
|
||||
let doc = LitStr::new(&doc, *span);
|
||||
|
||||
let doc_str = if idx == 0 {
|
||||
format!(" - {doc_str}")
|
||||
} else {
|
||||
format!(" {doc_str}")
|
||||
};
|
||||
|
||||
let docs = LitStr::new(&doc_str, doc.span());
|
||||
|
||||
if !doc_str.is_empty() {
|
||||
quote! { #[doc = #docs] }
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
}
|
||||
_ => abort!(attr, "could not parse attributes"),
|
||||
}
|
||||
quote! { #[doc = #doc] }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn typed_builder(&self) -> String {
|
||||
#[allow(unstable_name_collisions)]
|
||||
let doc_str = self
|
||||
.0
|
||||
.iter()
|
||||
.map(|attr| {
|
||||
match attr.parse_meta() {
|
||||
Ok(Meta::NameValue(MetaNameValue { lit: doc, .. })) => {
|
||||
let mut doc_str = quote!(#doc).to_string();
|
||||
|
||||
// Remove the leading and trailing `"`
|
||||
doc_str.pop();
|
||||
doc_str.remove(0);
|
||||
|
||||
doc_str
|
||||
}
|
||||
_ => abort!(attr, "could not parse attributes"),
|
||||
}
|
||||
})
|
||||
.intersperse("\n".to_string())
|
||||
.collect::<String>();
|
||||
let doc_str = self.0.iter().map(|s| s.0.as_str()).join("\n");
|
||||
|
||||
if doc_str.chars().filter(|c| *c != '\n').count() != 0 {
|
||||
format!("\n\n{doc_str}")
|
||||
|
||||
@@ -816,7 +816,7 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
|
||||
/// They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
|
||||
/// `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
|
||||
/// - **The `Scope` comes from the server.** Optionally, the first argument of a server function
|
||||
|
||||
@@ -842,17 +842,23 @@ where
|
||||
_ = location;
|
||||
}
|
||||
#[cfg(all(feature = "hydrate", debug_assertions))]
|
||||
crate::macros::debug_warn!(
|
||||
"At {location}, you are reading a resource in `hydrate` mode \
|
||||
outside a <Suspense/> or <Transition/>. This can cause \
|
||||
hydration mismatch errors and loses out on a significant \
|
||||
performance optimization. To fix this issue, you can either: \
|
||||
\n1. Wrap the place where you read the resource in a \
|
||||
<Suspense/> or <Transition/> component, or \n2. Switch to \
|
||||
using create_local_resource(), which will wait to load the \
|
||||
resource until the app is hydrated on the client side. (This \
|
||||
will have worse performance in most cases.)",
|
||||
);
|
||||
{
|
||||
if self.serializable != ResourceSerialization::Local {
|
||||
crate::macros::debug_warn!(
|
||||
"At {location}, you are reading a resource in \
|
||||
`hydrate` mode outside a <Suspense/> or \
|
||||
<Transition/>. This can cause hydration mismatch \
|
||||
errors and loses out on a significant performance \
|
||||
optimization. To fix this issue, you can either: \
|
||||
\n1. Wrap the place where you read the resource in a \
|
||||
<Suspense/> or <Transition/> component, or \n2. \
|
||||
Switch to using create_local_resource(), which will \
|
||||
wait to load the resource until the app is hydrated \
|
||||
on the client side. (This will have worse \
|
||||
performance in most cases.)",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let increment = move |_: Option<()>| {
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
//! need to deserialize the result to return it to the client.
|
||||
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
||||
//! form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
//! form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
||||
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
|
||||
//! `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
|
||||
//! - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-alpha"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.2.5"
|
||||
version = "0.3.0-alpha"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -21,7 +21,7 @@ regex = { version = "1", optional = true }
|
||||
url = { version = "2", optional = true }
|
||||
percent-encoding = "2"
|
||||
thiserror = "1"
|
||||
serde_urlencoded = "0.7"
|
||||
serde_qs = "0.12"
|
||||
serde = "1"
|
||||
tracing = "0.1"
|
||||
js-sys = { version = "0.3" }
|
||||
|
||||
@@ -537,14 +537,12 @@ where
|
||||
Self: Sized + serde::de::DeserializeOwned,
|
||||
{
|
||||
/// Tries to deserialize the data, given only the `submit` event.
|
||||
fn from_event(
|
||||
ev: &web_sys::Event,
|
||||
) -> Result<Self, serde_urlencoded::de::Error>;
|
||||
fn from_event(ev: &web_sys::Event) -> Result<Self, serde_qs::Error>;
|
||||
|
||||
/// Tries to deserialize the data, given the actual form data.
|
||||
fn from_form_data(
|
||||
form_data: &web_sys::FormData,
|
||||
) -> Result<Self, serde_urlencoded::de::Error>;
|
||||
) -> Result<Self, serde_qs::Error>;
|
||||
}
|
||||
|
||||
impl<T> FromFormData for T
|
||||
@@ -555,9 +553,7 @@ where
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn from_event(
|
||||
ev: &web_sys::Event,
|
||||
) -> Result<Self, serde_urlencoded::de::Error> {
|
||||
fn from_event(ev: &web_sys::Event) -> Result<Self, serde_qs::Error> {
|
||||
let (form, _, _, _) = extract_form_attributes(ev);
|
||||
|
||||
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
|
||||
@@ -570,11 +566,11 @@ where
|
||||
)]
|
||||
fn from_form_data(
|
||||
form_data: &web_sys::FormData,
|
||||
) -> Result<Self, serde_urlencoded::de::Error> {
|
||||
) -> Result<Self, serde_qs::Error> {
|
||||
let data =
|
||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
|
||||
.unwrap_throw();
|
||||
let data = data.to_string().as_string().unwrap_or_default();
|
||||
serde_urlencoded::from_str::<Self>(&data)
|
||||
serde_qs::from_str::<Self>(&data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ use crate::{
|
||||
RouteDefinition, RouteMatch,
|
||||
},
|
||||
use_is_back_navigation, RouteContext, RouterContext,
|
||||
ApiRouteListing
|
||||
};
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
cmp::Reverse,
|
||||
collections::HashMap,
|
||||
ops::IndexMut,
|
||||
rc::Rc,
|
||||
};
|
||||
@@ -33,20 +35,25 @@ pub fn Routes(
|
||||
) -> impl IntoView {
|
||||
let router = use_context::<RouterContext>(cx)
|
||||
.expect("<Routes/> component should be nested within a <Router/>.");
|
||||
let base_route = router.base();
|
||||
|
||||
Branches::initialize(&base.unwrap_or_default(), children(cx));
|
||||
let base_route = router.base();
|
||||
let base = base.unwrap_or_default();
|
||||
|
||||
Branches::initialize(&base, children(cx));
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
|
||||
Branches::with(|branches| *context.0.borrow_mut() = branches.to_vec());
|
||||
Branches::with(&base, |branches| {
|
||||
*context.ui.borrow_mut() = branches.to_vec();
|
||||
});
|
||||
}
|
||||
|
||||
let next_route = router.pathname();
|
||||
let current_route = next_route;
|
||||
|
||||
let root_equal = Rc::new(Cell::new(true));
|
||||
let route_states = route_states(cx, &router, current_route, &root_equal);
|
||||
let route_states =
|
||||
route_states(cx, base, &router, current_route, &root_equal);
|
||||
|
||||
let id = HydrationCtx::id();
|
||||
let root = root_route(cx, base_route, route_states, root_equal);
|
||||
@@ -103,13 +110,17 @@ pub fn AnimatedRoutes(
|
||||
) -> impl IntoView {
|
||||
let router = use_context::<RouterContext>(cx)
|
||||
.expect("<Routes/> component should be nested within a <Router/>.");
|
||||
let base_route = router.base();
|
||||
|
||||
Branches::initialize(&base.unwrap_or_default(), children(cx));
|
||||
let base_route = router.base();
|
||||
let base = base.unwrap_or_default();
|
||||
|
||||
Branches::initialize(&base, children(cx));
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
|
||||
Branches::with(|branches| *context.0.borrow_mut() = branches.to_vec());
|
||||
Branches::with(&base, |branches| {
|
||||
*context.ui.borrow_mut() = branches.to_vec()
|
||||
});
|
||||
}
|
||||
|
||||
let animation = Animation {
|
||||
@@ -128,13 +139,16 @@ pub fn AnimatedRoutes(
|
||||
let is_complete = Rc::new(Cell::new(true));
|
||||
let animation_and_route = create_memo(cx, {
|
||||
let is_complete = Rc::clone(&is_complete);
|
||||
let base = base.clone();
|
||||
|
||||
move |prev: Option<&(AnimationState, String)>| {
|
||||
let animation_state = animation_state.get();
|
||||
let next_route = next_route.get();
|
||||
let prev_matches =
|
||||
prev.map(|(_, r)| r).cloned().map(get_route_matches);
|
||||
let matches = get_route_matches(next_route.clone());
|
||||
let prev_matches = prev
|
||||
.map(|(_, r)| r)
|
||||
.cloned()
|
||||
.map(|location| get_route_matches(&base, location));
|
||||
let matches = get_route_matches(&base, next_route.clone());
|
||||
let same_route = prev_matches
|
||||
.and_then(|p| p.get(0).as_ref().map(|r| r.route.key.clone()))
|
||||
== matches.get(0).as_ref().map(|r| r.route.key.clone());
|
||||
@@ -162,7 +176,8 @@ pub fn AnimatedRoutes(
|
||||
let current_route = create_memo(cx, move |_| animation_and_route.get().1);
|
||||
|
||||
let root_equal = Rc::new(Cell::new(true));
|
||||
let route_states = route_states(cx, &router, current_route, &root_equal);
|
||||
let route_states =
|
||||
route_states(cx, base, &router, current_route, &root_equal);
|
||||
|
||||
let root = root_route(cx, base_route, route_states, root_equal);
|
||||
let node_ref = create_node_ref::<html::Div>(cx);
|
||||
@@ -210,68 +225,85 @@ pub fn AnimatedRoutes(
|
||||
|
||||
pub(crate) struct Branches;
|
||||
|
||||
type AppRoutes = (Vec<Branch>, Vec<ApiRouteListing>);
|
||||
|
||||
thread_local! {
|
||||
static BRANCHES: RefCell<Option<Vec<Branch>>> = RefCell::new(None);
|
||||
// map is indexed by base
|
||||
// this allows multiple apps per server
|
||||
static BRANCHES: RefCell<HashMap<String, AppRoutes>> = Default::default();
|
||||
}
|
||||
|
||||
impl Branches {
|
||||
pub fn initialize(base: &str, children: Fragment) {
|
||||
BRANCHES.with(|branches| {
|
||||
let mut current = branches.borrow_mut();
|
||||
if current.is_none() {
|
||||
let mut branches = Vec::new();
|
||||
let children = children
|
||||
if !current.contains_key(base) {
|
||||
let mut branches = (Vec::new(), Vec::new());
|
||||
let mut route_defs = Vec::new();
|
||||
let mut api_routes = Vec::new();
|
||||
for child in children
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
let def = child
|
||||
.as_transparent()
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>());
|
||||
if def.is_none() {
|
||||
.iter() {
|
||||
let transparent = child.as_transparent();
|
||||
if let Some(def) = transparent.and_then(|t| t.downcast_ref::<RouteDefinition>()) {
|
||||
route_defs.push(def.clone());
|
||||
} else if let Some(def) = transparent.and_then(|t| t.downcast_ref::<ApiRouteListing>()) {
|
||||
api_routes.push(def.clone());
|
||||
} else {
|
||||
warn!(
|
||||
"[NOTE] The <Routes/> component should \
|
||||
include *only* <Route/>or <ProtectedRoute/> \
|
||||
include *only* <Route/> or <ProtectedRoute/> or <ApiRoute/> \
|
||||
components, or some \
|
||||
#[component(transparent)] that returns a \
|
||||
RouteDefinition."
|
||||
);
|
||||
}
|
||||
def
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
create_branches(
|
||||
&children,
|
||||
&route_defs,
|
||||
base,
|
||||
&mut Vec::new(),
|
||||
&mut branches,
|
||||
);
|
||||
*current = Some(branches);
|
||||
current.insert(base.to_string(), branches);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with<T>(cb: impl FnOnce(&[Branch]) -> T) -> T {
|
||||
pub fn with<T>(base: &str, cb: impl FnOnce(&[Branch]) -> T) -> T {
|
||||
BRANCHES.with(|branches| {
|
||||
let branches = branches.borrow();
|
||||
let branches = branches.as_ref().expect(
|
||||
let branches = branches.get(base).expect(
|
||||
"Branches::initialize() should be called before \
|
||||
Branches::with()",
|
||||
);
|
||||
cb(branches)
|
||||
cb(&branches.0)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_api_routes<T>(base: &str, cb: impl FnOnce(&[ApiRouteListing]) -> T) -> T {
|
||||
BRANCHES.with(|branches| {
|
||||
let branches = branches.borrow();
|
||||
let branches = branches.get(base).expect(
|
||||
"Branches::initialize() should be called before \
|
||||
Branches::with()",
|
||||
);
|
||||
cb(&branches.1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn route_states(
|
||||
cx: Scope,
|
||||
base: String,
|
||||
router: &RouterContext,
|
||||
current_route: Memo<String>,
|
||||
root_equal: &Rc<Cell<bool>>,
|
||||
) -> Memo<RouterState> {
|
||||
// whenever path changes, update matches
|
||||
let matches =
|
||||
create_memo(cx, move |_| get_route_matches(current_route.get()));
|
||||
create_memo(cx, move |_| get_route_matches(&base, current_route.get()));
|
||||
|
||||
// iterate over the new matches, reusing old routes when they are the same
|
||||
// and replacing them with new routes when they differ
|
||||
@@ -466,7 +498,7 @@ fn create_branches(
|
||||
route_defs: &[RouteDefinition],
|
||||
base: &str,
|
||||
stack: &mut Vec<RouteData>,
|
||||
branches: &mut Vec<Branch>,
|
||||
branches: &mut (Vec<Branch>, Vec<ApiRouteListing>),
|
||||
) {
|
||||
for def in route_defs {
|
||||
let routes = create_routes(def, base);
|
||||
@@ -474,8 +506,8 @@ fn create_branches(
|
||||
stack.push(route.clone());
|
||||
|
||||
if def.children.is_empty() {
|
||||
let branch = create_branch(stack, branches.len());
|
||||
branches.push(branch);
|
||||
let branch = create_branch(stack, branches.0.len());
|
||||
branches.0.push(branch);
|
||||
} else {
|
||||
create_branches(&def.children, &route.pattern, stack, branches);
|
||||
}
|
||||
@@ -485,7 +517,7 @@ fn create_branches(
|
||||
}
|
||||
|
||||
if stack.is_empty() {
|
||||
branches.sort_by_key(|branch| Reverse(branch.score));
|
||||
branches.0.sort_by_key(|branch| Reverse(branch.score));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,24 @@ use crate::{
|
||||
Branch, Method, RouterIntegrationContext, ServerIntegration, SsrMode,
|
||||
};
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
||||
use std::{any::Any, cell::RefCell, collections::HashSet, rc::Rc, sync::Arc};
|
||||
|
||||
/// Context to contain all possible routes.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct PossibleBranchContext(pub(crate) Rc<RefCell<Vec<Branch>>>);
|
||||
pub struct PossibleBranchContext {
|
||||
pub(crate) ui: Rc<RefCell<Vec<Branch>>>,
|
||||
pub(crate) api: Rc<RefCell<Vec<ApiRouteListing>>>
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// A route that this application can serve.
|
||||
pub enum PossibleRouteListing {
|
||||
View(RouteListing),
|
||||
Api(ApiRouteListing)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
/// A route that this application can serve.
|
||||
/// Route listing for a component-based view.
|
||||
pub struct RouteListing {
|
||||
path: String,
|
||||
mode: SsrMode,
|
||||
@@ -46,12 +56,69 @@ impl RouteListing {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// Route listing for an API route.
|
||||
pub struct ApiRouteListing {
|
||||
path: String,
|
||||
methods: Option<HashSet<Method>>,
|
||||
// this will be downcast by the implementation
|
||||
handler: Arc<dyn Any>
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ApiRouteListing {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ApiRouteListing").field("path", &self.path).field("methods", &self.methods).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiRouteListing {
|
||||
/// Create an API route listing from its parts.
|
||||
pub fn new<T: 'static>(
|
||||
path: impl ToString,
|
||||
handler: T
|
||||
) -> Self {
|
||||
Self {
|
||||
path: path.to_string(),
|
||||
methods: None,
|
||||
handler: Arc::new(handler)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an API route listing from its parts.
|
||||
pub fn new_with_methods<T: 'static>(
|
||||
path: impl ToString,
|
||||
methods: impl IntoIterator<Item = Method>,
|
||||
handler: T
|
||||
) -> Self {
|
||||
Self {
|
||||
path: path.to_string(),
|
||||
methods: Some(methods.into_iter().collect()),
|
||||
handler: Arc::new(handler)
|
||||
}
|
||||
}
|
||||
|
||||
/// The path this route handles.
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// The HTTP request methods this path can handle.
|
||||
pub fn methods(&self) -> impl Iterator<Item = Method> + '_ {
|
||||
self.methods.iter().flatten().copied()
|
||||
}
|
||||
|
||||
/// The handler for a request at this route
|
||||
pub fn handler<T: 'static>(&self) -> Option<&T> {
|
||||
self.handler.downcast_ref::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router
|
||||
/// format. Odds are you want `generate_route_list()` from either the actix, axum, or viz integrations if you want
|
||||
/// to work with their router
|
||||
pub fn generate_route_list_inner<IV>(
|
||||
app_fn: impl FnOnce(Scope) -> IV + 'static,
|
||||
) -> Vec<RouteListing>
|
||||
) -> (Vec<RouteListing>, Vec<ApiRouteListing>)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -69,8 +136,8 @@ where
|
||||
_ = app_fn(cx).into_view(cx);
|
||||
leptos::suppress_resource_load(false);
|
||||
|
||||
let branches = branches.0.borrow();
|
||||
branches
|
||||
let ui_branches = branches.ui.borrow();
|
||||
let ui = ui_branches
|
||||
.iter()
|
||||
.flat_map(|branch| {
|
||||
let mode = branch
|
||||
@@ -93,6 +160,12 @@ where
|
||||
methods: methods.clone(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
let api_branches = branches.api.borrow();
|
||||
let api = api_branches
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
(ui, api)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use linear_map::LinearMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{rc::Rc, str::FromStr};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use thiserror::Error;
|
||||
|
||||
/// A key-value map of the current named route params and their values.
|
||||
@@ -123,7 +123,7 @@ where
|
||||
impl<T> IntoParam for Option<T>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::error::Error + 'static,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(
|
||||
value: Option<&str>,
|
||||
@@ -133,10 +133,7 @@ where
|
||||
None => Ok(None),
|
||||
Some(value) => match T::from_str(value) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
Err(ParamsError::Params(Rc::new(e)))
|
||||
}
|
||||
Err(e) => Err(ParamsError::Params(Arc::new(e))),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -154,7 +151,7 @@ cfg_if::cfg_if! {
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
|
||||
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
|
||||
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
|
||||
Self::from_str(value).map_err(|e| ParamsError::Params(Arc::new(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +165,7 @@ pub enum ParamsError {
|
||||
MissingParam(String),
|
||||
/// Something went wrong while deserializing a field.
|
||||
#[error("failed to deserialize parameters")]
|
||||
Params(Rc<dyn std::error::Error>),
|
||||
Params(Arc<dyn std::error::Error + Send + Sync>),
|
||||
}
|
||||
|
||||
impl PartialEq for ParamsError {
|
||||
|
||||
@@ -16,7 +16,10 @@ pub(crate) struct RouteMatch {
|
||||
pub route: RouteData,
|
||||
}
|
||||
|
||||
pub(crate) fn get_route_matches(location: String) -> Rc<Vec<RouteMatch>> {
|
||||
pub(crate) fn get_route_matches(
|
||||
base: &str,
|
||||
location: String,
|
||||
) -> Rc<Vec<RouteMatch>> {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use lru::LruCache;
|
||||
@@ -28,17 +31,17 @@ pub(crate) fn get_route_matches(location: String) -> Rc<Vec<RouteMatch>> {
|
||||
ROUTE_MATCH_CACHE.with(|cache| {
|
||||
let mut cache = cache.borrow_mut();
|
||||
Rc::clone(cache.get_or_insert(location.clone(), || {
|
||||
build_route_matches(location)
|
||||
build_route_matches(base, location)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
build_route_matches(location)
|
||||
build_route_matches(base, location)
|
||||
}
|
||||
|
||||
fn build_route_matches(location: String) -> Rc<Vec<RouteMatch>> {
|
||||
Rc::new(Branches::with(|branches| {
|
||||
fn build_route_matches(base: &str, location: String) -> Rc<Vec<RouteMatch>> {
|
||||
Rc::new(Branches::with(base, |branches| {
|
||||
for branch in branches {
|
||||
if let Some(matches) = branch.matcher(&location) {
|
||||
return matches;
|
||||
|
||||
@@ -11,7 +11,7 @@ readme = "../README.md"
|
||||
[dependencies]
|
||||
server_fn_macro_default = { workspace = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_urlencoded = "0.7"
|
||||
serde_qs = "0.12"
|
||||
thiserror = "1"
|
||||
serde_json = "1"
|
||||
quote = "1"
|
||||
|
||||
@@ -49,7 +49,7 @@ use syn::__private::ToTokens;
|
||||
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
|
||||
/// They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
#[proc_macro_attribute]
|
||||
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
//! need to deserialize the result to return it to the client.
|
||||
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
||||
//! form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
//! form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
||||
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
|
||||
// used by the macro
|
||||
@@ -308,7 +308,7 @@ where
|
||||
// decode the args
|
||||
let value = match Self::encoding() {
|
||||
Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => {
|
||||
serde_urlencoded::from_bytes(data).map_err(|e| {
|
||||
serde_qs::from_bytes(data).map_err(|e| {
|
||||
ServerFnError::Deserialization(e.to_string())
|
||||
})
|
||||
}
|
||||
@@ -408,7 +408,7 @@ where
|
||||
}
|
||||
let args_encoded = match &enc {
|
||||
Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => Payload::Url(
|
||||
serde_urlencoded::to_string(&args)
|
||||
serde_qs::to_string(&args)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?,
|
||||
),
|
||||
Encoding::Cbor => {
|
||||
|
||||
Reference in New Issue
Block a user