mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
20 Commits
diagnostic
...
gbj-patch-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8943539043 | ||
|
|
f6a272498d | ||
|
|
aef7c4ce8e | ||
|
|
b29eb8e032 | ||
|
|
da9183f4b5 | ||
|
|
ae3ddcb0e6 | ||
|
|
c6b8f0e8ed | ||
|
|
bab9f40a81 | ||
|
|
c2cfdf3678 | ||
|
|
8967eadc02 | ||
|
|
4cc65f837f | ||
|
|
22706e7371 | ||
|
|
9f9302662c | ||
|
|
6b90e1babd | ||
|
|
7e540a8f49 | ||
|
|
f06ffd72aa | ||
|
|
83d3d7579c | ||
|
|
39edb6eb45 | ||
|
|
d81c1a929e | ||
|
|
f69c28df18 |
28
Cargo.toml
28
Cargo.toml
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.4.0" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.0" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.0" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.0" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.0" }
|
||||
leptos_router = { path = "./router", version = "0.4.0" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.0" }
|
||||
leptos = { path = "./leptos", version = "0.4.2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.2" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.2" }
|
||||
leptos_router = { path = "./router", version = "0.4.2" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.2" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -25,9 +25,14 @@ cargo init leptos-tutorial
|
||||
> ```bash
|
||||
> rustup toolchain install nightly
|
||||
> rustup default nightly
|
||||
> rustup target add wasm32-unknown-unknown
|
||||
> ```
|
||||
|
||||
Make sure you've added the `wasm32-unknown-unknown` target do that Rust can compile your code to WebAssembly to run in the browser.
|
||||
|
||||
```bash
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
|
||||
```bash
|
||||
|
||||
@@ -198,7 +198,7 @@ let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<Show
|
||||
when=move || value() > 5
|
||||
when=move || { value() > 5 }
|
||||
fallback=|cx| view! { cx, <Small/> }
|
||||
>
|
||||
<Big/>
|
||||
|
||||
@@ -19,7 +19,7 @@ fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<label>
|
||||
"Type a number (or not!)"
|
||||
<input type="number" on:input=on_input/>
|
||||
<input on:input=on_input/>
|
||||
<p>
|
||||
"You entered "
|
||||
<strong>{value}</strong>
|
||||
@@ -69,7 +69,7 @@ fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
<h1>"Error Handling"</h1>
|
||||
<label>
|
||||
"Type a number (or something that's not a number!)"
|
||||
<input type="number" on:input=on_input/>
|
||||
<input on:input=on_input/>
|
||||
<ErrorBoundary
|
||||
// the fallback receives a signal containing current errors
|
||||
fallback=|cx, errors| view! { cx,
|
||||
|
||||
@@ -117,7 +117,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn ButtonC<F>(cx: Scope) -> impl IntoView {
|
||||
pub fn ButtonC(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<button>"Toggle"</button>
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
wasm-bindgen = "=0.2.86"
|
||||
wasm-bindgen = "=0.2.87"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -67,24 +67,10 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
<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=Counter/>
|
||||
<Route path="form" view=FormCounter/>
|
||||
<Route path="multi" view=MultiuserCounter/>
|
||||
<Route path="multi" view=NotFound/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -175,13 +161,9 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
"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
|
||||
<ActionForm action=clear>
|
||||
<input type="submit" value="Clear"/>
|
||||
</ActionForm>
|
||||
// We can submit named arguments to the server functions
|
||||
// by including them as input values with the same name
|
||||
<ActionForm action=adjust>
|
||||
<input type="hidden" name="delta" value="-1"/>
|
||||
<input type="hidden" name="msg" value="form value down"/>
|
||||
@@ -256,3 +238,14 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NotFound(cx: Scope) -> impl IntoView {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let resp = expect_context::<leptos_actix::ResponseOptions>(cx);
|
||||
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
view! { cx, <h1>"Not Found"</h1> }
|
||||
}
|
||||
|
||||
@@ -52,15 +52,36 @@ cfg_if! {
|
||||
App::new()
|
||||
.service(counter_events)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <Counters/> })
|
||||
.service(Files::new("/", site_root))
|
||||
// serve JS/WASM/CSS from `pkg`
|
||||
.service(Files::new("/pkg", format!("{site_root}/pkg")))
|
||||
// serve other assets from the `assets` directory
|
||||
.service(Files::new("/assets", site_root))
|
||||
// serve the favicon from /favicon.ico
|
||||
.service(favicon)
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
Counters,
|
||||
)
|
||||
.app_data(web::Data::new(leptos_options.to_owned()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[actix_web::get("favicon.ico")]
|
||||
async fn favicon(
|
||||
leptos_options: actix_web::web::Data<leptos::LeptosOptions>,
|
||||
) -> actix_web::Result<actix_files::NamedFile> {
|
||||
let leptos_options = leptos_options.into_inner();
|
||||
let site_root = &leptos_options.site_root;
|
||||
Ok(actix_files::NamedFile::open(format!(
|
||||
"{site_root}/favicon.ico"
|
||||
))?)
|
||||
}
|
||||
}
|
||||
|
||||
// client-only main for Trunk
|
||||
else {
|
||||
|
||||
@@ -5,10 +5,23 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
leptos_meta = { path = "../../meta", features = ["csr"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
wasm-bindgen = "0.2.87"
|
||||
wasm-bindgen-test = "0.3.37"
|
||||
pretty_assertions = "1.3.0"
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
features = [
|
||||
"Event",
|
||||
"EventInit",
|
||||
"EventTarget",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement",
|
||||
"XPathResult",
|
||||
]
|
||||
version = "0.3.64"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-test.toml" },
|
||||
]
|
||||
|
||||
@@ -14,7 +14,6 @@ test.describe("Add 1000 Counters", () => {
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("3000");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ test.describe("Add Counter", () => {
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("3");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,5 @@ test.describe("Decrement Count", () => {
|
||||
await ui.decrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("-3");
|
||||
await expect(ui.counters).toHaveText("1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,5 @@ test.describe("Enter Count", () => {
|
||||
await ui.enterCount("50", 1);
|
||||
|
||||
await expect(ui.total).toHaveText("250");
|
||||
await expect(ui.counters).toHaveText("3");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,5 @@ test.describe("Increment Count", () => {
|
||||
await ui.incrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("3");
|
||||
await expect(ui.counters).toHaveText("1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ test.describe("Remove Counter", () => {
|
||||
|
||||
await ui.removeCounter(1);
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("2");
|
||||
});
|
||||
});
|
||||
|
||||
109
examples/counters_stable/src/lib.rs
Normal file
109
examples/counters_stable/src/lib.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct CounterUpdater {
|
||||
set_counters: WriteSignal<CounterHolder>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
|
||||
provide_context(cx, CounterUpdater { set_counters });
|
||||
provide_meta_context(cx);
|
||||
|
||||
let add_counter = move |_| {
|
||||
let id = next_counter_id.get();
|
||||
let sig = create_signal(cx, 0);
|
||||
set_counters.update(move |counters| counters.push((id, sig)));
|
||||
set_next_counter_id.update(|id| *id += 1);
|
||||
};
|
||||
|
||||
let add_many_counters = move |_| {
|
||||
let next_id = next_counter_id.get();
|
||||
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
|
||||
let signal = create_signal(cx, 0);
|
||||
(id, signal)
|
||||
});
|
||||
|
||||
set_counters.update(move |counters| counters.extend(new_counters));
|
||||
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
|
||||
};
|
||||
|
||||
let clear_counters = move |_| {
|
||||
set_counters.update(|counters| counters.clear());
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<Title text="Counters (Stable)" />
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
<button on:click=add_many_counters>
|
||||
{format!("Add {MANY_COUNTERS} Counters")}
|
||||
</button>
|
||||
<button on:click=clear_counters>
|
||||
"Clear Counters"
|
||||
</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span data-testid="total">{move ||
|
||||
counters.get()
|
||||
.iter()
|
||||
.map(|(_, (count, _))| count.get())
|
||||
.sum::<i32>()
|
||||
.to_string()
|
||||
}</span>
|
||||
" from "
|
||||
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
<For
|
||||
each={move || counters.get()}
|
||||
key={|counter| counter.0}
|
||||
view=move |cx, (id, (value, set_value))| {
|
||||
view! {
|
||||
cx,
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Counter(
|
||||
cx: Scope,
|
||||
id: usize,
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context(cx).unwrap();
|
||||
|
||||
let input = move |ev| {
|
||||
set_value
|
||||
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li>
|
||||
<button data-testid="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input data-testid="counter_input" type="text"
|
||||
prop:value={move || value.get().to_string()}
|
||||
on:input=input
|
||||
/>
|
||||
<span>{value}</span>
|
||||
<button data-testid="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button data-testid="remove_counter" on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use counters_stable::Counters;
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
@@ -5,108 +6,3 @@ fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <Counters/> })
|
||||
}
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct CounterUpdater {
|
||||
set_counters: WriteSignal<CounterHolder>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
|
||||
provide_context(cx, CounterUpdater { set_counters });
|
||||
|
||||
let add_counter = move |_| {
|
||||
let id = next_counter_id.get();
|
||||
let sig = create_signal(cx, 0);
|
||||
set_counters.update(move |counters| counters.push((id, sig)));
|
||||
set_next_counter_id.update(|id| *id += 1);
|
||||
};
|
||||
|
||||
let add_many_counters = move |_| {
|
||||
let next_id = next_counter_id.get();
|
||||
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
|
||||
let signal = create_signal(cx, 0);
|
||||
(id, signal)
|
||||
});
|
||||
|
||||
set_counters.update(move |counters| counters.extend(new_counters));
|
||||
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
|
||||
};
|
||||
|
||||
let clear_counters = move |_| {
|
||||
set_counters.update(|counters| counters.clear());
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
<button on:click=add_many_counters>
|
||||
{format!("Add {MANY_COUNTERS} Counters")}
|
||||
</button>
|
||||
<button on:click=clear_counters>
|
||||
"Clear Counters"
|
||||
</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span data-testid="total">{move ||
|
||||
counters.get()
|
||||
.iter()
|
||||
.map(|(_, (count, _))| count.get())
|
||||
.sum::<i32>()
|
||||
.to_string()
|
||||
}</span>
|
||||
" from "
|
||||
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
<For
|
||||
each={move || counters.get()}
|
||||
key={|counter| counter.0}
|
||||
view=move |cx, (id, (value, set_value))| {
|
||||
view! {
|
||||
cx,
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Counter(
|
||||
cx: Scope,
|
||||
id: usize,
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context(cx).unwrap();
|
||||
|
||||
let input = move |ev| {
|
||||
set_value
|
||||
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li>
|
||||
<button id="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input type="text"
|
||||
prop:value={move || value.get().to_string()}
|
||||
on:input=input
|
||||
/>
|
||||
<span>{value}</span>
|
||||
<button id="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
17
examples/counters_stable/tests/web/add_1k_counters.rs
Normal file
17
examples/counters_stable/tests/web/add_1k_counters.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_number_of_counters() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
|
||||
// When
|
||||
ui::add_1k_counters();
|
||||
ui::add_1k_counters();
|
||||
ui::add_1k_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::counters(), 3000);
|
||||
}
|
||||
17
examples/counters_stable/tests/web/add_counter.rs
Normal file
17
examples/counters_stable/tests/web/add_counter.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_number_of_counters() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
|
||||
// When
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::counters(), 3);
|
||||
}
|
||||
19
examples/counters_stable/tests/web/clear_counters.rs
Normal file
19
examples/counters_stable/tests/web/clear_counters.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_reset_the_counts() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::clear_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 0);
|
||||
assert_eq!(ui::counters(), 0);
|
||||
}
|
||||
18
examples/counters_stable/tests/web/decrement_counter.rs
Normal file
18
examples/counters_stable/tests/web/decrement_counter.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrease_the_total_count() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::decrement_counter(1);
|
||||
ui::decrement_counter(1);
|
||||
ui::decrement_counter(1);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), -3);
|
||||
}
|
||||
34
examples/counters_stable/tests/web/enter_count.rs
Normal file
34
examples/counters_stable/tests/web/enter_count.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_total_count() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::enter_count(1, 5);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 5);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrease_the_total_count() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::enter_count(1, 100);
|
||||
ui::enter_count(2, 100);
|
||||
ui::enter_count(3, 100);
|
||||
ui::enter_count(1, 50);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 250);
|
||||
}
|
||||
112
examples/counters_stable/tests/web/fixtures/counters_page.rs
Normal file
112
examples/counters_stable/tests/web/fixtures/counters_page.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use counters_stable::Counters;
|
||||
use leptos::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Element, Event, EventInit, HtmlElement, HtmlInputElement};
|
||||
|
||||
// Actions
|
||||
|
||||
pub fn add_1k_counters() {
|
||||
find_by_text("Add 1000 Counters").click();
|
||||
}
|
||||
|
||||
pub fn add_counter() {
|
||||
find_by_text("Add Counter").click();
|
||||
}
|
||||
|
||||
pub fn clear_counters() {
|
||||
find_by_text("Clear Counters").click();
|
||||
}
|
||||
|
||||
pub fn decrement_counter(index: u32) {
|
||||
counter_html_element(index, "decrement_count").click();
|
||||
}
|
||||
|
||||
pub fn enter_count(index: u32, count: i32) {
|
||||
let input = counter_input_element(index, "counter_input");
|
||||
input.set_value(count.to_string().as_str());
|
||||
let mut event_init = EventInit::new();
|
||||
event_init.bubbles(true);
|
||||
let event = Event::new_with_event_init_dict("input", &event_init).unwrap();
|
||||
input.dispatch_event(&event).unwrap();
|
||||
}
|
||||
|
||||
pub fn increment_counter(index: u32) {
|
||||
counter_html_element(index, "increment_count").click();
|
||||
}
|
||||
|
||||
pub fn remove_counter(index: u32) {
|
||||
counter_html_element(index, "remove_counter").click();
|
||||
}
|
||||
|
||||
pub fn view_counters() {
|
||||
remove_existing_counters();
|
||||
mount_to_body(|cx| view! { cx, <Counters/> });
|
||||
}
|
||||
|
||||
// Results
|
||||
|
||||
pub fn counters() -> i32 {
|
||||
data_test_id("counters").parse::<i32>().unwrap()
|
||||
}
|
||||
|
||||
pub fn title() -> String {
|
||||
leptos::document().title()
|
||||
}
|
||||
|
||||
pub fn total() -> i32 {
|
||||
data_test_id("total").parse::<i32>().unwrap()
|
||||
}
|
||||
|
||||
// Internal
|
||||
|
||||
fn counter_element(index: u32, text: &str) -> Element {
|
||||
let selector =
|
||||
format!("li:nth-child({}) [data-testid=\"{}\"]", index, text);
|
||||
leptos::document()
|
||||
.query_selector(&selector)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn counter_html_element(index: u32, text: &str) -> HtmlElement {
|
||||
counter_element(index, text)
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn counter_input_element(index: u32, text: &str) -> HtmlInputElement {
|
||||
counter_element(index, text)
|
||||
.dyn_into::<HtmlInputElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn data_test_id(id: &str) -> String {
|
||||
let selector = format!("[data-testid=\"{}\"]", id);
|
||||
leptos::document()
|
||||
.query_selector(&selector)
|
||||
.unwrap()
|
||||
.expect("counters not found")
|
||||
.text_content()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn find_by_text(text: &str) -> HtmlElement {
|
||||
let xpath = format!("//*[text()='{}']", text);
|
||||
let document = leptos::document();
|
||||
document
|
||||
.evaluate(&xpath, &document)
|
||||
.unwrap()
|
||||
.iterate_next()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn remove_existing_counters() {
|
||||
if let Some(counter) =
|
||||
leptos::document().query_selector("body div").unwrap()
|
||||
{
|
||||
counter.remove();
|
||||
}
|
||||
}
|
||||
1
examples/counters_stable/tests/web/fixtures/mod.rs
Normal file
1
examples/counters_stable/tests/web/fixtures/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod counters_page;
|
||||
18
examples/counters_stable/tests/web/increment_counter.rs
Normal file
18
examples/counters_stable/tests/web/increment_counter.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_total_count() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::increment_counter(1);
|
||||
ui::increment_counter(1);
|
||||
ui::increment_counter(1);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 3);
|
||||
}
|
||||
16
examples/counters_stable/tests/web/main.rs
Normal file
16
examples/counters_stable/tests/web/main.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
// Test Suites
|
||||
pub mod add_1k_counters;
|
||||
pub mod add_counter;
|
||||
pub mod clear_counters;
|
||||
pub mod decrement_counter;
|
||||
pub mod enter_count;
|
||||
pub mod increment_counter;
|
||||
pub mod remove_counter;
|
||||
pub mod view_counters;
|
||||
|
||||
pub mod fixtures;
|
||||
pub use fixtures::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
18
examples/counters_stable/tests/web/remove_counter.rs
Normal file
18
examples/counters_stable/tests/web/remove_counter.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrement_the_number_of_counters() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::remove_counter(2);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::counters(), 2);
|
||||
}
|
||||
22
examples/counters_stable/tests/web/view_counters.rs
Normal file
22
examples/counters_stable/tests/web/view_counters.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_see_the_initial_counts() {
|
||||
// When
|
||||
ui::view_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 0);
|
||||
assert_eq!(ui::counters(), 0);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_see_the_title() {
|
||||
// When
|
||||
ui::view_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::title(), "Counters (Stable)");
|
||||
}
|
||||
@@ -11,7 +11,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
<h1>"Error Handling"</h1>
|
||||
<label>
|
||||
"Type a number (or something that's not a number!)"
|
||||
<input type="number" on:input=on_input/>
|
||||
<input on:input=on_input/>
|
||||
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
|
||||
// the fallback will be displayed. Otherwise, the children of the
|
||||
// <ErrorBoundary/> will be displayed.
|
||||
|
||||
@@ -14,10 +14,6 @@ cfg_if! {
|
||||
async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
#[get("/favicon.ico")]
|
||||
async fn favicon() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./target/site//favicon.ico").await
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
@@ -33,17 +29,34 @@ cfg_if! {
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(css)
|
||||
.service(favicon)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <App/> })
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/pkg", format!("{site_root}/pkg")))
|
||||
.service(Files::new("/assets", site_root))
|
||||
.service(favicon)
|
||||
.service(css)
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.app_data(web::Data::new(leptos_options.to_owned()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[actix_web::get("favicon.ico")]
|
||||
async fn favicon(
|
||||
leptos_options: actix_web::web::Data<leptos::LeptosOptions>,
|
||||
) -> actix_web::Result<actix_files::NamedFile> {
|
||||
let leptos_options = leptos_options.into_inner();
|
||||
let site_root = &leptos_options.site_root;
|
||||
Ok(actix_files::NamedFile::open(format!(
|
||||
"{site_root}/favicon.ico"
|
||||
))?)
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
use hackernews::{App};
|
||||
|
||||
@@ -27,6 +27,11 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
view=Post
|
||||
ssr=SsrMode::Async
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/*any"
|
||||
view=NotFound
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -182,3 +187,14 @@ pub async fn get_post(id: usize) -> Result<Option<Post>, ServerFnError> {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
Ok(POSTS.iter().find(|post| post.id == id).cloned())
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NotFound(cx: Scope) -> impl IntoView {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let resp = expect_context::<leptos_actix::ResponseOptions>(cx);
|
||||
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
view! { cx, <h1>"Not Found"</h1> }
|
||||
}
|
||||
|
||||
@@ -24,12 +24,11 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
App::new()
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/pkg", format!("{site_root}/pkg")))
|
||||
.service(Files::new("/assets", site_root))
|
||||
.service(favicon)
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
|
||||
.app_data(web::Data::new(leptos_options.to_owned()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
@@ -37,6 +36,18 @@ async fn main() -> std::io::Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::get("favicon.ico")]
|
||||
async fn favicon(
|
||||
leptos_options: actix_web::web::Data<leptos::LeptosOptions>,
|
||||
) -> actix_web::Result<actix_files::NamedFile> {
|
||||
let leptos_options = leptos_options.into_inner();
|
||||
let site_root = &leptos_options.site_root;
|
||||
Ok(actix_files::NamedFile::open(format!(
|
||||
"{site_root}/favicon.ico"
|
||||
))?)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
|
||||
@@ -30,6 +30,25 @@ npm install -D tailwindcss
|
||||
|
||||
If you'd rather not use `npm`, you can install the Tailwind binary [here](https://github.com/tailwindlabs/tailwindcss/releases).
|
||||
|
||||
## Adding Tailwind plugins
|
||||
|
||||
If you'd like to add [Tailwind plugins](https://tailwindcss.com/docs/plugins), such as [DaisyUI](https://daisyui.com/), you can do the following:
|
||||
|
||||
`npm install -D daisyui@latest`
|
||||
|
||||
Then add the plugin to your exports in `tailwind.config.js` :
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
//...
|
||||
plugins: [require("daisyui")],
|
||||
};
|
||||
```
|
||||
|
||||
And re-run the following to generate the css:
|
||||
|
||||
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
|
||||
|
||||
## Setting up with VS Code and Additional Tools
|
||||
|
||||
If you're using VS Code, add the following to your `settings.json`
|
||||
|
||||
@@ -47,14 +47,32 @@ cfg_if! {
|
||||
App::new()
|
||||
.service(css)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <TodoApp/> })
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/pkg", format!("{site_root}/pkg")))
|
||||
.service(Files::new("/assets", site_root))
|
||||
.service(favicon)
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
TodoApp,
|
||||
)
|
||||
.app_data(web::Data::new(leptos_options.to_owned()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[actix_web::get("favicon.ico")]
|
||||
async fn favicon(
|
||||
leptos_options: actix_web::web::Data<leptos::LeptosOptions>,
|
||||
) -> actix_web::Result<actix_files::NamedFile> {
|
||||
let leptos_options = leptos_options.into_inner();
|
||||
let site_root = &leptos_options.site_root;
|
||||
Ok(actix_files::NamedFile::open(format!(
|
||||
"{site_root}/favicon.ico"
|
||||
))?)
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
// no client-side main function
|
||||
|
||||
@@ -92,10 +92,8 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<Todos/>
|
||||
}/>
|
||||
<Route path="" view=Todos/>
|
||||
<Route path="/*any" view=NotFound/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -200,3 +198,14 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NotFound(cx: Scope) -> impl IntoView {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let resp = expect_context::<leptos_actix::ResponseOptions>(cx);
|
||||
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
view! { cx, <h1>"Not Found"</h1> }
|
||||
}
|
||||
|
||||
26
flake.lock
generated
26
flake.lock
generated
@@ -38,11 +38,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1681920287,
|
||||
"narHash": "sha256-+/d6XQQfhhXVfqfLROJoqj3TuG38CAeoT6jO1g9r1k0=",
|
||||
"lastModified": 1687898314,
|
||||
"narHash": "sha256-B4BHon3uMXQw8ZdbwxRK1BmxVOGBV4viipKpGaIlGwk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "645bc49f34fa8eff95479f0345ff57e55b53437e",
|
||||
"rev": "e18dc963075ed115afb3e312b64643bf8fd4b474",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -52,22 +52,6 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681358109,
|
||||
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
@@ -78,7 +62,9 @@
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1682043560,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
@@ -22,7 +23,8 @@
|
||||
openssl
|
||||
pkg-config
|
||||
cacert
|
||||
(rust-bin.selectLatestNightlyWith( toolchain: toolchain.default.override {
|
||||
mdbook
|
||||
(rust-bin.selectLatestNightlyWith(toolchain: toolchain.default.override {
|
||||
extensions= [ "rust-src" "rust-analyzer" ];
|
||||
targets = [ "wasm32-unknown-unknown" ];
|
||||
}))
|
||||
|
||||
@@ -1276,18 +1276,24 @@ impl ExtractorHelper {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn extract<F, T, U>(&self, f: F) -> Result<U, T::Rejection>
|
||||
pub async fn extract<F, T, U, S>(
|
||||
&self,
|
||||
f: F,
|
||||
s: S,
|
||||
) -> Result<U, T::Rejection>
|
||||
where
|
||||
F: Extractor<T, U>,
|
||||
T: std::fmt::Debug + Send + FromRequestParts<()> + 'static,
|
||||
S: Sized,
|
||||
F: Extractor<T, U, S>,
|
||||
T: std::fmt::Debug + Send + FromRequestParts<S> + 'static,
|
||||
T::Rejection: std::fmt::Debug + Send + 'static,
|
||||
{
|
||||
let mut parts = self.parts.lock().await;
|
||||
let data = T::from_request_parts(&mut parts, &()).await?;
|
||||
let data = T::from_request_parts(&mut parts, &s).await?;
|
||||
Ok(f.call(data).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Getting ExtractorHelper from a request will return an ExtractorHelper whose state is ().
|
||||
impl<B> From<Request<B>> for ExtractorHelper {
|
||||
fn from(req: Request<B>) -> Self {
|
||||
// TODO provide body for extractors there, too?
|
||||
@@ -1324,34 +1330,71 @@ impl<B> From<Request<B>> for ExtractorHelper {
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub async fn extract<T, U>(
|
||||
cx: Scope,
|
||||
f: impl Extractor<T, U>,
|
||||
f: impl Extractor<T, U, ()>,
|
||||
) -> Result<U, T::Rejection>
|
||||
where
|
||||
T: std::fmt::Debug + Send + FromRequestParts<()> + 'static,
|
||||
T::Rejection: std::fmt::Debug + Send + 'static,
|
||||
{
|
||||
extract_with_state(cx, (), f).await
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions. This takes
|
||||
/// a handler function and state as its arguments. The handler rules similar to Axum
|
||||
/// [handlers](https://docs.rs/axum/latest/axum/extract/index.html#intro): it is an async function
|
||||
/// whose arguments are “extractors.”
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[server(QueryExtract, "/api")]
|
||||
/// pub async fn query_extract(cx: Scope) -> Result<String, ServerFnError> {
|
||||
/// use axum::{extract::Query, http::Method};
|
||||
/// use leptos_axum::extract;
|
||||
/// let state: ServerState = use_context::<crate::ServerState>(cx)
|
||||
/// .ok_or(ServerFnError::ServerError("No server state".to_string()))?;
|
||||
///
|
||||
/// extract_with_state(cx, state, |method: Method, res: Query<MyQuery>| async move {
|
||||
/// format!("{method:?} and {}", res.q)
|
||||
/// },
|
||||
/// )
|
||||
/// .await
|
||||
/// .map_err(|e| ServerFnError::ServerError("Could not extract method and query...".to_string()))
|
||||
/// }
|
||||
/// ```
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub async fn extract_with_state<T, U, S>(
|
||||
cx: Scope,
|
||||
state: S,
|
||||
f: impl Extractor<T, U, S>,
|
||||
) -> Result<U, T::Rejection>
|
||||
where
|
||||
S: Sized,
|
||||
T: std::fmt::Debug + Send + FromRequestParts<S> + 'static,
|
||||
T::Rejection: std::fmt::Debug + Send + 'static,
|
||||
{
|
||||
use_context::<ExtractorHelper>(cx)
|
||||
.expect(
|
||||
"should have had ExtractorHelper provided by the leptos_axum \
|
||||
integration",
|
||||
)
|
||||
.extract(f)
|
||||
.extract(f, state)
|
||||
.await
|
||||
}
|
||||
|
||||
pub trait Extractor<T, U>
|
||||
pub trait Extractor<T, U, S>
|
||||
where
|
||||
T: FromRequestParts<()>,
|
||||
S: Sized,
|
||||
T: FromRequestParts<S>,
|
||||
{
|
||||
fn call(self, args: T) -> Pin<Box<dyn Future<Output = U>>>;
|
||||
}
|
||||
|
||||
macro_rules! factory_tuple ({ $($param:ident)* } => {
|
||||
impl<Func, Fut, U, $($param,)*> Extractor<($($param,)*), U> for Func
|
||||
impl<Func, Fut, U, S, $($param,)*> Extractor<($($param,)*), U, S> for Func
|
||||
where
|
||||
$($param: FromRequestParts<()> + Send,)*
|
||||
$($param: FromRequestParts<S> + Send,)*
|
||||
Func: FnOnce($($param),*) -> Fut + 'static,
|
||||
Fut: Future<Output = U> + 'static,
|
||||
S: Sized + Send + Sync,
|
||||
{
|
||||
#[inline]
|
||||
#[allow(non_snake_case)]
|
||||
|
||||
@@ -55,6 +55,8 @@ fn view_fn(cx: Scope) -> impl IntoView {
|
||||
<hr/>
|
||||
<Test from=[1, 4, 3, 2, 5] to=[1, 2, 3, 4, 5]/>
|
||||
<Test from=[4, 5, 3, 1, 2] to=[1, 2, 3, 4, 5]/>
|
||||
<Test from=[0, 1, 2, 3] to=[1, 3]/> // issue #1274
|
||||
<Test from=[] to=[3, 9, 17] then=vec![3, 5, 7, 9, 17, 23]/> // issue #1297
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,8 +271,9 @@ where
|
||||
let mut repr = ComponentRepr::new_with_id(name, id);
|
||||
|
||||
// disposed automatically when the parent scope is disposed
|
||||
let (child, _) = cx
|
||||
.run_child_scope(|cx| cx.untrack(|| children_fn(cx).into_view(cx)));
|
||||
let (child, _) = cx.run_child_scope(|cx| {
|
||||
cx.untrack_with_diagnostics(|| children_fn(cx).into_view(cx))
|
||||
});
|
||||
|
||||
repr.children.push(child);
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ mod web {
|
||||
};
|
||||
pub use drain_filter_polyfill::VecExt as VecDrainFilterExt;
|
||||
pub use leptos_reactive::create_effect;
|
||||
pub use std::cell::OnceCell;
|
||||
pub use once_cell::unsync::OnceCell;
|
||||
pub use wasm_bindgen::JsCast;
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[allow(dead_code)] // not used in SSR
|
||||
type FxIndexSet<T> =
|
||||
indexmap::IndexSet<T, std::hash::BuildHasherDefault<rustc_hash::FxHasher>>;
|
||||
|
||||
@@ -230,7 +230,12 @@ impl EachItem {
|
||||
fragment.append_with_node_1(&closing.node).unwrap();
|
||||
}
|
||||
|
||||
mount_child(MountKind::Before(&closing.node), &child);
|
||||
// if child view is Text and if we are hydrating, we do not
|
||||
// need to mount it. otherwise, mount it here
|
||||
if !HydrationCtx::is_hydrating() || !matches!(child, View::Text(_))
|
||||
{
|
||||
mount_child(MountKind::Before(&closing.node), &child);
|
||||
}
|
||||
|
||||
Some(fragment)
|
||||
} else {
|
||||
@@ -494,8 +499,8 @@ where
|
||||
#[educe(Debug)]
|
||||
struct HashRun<T>(#[educe(Debug(ignore))] T);
|
||||
|
||||
/// Calculates the operations need to get from `a` to `b`.
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
/// Calculates the operations needed to get from `from` to `to`.
|
||||
#[allow(dead_code)] // not used in SSR but useful to have available for testing
|
||||
fn diff<K: Eq + Hash>(from: &FxIndexSet<K>, to: &FxIndexSet<K>) -> Diff {
|
||||
if from.is_empty() && to.is_empty() {
|
||||
return Diff::default();
|
||||
@@ -518,207 +523,90 @@ fn diff<K: Eq + Hash>(from: &FxIndexSet<K>, to: &FxIndexSet<K>) -> Diff {
|
||||
};
|
||||
}
|
||||
|
||||
// Get removed items
|
||||
let removed = from.difference(to);
|
||||
let mut removed = vec![];
|
||||
let mut moved = vec![];
|
||||
let mut added = vec![];
|
||||
let max_len = std::cmp::max(from.len(), to.len());
|
||||
|
||||
let remove_cmds = removed
|
||||
.clone()
|
||||
.map(|k| from.get_full(k).unwrap().0)
|
||||
.map(|idx| DiffOpRemove { at: idx });
|
||||
for index in 0..max_len {
|
||||
let from_item = from.get_index(index);
|
||||
let to_item = to.get_index(index);
|
||||
|
||||
// Get added items
|
||||
let added = to.difference(from);
|
||||
// if they're the same, do nothing
|
||||
if from_item != to_item {
|
||||
// if it's only in old, not new, remove it
|
||||
if from_item.is_some() && !to.contains(from_item.unwrap()) {
|
||||
let op = DiffOpRemove { at: index };
|
||||
removed.push(op);
|
||||
}
|
||||
// if it's only in new, not old, add it
|
||||
if to_item.is_some() && !from.contains(to_item.unwrap()) {
|
||||
let op = DiffOpAdd {
|
||||
at: index,
|
||||
mode: DiffOpAddMode::Normal,
|
||||
};
|
||||
added.push(op);
|
||||
}
|
||||
// if it's in both old and new, it can either
|
||||
// 1) be moved (and need to move in the DOM)
|
||||
// 2) be moved (but not need to move in the DOM)
|
||||
// * this would happen if, for example, 2 items
|
||||
// have been added before it, and it has moved by 2
|
||||
if let Some(from_item) = from_item {
|
||||
if let Some(to_item) = to.get_full(from_item) {
|
||||
let moves_forward_by = (to_item.0 as i32) - (index as i32);
|
||||
let move_in_dom = moves_forward_by
|
||||
!= (added.len() as i32) - (removed.len() as i32);
|
||||
|
||||
let add_cmds =
|
||||
added
|
||||
.clone()
|
||||
.map(|k| to.get_full(k).unwrap().0)
|
||||
.map(|idx| DiffOpAdd {
|
||||
at: idx,
|
||||
mode: Default::default(),
|
||||
});
|
||||
let op = DiffOpMove {
|
||||
from: index,
|
||||
len: 1,
|
||||
to: to_item.0,
|
||||
move_in_dom,
|
||||
};
|
||||
moved.push(op);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get items that might have moved
|
||||
let from_moved = from.intersection(&to).collect::<FxIndexSet<_>>();
|
||||
let to_moved = to.intersection(&from).collect::<FxIndexSet<_>>();
|
||||
moved = group_adjacent_moves(moved);
|
||||
|
||||
let move_cmds = find_ranges(from_moved, to_moved, from, to);
|
||||
|
||||
let mut diff = Diff {
|
||||
removed: remove_cmds.collect(),
|
||||
items_to_move: move_cmds.iter().map(|range| range.len).sum(),
|
||||
moved: move_cmds,
|
||||
added: add_cmds.collect(),
|
||||
Diff {
|
||||
removed,
|
||||
items_to_move: moved.iter().map(|m| m.len).sum(),
|
||||
moved,
|
||||
added,
|
||||
clear: false,
|
||||
};
|
||||
|
||||
apply_opts(from, to, &mut diff);
|
||||
|
||||
#[cfg(test)]
|
||||
{
|
||||
let mut adds_sorted = diff.added.clone();
|
||||
adds_sorted.sort_unstable_by_key(|add| add.at);
|
||||
|
||||
assert_eq!(diff.added, adds_sorted, "adds must be sorted");
|
||||
|
||||
let mut moves_sorted = diff.moved.clone();
|
||||
moves_sorted.sort_unstable_by_key(|move_| move_.to);
|
||||
|
||||
assert_eq!(diff.moved, moves_sorted, "moves must be sorted by `to`");
|
||||
}
|
||||
|
||||
diff
|
||||
}
|
||||
|
||||
/// Builds and returns the ranges of items that need to
|
||||
/// move sorted by `to`.
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
fn find_ranges<K: Eq + Hash>(
|
||||
from_moved: FxIndexSet<&K>,
|
||||
to_moved: FxIndexSet<&K>,
|
||||
from: &FxIndexSet<K>,
|
||||
to: &FxIndexSet<K>,
|
||||
) -> Vec<DiffOpMove> {
|
||||
let mut ranges = Vec::with_capacity(from.len());
|
||||
let mut prev_to_moved_index = 0;
|
||||
let mut range = DiffOpMove::default();
|
||||
|
||||
for (i, k) in from_moved.into_iter().enumerate() {
|
||||
let to_moved_index = to_moved.get_index_of(k).unwrap();
|
||||
|
||||
if i == 0 {
|
||||
range.from = from.get_index_of(k).unwrap();
|
||||
range.to = to.get_index_of(k).unwrap();
|
||||
}
|
||||
// The range continues
|
||||
else if to_moved_index == prev_to_moved_index + 1 {
|
||||
range.len += 1;
|
||||
}
|
||||
// We're done with this range, start a new one
|
||||
else {
|
||||
ranges.push(std::mem::take(&mut range));
|
||||
|
||||
range.from = from.get_index_of(k).unwrap();
|
||||
range.to = to.get_index_of(k).unwrap();
|
||||
}
|
||||
|
||||
prev_to_moved_index = to_moved_index;
|
||||
}
|
||||
|
||||
ranges.push(std::mem::take(&mut range));
|
||||
|
||||
// We need to remove ranges that didn't move relative to each other
|
||||
// as well as marking items that don't need to move in the DOM
|
||||
let mut to_ranges = ranges.clone();
|
||||
to_ranges.sort_unstable_by_key(|range| range.to);
|
||||
|
||||
let mut filtered_ranges = vec![];
|
||||
|
||||
let to_ranges_len = to_ranges.len();
|
||||
|
||||
for (i, range) in to_ranges.into_iter().enumerate() {
|
||||
if range != ranges[i] {
|
||||
filtered_ranges.push(range);
|
||||
}
|
||||
// The item did move, just not in the DOM
|
||||
else if range.from != range.to {
|
||||
filtered_ranges.push(DiffOpMove {
|
||||
move_in_dom: false,
|
||||
..range
|
||||
});
|
||||
} else if to_ranges_len > 2 {
|
||||
// TODO: Remove this else case...this is one of the biggest
|
||||
// optimizations we can do, but we're skipping this right now
|
||||
// until we figure out a way to handle moving around ranges
|
||||
// that did not move
|
||||
filtered_ranges.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
filtered_ranges
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
fn apply_opts<K: Eq + Hash>(
|
||||
from: &FxIndexSet<K>,
|
||||
to: &FxIndexSet<K>,
|
||||
cmds: &mut Diff,
|
||||
) {
|
||||
optimize_moves(&mut cmds.moved);
|
||||
|
||||
// We can optimize the case of replacing all items
|
||||
if !from.is_empty()
|
||||
&& !to.is_empty()
|
||||
&& cmds.removed.len() == from.len()
|
||||
&& cmds.moved.is_empty()
|
||||
{
|
||||
cmds.clear = true;
|
||||
cmds.removed.clear();
|
||||
|
||||
cmds.added
|
||||
.iter_mut()
|
||||
.for_each(|op| op.mode = DiffOpAddMode::Append);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We can optimize appends.
|
||||
if !cmds.added.is_empty()
|
||||
&& cmds.moved.is_empty()
|
||||
&& cmds.removed.is_empty()
|
||||
&& cmds.added[0].at >= from.len()
|
||||
{
|
||||
cmds.added
|
||||
.iter_mut()
|
||||
.for_each(|op| op.mode = DiffOpAddMode::Append);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
fn optimize_moves(moves: &mut Vec<DiffOpMove>) {
|
||||
if moves.is_empty() || moves.len() == 1 {
|
||||
// Do nothing
|
||||
}
|
||||
// This is the easiest optimal move case, which is to
|
||||
// simply swap the 2 ranges. We only need to move the range
|
||||
// that is smallest.
|
||||
else if moves.len() == 2 {
|
||||
if moves[1].len < moves[0].len {
|
||||
moves[0].move_in_dom = false;
|
||||
} else {
|
||||
moves[1].move_in_dom = false;
|
||||
/// Group adjacent items that are being moved as a group.
|
||||
/// For example from `[2, 3, 5, 6]` to `[1, 2, 3, 4, 5, 6]` should result
|
||||
/// in a move for `2,3` and `5,6` rather than 4 individual moves.
|
||||
fn group_adjacent_moves(moved: Vec<DiffOpMove>) -> Vec<DiffOpMove> {
|
||||
let mut prev: Option<DiffOpMove> = None;
|
||||
let mut new_moved = Vec::with_capacity(moved.len());
|
||||
for m in moved {
|
||||
match prev {
|
||||
Some(mut p) => {
|
||||
if (m.from == p.from + p.len) && (m.to == p.to + p.len) {
|
||||
p.len += 1;
|
||||
prev = Some(p);
|
||||
} else {
|
||||
new_moved.push(prev.take().unwrap());
|
||||
prev = Some(m);
|
||||
}
|
||||
}
|
||||
None => prev = Some(m),
|
||||
}
|
||||
}
|
||||
// Interestingly enoughs, there are NO configuration that are possible
|
||||
// for ranges of 3.
|
||||
//
|
||||
// For example, take A, B, C. Here are all possible configurations and
|
||||
// reasons for why they are impossible:
|
||||
// - A B C # identity, would be removed by ranges that didn't move
|
||||
// - A C B # `A` would be removed, thus it's a case of length 2
|
||||
// - B A C # `C` would be removed, thus it's a case of length 2
|
||||
// - B C A # `B C` are congiguous, so this is would have been a single range
|
||||
// - C A B # `A B` are congiguous, so this is would have been a single range
|
||||
// - C B A # `B` would be removed, thus it's a case of length 2
|
||||
//
|
||||
// We can add more pre-computed tables here if benchmarking or
|
||||
// user demand needs it...nevertheless, it is unlikely for us
|
||||
// to implement this algorithm to handle N ranges, because this
|
||||
// becomes exponentially more expensive to compute. It's faster,
|
||||
// for the most part, to assume the ranges are random and move
|
||||
// all the ranges around than to try and figure out the best way
|
||||
// to move them
|
||||
else {
|
||||
// The idea here is that for N ranges, we never need to
|
||||
// move the largest range, rather, have all ranges move
|
||||
// around it.
|
||||
let move_ = moves.iter_mut().max_by_key(|move_| move_.len).unwrap();
|
||||
|
||||
move_.move_in_dom = false;
|
||||
if let Some(prev) = prev {
|
||||
new_moved.push(prev)
|
||||
}
|
||||
new_moved
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
struct Diff {
|
||||
removed: Vec<DiffOpRemove>,
|
||||
@@ -728,7 +616,6 @@ struct Diff {
|
||||
clear: bool,
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct DiffOpMove {
|
||||
/// The index this range is starting relative to `from`.
|
||||
@@ -742,7 +629,6 @@ struct DiffOpMove {
|
||||
move_in_dom: bool,
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Default for DiffOpMove {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -754,20 +640,18 @@ impl Default for DiffOpMove {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
struct DiffOpAdd {
|
||||
at: usize,
|
||||
mode: DiffOpAddMode,
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct DiffOpRemove {
|
||||
at: usize,
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[allow(dead_code)] // Append not used in SSR but useful
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum DiffOpAddMode {
|
||||
Normal,
|
||||
@@ -776,7 +660,6 @@ enum DiffOpAddMode {
|
||||
_Prepend,
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Default for DiffOpAddMode {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
@@ -986,404 +869,173 @@ fn unpack_moves(diff: &Diff) -> (Vec<DiffOpMove>, Vec<DiffOpAdd>) {
|
||||
(moves, adds)
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod test_utils {
|
||||
// use super::*;
|
||||
#[cfg(test)]
|
||||
mod test_utils {
|
||||
use super::*;
|
||||
|
||||
// pub trait IntoFxIndexSet<K> {
|
||||
// fn into_fx_index_set(self) -> FxIndexSet<K>;
|
||||
// }
|
||||
pub trait IntoFxIndexSet<K> {
|
||||
fn into_fx_index_set(self) -> FxIndexSet<K>;
|
||||
}
|
||||
|
||||
// impl<T, K> IntoFxIndexSet<K> for T
|
||||
// where
|
||||
// T: IntoIterator<Item = K>,
|
||||
// K: Eq + Hash,
|
||||
// {
|
||||
// fn into_fx_index_set(self) -> FxIndexSet<K> {
|
||||
// self.into_iter().collect()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
impl<T, K> IntoFxIndexSet<K> for T
|
||||
where
|
||||
T: IntoIterator<Item = K>,
|
||||
K: Eq + Hash,
|
||||
{
|
||||
fn into_fx_index_set(self) -> FxIndexSet<K> {
|
||||
self.into_iter().collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// use test_utils::*;
|
||||
#[cfg(test)]
|
||||
use test_utils::*;
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod find_ranges {
|
||||
// use super::*;
|
||||
#[cfg(test)]
|
||||
mod diff {
|
||||
use super::*;
|
||||
|
||||
// // Single range tests will be empty because of removing ranges
|
||||
// // that didn't move
|
||||
// #[test]
|
||||
// fn single_range() {
|
||||
// let ranges = find_ranges(
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// &[1, 2, 3, 4].into_fx_index_set(),
|
||||
// &[1, 2, 3, 4].into_fx_index_set(),
|
||||
// );
|
||||
#[test]
|
||||
fn only_adds() {
|
||||
let diff =
|
||||
diff(&[].into_fx_index_set(), &[1, 2, 3].into_fx_index_set());
|
||||
|
||||
// assert_eq!(ranges, vec![]);
|
||||
// }
|
||||
assert_eq!(
|
||||
diff,
|
||||
Diff {
|
||||
added: vec![
|
||||
DiffOpAdd {
|
||||
at: 0,
|
||||
mode: DiffOpAddMode::Append
|
||||
},
|
||||
DiffOpAdd {
|
||||
at: 1,
|
||||
mode: DiffOpAddMode::Append
|
||||
},
|
||||
DiffOpAdd {
|
||||
at: 2,
|
||||
mode: DiffOpAddMode::Append
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn single_range_with_adds() {
|
||||
// let ranges = find_ranges(
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// &[1, 2, 3, 4].into_fx_index_set(),
|
||||
// &[1, 2, 5, 3, 4].into_fx_index_set(),
|
||||
// );
|
||||
#[test]
|
||||
fn only_removes() {
|
||||
let diff =
|
||||
diff(&[1, 2, 3].into_fx_index_set(), &[3].into_fx_index_set());
|
||||
|
||||
// assert_eq!(ranges, vec![]);
|
||||
// }
|
||||
assert_eq!(
|
||||
diff,
|
||||
Diff {
|
||||
removed: vec![DiffOpRemove { at: 0 }, DiffOpRemove { at: 1 }],
|
||||
moved: vec![DiffOpMove {
|
||||
from: 2,
|
||||
len: 1,
|
||||
to: 0,
|
||||
move_in_dom: false
|
||||
}],
|
||||
items_to_move: 1,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn single_range_with_removals() {
|
||||
// let ranges = find_ranges(
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// &[1, 2, 5, 3, 4].into_fx_index_set(),
|
||||
// &[1, 2, 3, 4].into_fx_index_set(),
|
||||
// );
|
||||
#[test]
|
||||
fn adds_with_no_move() {
|
||||
let diff =
|
||||
diff(&[3].into_fx_index_set(), &[1, 2, 3].into_fx_index_set());
|
||||
|
||||
// assert_eq!(ranges, vec![]);
|
||||
// }
|
||||
assert_eq!(
|
||||
diff,
|
||||
Diff {
|
||||
added: vec![
|
||||
DiffOpAdd {
|
||||
at: 0,
|
||||
..Default::default()
|
||||
},
|
||||
DiffOpAdd {
|
||||
at: 1,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
moved: vec![DiffOpMove {
|
||||
from: 0,
|
||||
len: 1,
|
||||
to: 2,
|
||||
move_in_dom: true
|
||||
}],
|
||||
items_to_move: 1,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn two_ranges() {
|
||||
// let ranges = find_ranges(
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// [3, 4, 1, 2].iter().into_fx_index_set(),
|
||||
// &[1, 2, 3, 4].into_fx_index_set(),
|
||||
// &[3, 4, 1, 2].into_fx_index_set(),
|
||||
// );
|
||||
#[test]
|
||||
fn move_as_group() {
|
||||
let diff = diff(
|
||||
&[2, 3, 4, 5].into_fx_index_set(),
|
||||
&[1, 2, 3, 4, 5].into_fx_index_set(),
|
||||
);
|
||||
|
||||
// assert_eq!(
|
||||
// ranges,
|
||||
// vec![
|
||||
// DiffOpMove {
|
||||
// from: 2,
|
||||
// to: 0,
|
||||
// len: 2,
|
||||
// move_in_dom: true,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 2,
|
||||
// len: 2,
|
||||
// move_in_dom: true,
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
assert_eq!(
|
||||
diff,
|
||||
Diff {
|
||||
added: vec![DiffOpAdd {
|
||||
at: 0,
|
||||
..Default::default()
|
||||
},],
|
||||
moved: vec![DiffOpMove {
|
||||
from: 0,
|
||||
len: 4,
|
||||
to: 1,
|
||||
move_in_dom: false
|
||||
},],
|
||||
items_to_move: 4,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn two_ranges_with_adds() {
|
||||
// let ranges = find_ranges(
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// [3, 4, 1, 2].iter().into_fx_index_set(),
|
||||
// &[1, 2, 3, 4].into_fx_index_set(),
|
||||
// &[3, 4, 5, 1, 6, 2].into_fx_index_set(),
|
||||
// );
|
||||
#[test]
|
||||
fn move_as_group_with_gap() {
|
||||
let diff = diff(
|
||||
&[2, 3, 5, 6].into_fx_index_set(),
|
||||
&[1, 2, 3, 4, 5, 6].into_fx_index_set(),
|
||||
);
|
||||
|
||||
// assert_eq!(
|
||||
// ranges,
|
||||
// vec![
|
||||
// DiffOpMove {
|
||||
// from: 2,
|
||||
// to: 0,
|
||||
// len: 2,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 3,
|
||||
// len: 2,
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
// #[test]
|
||||
// fn two_ranges_with_removals() {
|
||||
// let ranges = find_ranges(
|
||||
// [1, 2, 3, 4].iter().into_fx_index_set(),
|
||||
// [3, 4, 1, 2].iter().into_fx_index_set(),
|
||||
// &[1, 5, 2, 6, 3, 4].into_fx_index_set(),
|
||||
// &[3, 4, 1, 2].into_fx_index_set(),
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// ranges,
|
||||
// vec![
|
||||
// DiffOpMove {
|
||||
// from: 4,
|
||||
// to: 0,
|
||||
// len: 2,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 2,
|
||||
// len: 2,
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn remove_ranges_that_did_not_move() {
|
||||
// // Here, 'C' doesn't change
|
||||
// let ranges = find_ranges(
|
||||
// ['A', 'B', 'C', 'D'].iter().into_fx_index_set(),
|
||||
// ['B', 'D', 'C', 'A'].iter().into_fx_index_set(),
|
||||
// &['A', 'B', 'C', 'D'].into_fx_index_set(),
|
||||
// &['B', 'D', 'C', 'A'].into_fx_index_set(),
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// ranges,
|
||||
// vec![
|
||||
// DiffOpMove {
|
||||
// from: 1,
|
||||
// to: 0,
|
||||
// len: 1,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 3,
|
||||
// to: 1,
|
||||
// len: 1,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 3,
|
||||
// len: 1,
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
|
||||
// // Now we're going to to the same as above, just with more items
|
||||
// //
|
||||
// // A = 1
|
||||
// // B = 2, 3
|
||||
// // C = 4, 5, 6
|
||||
// // D = 7, 8, 9, 0
|
||||
|
||||
// let ranges = find_ranges(
|
||||
// //A B C D
|
||||
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 0].iter().into_fx_index_set(),
|
||||
// //B D C A
|
||||
// [2, 3, 7, 8, 9, 0, 4, 5, 6, 1].iter().into_fx_index_set(),
|
||||
// //A B C D
|
||||
// &[1, 2, 3, 4, 5, 6, 7, 8, 9, 0].into_fx_index_set(),
|
||||
// //B D C A
|
||||
// &[2, 3, 7, 8, 9, 0, 4, 5, 6, 1].into_fx_index_set(),
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// ranges,
|
||||
// vec![
|
||||
// DiffOpMove {
|
||||
// from: 1,
|
||||
// to: 0,
|
||||
// len: 2,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 6,
|
||||
// to: 2,
|
||||
// len: 4,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 9,
|
||||
// len: 1,
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod optimize_moves {
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn swap() {
|
||||
// let mut moves = vec![
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 6,
|
||||
// len: 2,
|
||||
// ..Default::default()
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 6,
|
||||
// to: 0,
|
||||
// len: 7,
|
||||
// ..Default::default()
|
||||
// },
|
||||
// ];
|
||||
|
||||
// optimize_moves(&mut moves);
|
||||
|
||||
// assert_eq!(
|
||||
// moves,
|
||||
// vec![DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 6,
|
||||
// len: 2,
|
||||
// ..Default::default()
|
||||
// }]
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod add_or_move {
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn simple_range() {
|
||||
// let cmds = AddOrMove::from_diff(&Diff {
|
||||
// moved: vec![DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 0,
|
||||
// len: 3,
|
||||
// }],
|
||||
// ..Default::default()
|
||||
// });
|
||||
|
||||
// assert_eq!(
|
||||
// cmds,
|
||||
// vec![
|
||||
// DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 0,
|
||||
// len: 1,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 1,
|
||||
// to: 1,
|
||||
// len: 1,
|
||||
// },
|
||||
// DiffOpMove {
|
||||
// from: 2,
|
||||
// to: 2,
|
||||
// len: 1,
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn range_with_add() {
|
||||
// let cmds = AddOrMove::from_diff(&Diff {
|
||||
// moved: vec![DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 0,
|
||||
// len: 3,
|
||||
// move_in_dom: true,
|
||||
// }],
|
||||
// added: vec![DiffOpAdd {
|
||||
// at: 2,
|
||||
// ..Default::default()
|
||||
// }],
|
||||
// ..Default::default()
|
||||
// });
|
||||
|
||||
// assert_eq!(
|
||||
// cmds,
|
||||
// vec![
|
||||
// AddOrMove::Move(DiffOpMove {
|
||||
// from: 0,
|
||||
// to: 0,
|
||||
// len: 1,
|
||||
// move_in_dom: true,
|
||||
// }),
|
||||
// AddOrMove::Move(DiffOpMove {
|
||||
// from: 1,
|
||||
// to: 1,
|
||||
// len: 1,
|
||||
// move_in_dom: true,
|
||||
// }),
|
||||
// AddOrMove::Add(DiffOpAdd {
|
||||
// at: 2,
|
||||
// ..Default::default()
|
||||
// }),
|
||||
// AddOrMove::Move(DiffOpMove {
|
||||
// from: 3,
|
||||
// to: 3,
|
||||
// len: 1,
|
||||
// move_in_dom: true,
|
||||
// }),
|
||||
// ]
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod diff {
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn only_adds() {
|
||||
// let diff =
|
||||
// diff(&[].into_fx_index_set(), &[1, 2, 3].into_fx_index_set());
|
||||
|
||||
// assert_eq!(
|
||||
// diff,
|
||||
// Diff {
|
||||
// added: vec![
|
||||
// DiffOpAdd {
|
||||
// at: 0,
|
||||
// mode: DiffOpAddMode::Append
|
||||
// },
|
||||
// DiffOpAdd {
|
||||
// at: 1,
|
||||
// mode: DiffOpAddMode::Append
|
||||
// },
|
||||
// DiffOpAdd {
|
||||
// at: 2,
|
||||
// mode: DiffOpAddMode::Append
|
||||
// },
|
||||
// ],
|
||||
// ..Default::default()
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn only_removes() {
|
||||
// let diff =
|
||||
// diff(&[1, 2, 3].into_fx_index_set(), &[3].into_fx_index_set());
|
||||
|
||||
// assert_eq!(
|
||||
// diff,
|
||||
// Diff {
|
||||
// removed: vec![DiffOpRemove { at: 0 }, DiffOpRemove { at: 1 }],
|
||||
// ..Default::default()
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn adds_with_no_move() {
|
||||
// let diff =
|
||||
// diff(&[3].into_fx_index_set(), &[1, 2, 3].into_fx_index_set());
|
||||
|
||||
// assert_eq!(
|
||||
// diff,
|
||||
// Diff {
|
||||
// added: vec![
|
||||
// DiffOpAdd {
|
||||
// at: 0,
|
||||
// ..Default::default()
|
||||
// },
|
||||
// DiffOpAdd {
|
||||
// at: 1,
|
||||
// ..Default::default()
|
||||
// },
|
||||
// ],
|
||||
// ..Default::default()
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
assert_eq!(
|
||||
diff,
|
||||
Diff {
|
||||
added: vec![
|
||||
DiffOpAdd {
|
||||
at: 0,
|
||||
..Default::default()
|
||||
},
|
||||
DiffOpAdd {
|
||||
at: 3,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
moved: vec![
|
||||
DiffOpMove {
|
||||
from: 0,
|
||||
len: 2,
|
||||
to: 1,
|
||||
move_in_dom: false
|
||||
},
|
||||
DiffOpMove {
|
||||
from: 2,
|
||||
len: 2,
|
||||
to: 4,
|
||||
move_in_dom: true
|
||||
}
|
||||
],
|
||||
items_to_move: 4,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,6 +483,9 @@ impl Text {
|
||||
|
||||
/// A leptos view which can be mounted to the DOM.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[must_use = "You are creating a View but not using it. An unused view can \
|
||||
cause your view to be rendered as () unexpectedly, and it can \
|
||||
also cause issues with client-side hydration."]
|
||||
pub enum View {
|
||||
/// HTML element node.
|
||||
Element(Element),
|
||||
|
||||
@@ -79,6 +79,7 @@ pub fn class_helper(
|
||||
name: Cow<'static, str>,
|
||||
value: Class,
|
||||
) {
|
||||
use crate::HydrationCtx;
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
let class_list = el.class_list();
|
||||
@@ -86,7 +87,9 @@ pub fn class_helper(
|
||||
Class::Fn(cx, f) => {
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) && (old.is_some() || new) {
|
||||
if old.as_ref() != Some(&new)
|
||||
&& (old.is_some() || new || HydrationCtx::is_hydrating())
|
||||
{
|
||||
class_expression(&class_list, &name, new, true)
|
||||
}
|
||||
new
|
||||
|
||||
@@ -479,11 +479,17 @@ impl RuntimeId {
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub(crate) fn untrack<T>(self, f: impl FnOnce() -> T) -> T {
|
||||
pub(crate) fn untrack<T>(
|
||||
self,
|
||||
f: impl FnOnce() -> T,
|
||||
diagnostics: bool,
|
||||
) -> T {
|
||||
with_runtime(self, |runtime| {
|
||||
let untracked_result;
|
||||
|
||||
SpecialNonReactiveZone::enter();
|
||||
if !diagnostics {
|
||||
SpecialNonReactiveZone::enter();
|
||||
}
|
||||
|
||||
let prev_observer =
|
||||
SetObserverOnDrop(self, runtime.observer.take());
|
||||
@@ -493,7 +499,9 @@ impl RuntimeId {
|
||||
runtime.observer.set(prev_observer.1);
|
||||
std::mem::forget(prev_observer); // avoid Drop
|
||||
|
||||
SpecialNonReactiveZone::exit();
|
||||
if !diagnostics {
|
||||
SpecialNonReactiveZone::exit();
|
||||
}
|
||||
|
||||
untracked_result
|
||||
})
|
||||
@@ -758,7 +766,7 @@ impl RuntimeId {
|
||||
cur_deps_value.replace(Some(deps_value.clone()));
|
||||
|
||||
let callback_value =
|
||||
Some(self.untrack(wrapped_callback.clone()));
|
||||
Some(self.untrack(wrapped_callback.clone(), false));
|
||||
|
||||
prev_callback_value.replace(callback_value);
|
||||
|
||||
|
||||
@@ -208,7 +208,14 @@ impl Scope {
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||
self.runtime.untrack(f)
|
||||
self.runtime.untrack(f, false)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Suspends reactive tracking but keeps the diagnostic warnings for
|
||||
/// untracked functions.
|
||||
pub fn untrack_with_diagnostics<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||
self.runtime.untrack(f, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ use crate::{
|
||||
/// // setting name only causes name to log, not count
|
||||
/// set_name.set("Bob".into());
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn create_slice<T, O, S>(
|
||||
cx: Scope,
|
||||
signal: RwSignal<T>,
|
||||
@@ -85,6 +86,7 @@ where
|
||||
|
||||
/// Takes a memoized, read-only slice of a signal. This is equivalent to the
|
||||
/// read-only half of [`create_slice`].
|
||||
#[track_caller]
|
||||
pub fn create_read_slice<T, O>(
|
||||
cx: Scope,
|
||||
signal: RwSignal<T>,
|
||||
@@ -98,6 +100,7 @@ where
|
||||
|
||||
/// Creates a setter to access one slice of a signal. This is equivalent to the
|
||||
/// write-only half of [`create_slice`].
|
||||
#[track_caller]
|
||||
pub fn create_write_slice<T, O>(
|
||||
cx: Scope,
|
||||
signal: RwSignal<T>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -17,6 +17,7 @@ gloo-net = { version = "0.2", features = ["http"] }
|
||||
lazy_static = "1"
|
||||
linear-map = { version = "1", features = ["serde_impl"] }
|
||||
log = "0.4"
|
||||
once_cell = "1.18"
|
||||
regex = { version = "1", optional = true }
|
||||
url = { version = "2", optional = true }
|
||||
percent-encoding = "2"
|
||||
|
||||
@@ -143,10 +143,7 @@ where
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
if url.origin
|
||||
!= window()
|
||||
.location()
|
||||
.origin()
|
||||
.unwrap_or_default()
|
||||
!= current_window_origin()
|
||||
{
|
||||
_ = window()
|
||||
.location()
|
||||
@@ -227,10 +224,7 @@ where
|
||||
match Url::try_from(resp_url.as_str()) {
|
||||
Ok(url) => {
|
||||
if url.origin
|
||||
!= window()
|
||||
.location()
|
||||
.hostname()
|
||||
.unwrap_or_default()
|
||||
!= current_window_origin()
|
||||
{
|
||||
_ = window()
|
||||
.location()
|
||||
@@ -328,6 +322,20 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
fn current_window_origin() -> String {
|
||||
let location = window().location();
|
||||
let protocol = location.protocol().unwrap_or_default();
|
||||
let hostname = location.hostname().unwrap_or_default();
|
||||
let port = location.port().unwrap_or_default();
|
||||
format!(
|
||||
"{}//{}{}{}",
|
||||
protocol,
|
||||
hostname,
|
||||
if port.is_empty() { "" } else { ":" },
|
||||
port
|
||||
)
|
||||
}
|
||||
|
||||
/// Automatically turns a server [Action](leptos_server::Action) into an HTML
|
||||
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
|
||||
/// progressively enhanced to use client-side routing.
|
||||
|
||||
@@ -33,7 +33,7 @@ where
|
||||
{
|
||||
// resolve relative path
|
||||
let path = use_resolved_path(cx, move || path.to_string());
|
||||
let path = path.get().unwrap_or_else(|| "/".to_string());
|
||||
let path = path.get_untracked().unwrap_or_else(|| "/".to_string());
|
||||
|
||||
// redirect on the server
|
||||
if let Some(redirect_fn) = use_context::<ServerRedirectFunction>(cx) {
|
||||
|
||||
@@ -3,14 +3,20 @@ use std::borrow::Cow;
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
use js_sys::RegExp;
|
||||
use once_cell::unsync::Lazy;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let OPTIONAL_RE = js_sys::RegExp::new(OPTIONAL, "");
|
||||
#[allow(non_snake_case)]
|
||||
let OPTIONAL_RE_2 = js_sys::RegExp::new(OPTIONAL_2, "");
|
||||
thread_local! {
|
||||
static OPTIONAL_RE: Lazy<RegExp> = Lazy::new(|| {
|
||||
RegExp::new(OPTIONAL, "")
|
||||
});
|
||||
static OPTIONAL_RE_2: Lazy<RegExp> = Lazy::new(|| {
|
||||
RegExp::new(OPTIONAL_2, "")
|
||||
});
|
||||
}
|
||||
|
||||
let captures = OPTIONAL_RE.exec(pattern);
|
||||
let captures = OPTIONAL_RE.with(|re| re.exec(pattern));
|
||||
match captures {
|
||||
None => vec![pattern.into()],
|
||||
Some(matched) => {
|
||||
@@ -28,7 +34,7 @@ pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
prefixes.push(prefix.clone());
|
||||
|
||||
while let Some(matched) =
|
||||
OPTIONAL_RE_2.exec(suffix.trim_start_matches('?'))
|
||||
OPTIONAL_RE_2.with(|re| re.exec(suffix.trim_start_matches('?')))
|
||||
{
|
||||
prefix += &matched.get(1).as_string().unwrap();
|
||||
prefixes.push(prefix.clone());
|
||||
|
||||
Reference in New Issue
Block a user