Compare commits

...

20 Commits

Author SHA1 Message Date
Greg Johnston
8943539043 docs: clarify WASM target 2023-07-09 17:05:37 -04:00
Joseph Cruz
f6a272498d test(counters_stable/wasm): enter count (#1307) 2023-07-08 12:07:11 -04:00
Ari Seyhun
aef7c4ce8e perf: use lazy thread local for regex in router match_optionals (#1309) 2023-07-08 08:47:52 -04:00
Greg Johnston
b29eb8e032 fix: <ActionForm/> should check origin correctly before doing a full-page refresh (#1304) 2023-07-08 08:00:48 -04:00
Greg Johnston
da9183f4b5 docs: fix braces in <Show/> example (#1303) 2023-07-08 06:42:27 -04:00
Greg Johnston
ae3ddcb0e6 docs: must use View (#1302) 2023-07-08 06:42:02 -04:00
Greg Johnston
c6b8f0e8ed v0.4.2 2023-07-07 15:34:56 -04:00
g-re-g
bab9f40a81 fix: rework diff functionality for <For/> (#1296) 2023-07-07 15:32:26 -04:00
webmstk
c2cfdf3678 docs: fixed typo in parent-child doc (#1300) 2023-07-07 13:59:08 -04:00
Joseph Cruz
8967eadc02 test(counters_stable): add wasm testing (#1278)
* test(counters_stable/wasm): view counters > counts

* test(counters_stable/wasm): view counters > title

* test(counters_stable/wasm): add counter

* test(counters_stable/wasm): add 1k counters

* clear(counters_stable/wasm): clear counters

* test(counters_stable/wasm): increment counters

* test(counters_stable/wasm): decrement counter

* test(counters_stable/wasm): remove counter
2023-07-07 13:07:27 -04:00
Greg Johnston
4cc65f837f chore: add mdbook in flake (#1299)
* nix-flake: use follows for `rust-overlay` (..)

This removes the extra nixpkgs dependency by re-using the nixpkgs
already defined.

https://nixos.wiki/wiki/Flakes
https://web.archive.org/web/20230621091703/https://nixos.wiki/wiki/Flakes

> # The `follows` keyword in inputs is used for inheritance.
> # Here, `inputs.nixpkgs` of sops-nix is kept consistent with the `inputs.nixpkgs` of
> # the current flake, to avoid problems caused by different versions of nixpkgs.

* nix-flake: use nix overlays for rustc/cargo (..)

This allows the same nightly rust version to be used across the
flake (vs. strictly in the `devShell`).

* nix-flake: add `mdbook` to the development shell (..)

bumped `nixpkgs` to the latest unstable to pull in mdbook version `0.4.30`.

* nix-flake: expose all rust tools in dev environment (..)

The `oxalica / rust-overlay` docs expose all tools in the development
environment, so we should do the same:

https://github.com/oxalica/rust-overlay#use-in-devshell-for-nix-develop

---------

Co-authored-by: Jay Querie <jay@querie.cc>
2023-07-07 12:59:18 -04:00
Dessalines
22706e7371 docs: adding instructions to add a tailwind plugin to examples. (#1293) 2023-07-07 12:35:24 -04:00
webmstk
9f9302662c docs/examples: make error handling example more obvious for Chrome users (#1292) 2023-07-07 12:34:57 -04:00
Greg Johnston
6b90e1babd examples: add 404 support in Actix examples (closes #1031) (#1291) 2023-07-06 10:35:37 -04:00
sjud
7e540a8f49 feat: support Axum extractors with state other than () (#1275)
This requires state to be provided via context using a special handler, but allows for extractors that use this state, rather than only `()`, as previously.
2023-07-05 20:40:29 -04:00
Greg Johnston
f06ffd72aa fix: use once_cell::OnceCell rather than std::OnceCell (#1288) 2023-07-05 17:16:51 -04:00
Greg Johnston
83d3d7579c fix: issue with class hydration not removing classes correctly (closes #1286) (#1287) 2023-07-05 12:00:27 -04:00
Greg Johnston
39edb6eb45 fix: untracked read in <Redirect/> (#1280) 2023-07-04 11:52:13 -04:00
Greg Johnston
d81c1a929e fix: duplicate text nodes during <For/> hydration (closes #1279) (#1281) 2023-07-04 11:50:59 -04:00
Greg Johnston
f69c28df18 fix: improved diagnostics about non-reactive signal access (#1277) 2023-07-03 19:37:15 -04:00
52 changed files with 953 additions and 818 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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" },
]

View File

@@ -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");
});
});

View File

@@ -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");
});
});

View File

@@ -12,6 +12,5 @@ test.describe("Decrement Count", () => {
await ui.decrementCount();
await expect(ui.total).toHaveText("-3");
await expect(ui.counters).toHaveText("1");
});
});

View File

@@ -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");
});
});

View File

@@ -12,6 +12,5 @@ test.describe("Increment Count", () => {
await ui.incrementCount();
await expect(ui.total).toHaveText("3");
await expect(ui.counters).toHaveText("1");
});
});

View File

@@ -12,7 +12,6 @@ test.describe("Remove Counter", () => {
await ui.removeCounter(1);
await expect(ui.total).toHaveText("0");
await expect(ui.counters).toHaveText("2");
});
});

View 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>
}
}

View File

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

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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();
}
}

View File

@@ -0,0 +1 @@
pub mod counters_page;

View 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);
}

View 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);

View 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);
}

View 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)");
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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" ];
}))

View File

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

View File

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

View File

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

View File

@@ -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()
}
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.4.0"
version = "0.4.2"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

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

View File

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

View File

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

View File

@@ -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());