Compare commits

..

15 Commits

Author SHA1 Message Date
Greg Johnston
1cf6cbe84a docs: clarify SSR/WASM binary size comments 2023-05-19 15:46:20 -04:00
Greg Johnston
87f6802967 docs: update notes on WASM binary size to work with SSR too (closes #1059) (#1068) 2023-05-19 15:08:32 -04:00
Greg Johnston
2cbf3581c5 fix: docs note on style refers to class (#1066) 2023-05-19 13:42:16 -04:00
agilarity
5a67e208fd test: verify tailwind example with playwright tests (#1062)
* chore: ignore playwright output

* fix: could not run playwright test

* test: should see the welcome message

* build: clean playwright output

* build: run playwright web tests

* build: setup e2e dependencies
2023-05-19 13:04:06 -04:00
Greg Johnston
3391a4a035 examples: fix todo_app_sqlite_axum (#1064) 2023-05-19 13:02:52 -04:00
Daniel Santana
076aa363a4 feat: added Debug, PartialEq and Eq derives to trigger. (#1060) 2023-05-18 20:32:25 -04:00
agilarity
2cb68c0bd4 fix: todomvc example style errors (#1058) 2023-05-18 15:49:34 -04:00
Greg Johnston
6eb24b5017 tests: fix broken SSR doctests (#1056) 2023-05-18 10:17:14 -04:00
yuuma03
b2faa6b86c feat: allow multipart forms on server fns (Actix) (#1048) 2023-05-17 19:53:55 -04:00
sjud
43990b5b67 docs: include link to book, Discord, examples (#1053) 2023-05-17 13:07:17 -04:00
kasbuunk
9453164dd2 docs: fix typo in view fn (#1050) 2023-05-16 14:34:37 -04:00
Greg Johnston
00fcd1c65e docs: fix small docs issues (closes #1045) (#1049) 2023-05-16 13:01:29 -04:00
Greg Johnston
85ad7b0f38 fix: <Suspense/> hydration when no resources are read under it (#1046) 2023-05-16 12:20:23 -04:00
Greg Johnston
f0a9940364 fix: leak in todomvc example (closes #706) 2023-05-15 14:53:39 -04:00
Mark Catley
b472aaf6a0 fix: typo in actix extract documentation (#1043) 2023-05-15 08:57:49 -04:00
26 changed files with 341 additions and 160 deletions

View File

@@ -41,6 +41,14 @@ build-std = ["std", "panic_abort", "core", "alloc"]
build-std-features = ["panic_immediate_abort"]
```
Note that if you're using this with SSR too, the same Cargo profile will be applied. You'll need to explicitly specify your target:
```toml
[build]
target = "x86_64-unknown-linux-gnu" # or whatever
```
And you'll need to add `panic = "abort"` to `[profile.release]` in `Cargo.toml`. Note that this applies the same `build-std` and panic settings to your server binary, which may not be desirable. Some further exploration is probably needed here.
5. One of the sources of binary size in WASM binaries can be `serde` serialization/deserialization code. Leptos uses `serde` by default to serialize and deserialize resources created with `create_resource`. You might try experimenting with the `miniserde` and `serde-lite` features, which allow you to use those crates for serialization and deserialization instead; each only implements a subset of `serde`s functionality, but typically optimizes for size over speed.
## Things to Avoid

View File

@@ -15,7 +15,7 @@ You _could_ do this by just creating two `<progress>` elements:
let (count, set_count) = create_signal(cx, 0);
let double_count = move || count() * 2;
view! {
view! { cx,
<progress
max="50"
value=count

View File

@@ -0,0 +1,5 @@
[tasks.web-test]
dependencies = ["cargo-leptos-e2e"]
[tasks.clean-all]
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]

View File

@@ -1,3 +1,6 @@
[env]
END2END_DIR = "end2end"
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
@@ -5,6 +8,9 @@ env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.check-format]
args = ["fmt", "--", "--check", "--config-path", "../../"]
[tasks.verify-local]
description = "Run all quality checks and tests from an example directory"
dependencies = ["check-style", "test-local"]
@@ -13,11 +19,62 @@ dependencies = ["check-style", "test-local"]
description = "Run all tests from an example directory"
dependencies = ["test", "web-test"]
[tasks.clean-cargo]
description = "Runs the cargo clean command."
category = "Cleanup"
command = "cargo"
args = ["clean"]
[tasks.clean-trunk]
description = "Runs the trunk clean command."
category = "Cleanup"
command = "trunk"
args = ["clean"]
[tasks.clean-node_modules]
description = "Delete all node_modules directories"
category = "Cleanup"
script = '''
find . -type d -name node_modules | xargs rm -rf
'''
[tasks.clean-playwright]
description = "Delete playwright directories"
category = "Cleanup"
cwd = "${END2END_DIR}"
command = "rm"
args = ["-rf", "playwright", "playwright/.cache", "test-results"]
[tasks.clean-all]
dependencies = ["clean", "clean-trunk"]
description = "Delete all temporary directories"
category = "Cleanup"
dependencies = ["clean-cargo"]
[tasks.wasm-web-test]
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
command = "cargo"
args = ["make", "wasm-pack-test"]
[tasks.cargo-leptos-e2e]
description = "Runs end to end tests with cargo leptos"
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.setup]
description = "Setup e2e dependencies"
cwd = "${END2END_DIR}"
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
RED="\e[0;31m"
RESET="\e[0m"
if command -v pnpm; then
pnpm install
elif command -v npm; then
npm install
else
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
exit 1
fi
'''

View File

@@ -1,4 +1,5 @@
[tasks.web-test]
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
command = "cargo"
args = ["make", "wasm-pack-test"]
dependencies = ["wasm-web-test"]
[tasks.clean-all]
dependencies = ["clean-cargo", "clean-trunk"]

View File

@@ -8,3 +8,10 @@ Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# Support playwright testing
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/
pnpm-lock.yaml

View File

@@ -96,6 +96,7 @@ site-addr = "127.0.0.1:3000"
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head

View File

@@ -1,4 +1,7 @@
extend = [{ path = "../cargo-make/common.toml" }]
extend = [
{ path = "../cargo-make/common.toml" },
{ path = "../cargo-make/cargo-leptos-web-test.toml" },
]
[tasks.build]
command = "cargo"

View File

@@ -104,3 +104,8 @@ You'll need to install trunk to client side render this bundle.
## Attribution
Many thanks to GreatGreg for putting together this guide. You can find the original, with added details, [here](https://github.com/leptos-rs/leptos/discussions/125).
## Playwright Testing
- Run `cargo make setup` to install dependencies
- Run `cargo leptos test` or `cargo leptos end-to-end` to run the tests

View File

@@ -1,9 +1,7 @@
import { test, expect } from "@playwright/test";
test("homepage has title and links to intro page", async ({ page }) => {
test("should see the welcome message", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page).toHaveTitle("Cargo Leptos");
await expect(page.locator("h1")).toHaveText("Hi from your Leptos WASM!");
await expect(page.locator("h2")).toHaveText("Welcome to Leptos with Tailwind");
});

View File

@@ -146,11 +146,8 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
</header>
<main>
<Routes>
<Route path="" view=|cx| view! {
cx,
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<Todos/>
</ErrorBoundary>
<Route path="" view=|cx| view! { cx,
<Todos/>
}/> //Route
<Route path="weird" methods=&[Method::Get, Method::Post]
ssr=SsrMode::Async
@@ -203,63 +200,65 @@ pub fn Todos(cx: Scope) -> impl IntoView {
<input type="submit" value="Add"/>
</MultiActionForm>
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
{move || {
let existing_todos = {
move || {
todos.read(cx)
.map(move |todos| match todos {
Err(e) => {
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
}
Ok(todos) => {
if todos.is_empty() {
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
} else {
todos
.into_iter()
.map(move |todo| {
view! {
cx,
<li>
{todo.title}
<ActionForm action=delete_todo>
<input type="hidden" name="id" value={todo.id}/>
<input type="submit" value="X"/>
</ActionForm>
</li>
}
})
.collect_view(cx)
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
{move || {
let existing_todos = {
move || {
todos.read(cx)
.map(move |todos| match todos {
Err(e) => {
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
}
}
})
.unwrap_or_default()
}
};
let pending_todos = move || {
submissions
.get()
.into_iter()
.filter(|submission| submission.pending().get())
.map(|submission| {
view! {
cx,
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
Ok(todos) => {
if todos.is_empty() {
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
} else {
todos
.into_iter()
.map(move |todo| {
view! {
cx,
<li>
{todo.title}
<ActionForm action=delete_todo>
<input type="hidden" name="id" value={todo.id}/>
<input type="submit" value="X"/>
</ActionForm>
</li>
}
})
.collect_view(cx)
}
}
})
.unwrap_or_default()
}
})
.collect_view(cx)
};
};
view! {
cx,
<ul>
{existing_todos}
{pending_todos}
</ul>
let pending_todos = move || {
submissions
.get()
.into_iter()
.filter(|submission| submission.pending().get())
.map(|submission| {
view! {
cx,
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect_view(cx)
};
view! {
cx,
<ul>
{existing_todos}
{pending_todos}
</ul>
}
}
}
}
</ErrorBoundary>
</Transition>
</div>
}

View File

@@ -43,7 +43,7 @@ impl Todos {
}
pub fn remove(&mut self, id: Uuid) {
self.0.retain(|todo| todo.id != id);
self.retain(|todo| todo.id != id);
}
pub fn remaining(&self) -> usize {
@@ -76,7 +76,23 @@ impl Todos {
}
fn clear_completed(&mut self) {
self.0.retain(|todo| !todo.completed.get());
self.retain(|todo| !todo.completed.get());
}
fn retain(&mut self, mut f: impl FnMut(&Todo) -> bool) {
self.0.retain(|todo| {
let retain = f(todo);
// because these signals are created at the top level,
// they are owned by the <TodoMVC/> component and not
// by the individual <Todo/> components. This means
// that if they are not manually disposed when removed, they
// will be held onto until the <TodoMVC/> is unmounted.
if !retain {
todo.title.dispose();
todo.completed.dispose();
}
retain
})
}
}
@@ -136,7 +152,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
// Handle the three filter modes: All, Active, and Completed
let (mode, set_mode) = create_signal(cx, Mode::All);
window_event_listener_untyped("hashchange", move |_| {
window_event_listener(ev::hashchange, move |_| {
let new_mode =
location_hash().map(|hash| route(&hash)).unwrap_or_default();
set_mode(new_mode);

View File

@@ -8,6 +8,7 @@ repository = "https://github.com/leptos-rs/leptos"
description = "Actix integrations for the Leptos web framework."
[dependencies]
actix-http = "3"
actix-web = "4"
futures = "0.3"
leptos = { workspace = true, features = ["ssr"] }

View File

@@ -185,7 +185,7 @@ pub fn handle_server_fns_with_context(
.and_then(|value| value.to_str().ok());
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
let body: &[u8] = &body;
let body_ref: &[u8] = &body;
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
@@ -198,10 +198,28 @@ pub fn handle_server_fns_with_context(
provide_context(cx, req.clone());
provide_context(cx, res_options.clone());
// we consume the body here (using the web::Bytes extractor), but it is required for things
// like MultipartForm
if req
.headers()
.get("Content-Type")
.and_then(|value| value.to_str().ok())
.and_then(|value| {
Some(
value.starts_with(
"multipart/form-data; boundary=",
),
)
})
== Some(true)
{
provide_context(cx, body.clone());
}
let query = req.query_string().as_bytes();
let data = match &server_fn.encoding {
Encoding::Url | Encoding::Cbor => body,
Encoding::Url | Encoding::Cbor => body_ref,
Encoding::GetJSON | Encoding::GetCBOR => query,
};
let res = match (server_fn.trait_obj)(cx, data).await {
@@ -1028,7 +1046,7 @@ where
}
}
/// A helper to make it easier to use Axum extractors in server functions. This takes
/// A helper to make it easier to use Actix extractors in server functions. This takes
/// a handler function as its argument. The handler follows similar rules to an Actix
/// [Handler](actix_web::Handler): it is an async function that receives arguments that
/// will be extracted from the request and returns some value.
@@ -1072,9 +1090,17 @@ where
{
let req = use_context::<actix_web::HttpRequest>(cx)
.expect("HttpRequest should have been provided via context");
let input = E::extract(&req)
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
let input = if let Some(body) = use_context::<Bytes>(cx) {
let (_, mut payload) = actix_http::h1::Payload::create(false);
payload.unread_data(body);
E::from_request(&req, &mut dev::Payload::from(payload))
} else {
E::extract(&req)
}
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
Ok(f.call(input).await)
}

View File

@@ -12,6 +12,10 @@
//!
//! And you can do all three of these **using the same Leptos code.**
//!
//! Take a look at the [Leptos Book](https://leptos-rs.github.io/leptos/) for a walkthrough of the framework.
//! Join us on our [Discord Channel](https://discord.gg/v38Eef6sWG) to see what the community is building.
//! Explore our [Examples](https://github.com/leptos-rs/leptos/tree/main/examples) to see Leptos in action.
//!
//! # `nightly` Note
//! Most of the examples assume youre using `nightly` Rust. If youre on stable, note the following:
//! 1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.0", features = ["stable"] }`

View File

@@ -101,13 +101,17 @@ where
use leptos_reactive::signal_prelude::*;
// run the child; we'll probably throw this away, but it will register resource reads
let child = orig_child(cx).into_view(cx);
let _child = orig_child(cx).into_view(cx);
let after_original_child = HydrationCtx::id();
let initial = {
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
child
let orig_child = Rc::clone(&orig_child);
HydrationCtx::continue_from(current_id.clone());
Fragment::lazy(Box::new(move || {
vec![DynChild::new(move || orig_child(cx)).into_view(cx)]
})).into_view(cx)
}
// show the fallback, but also prepare to stream HTML
else {

View File

@@ -14,16 +14,13 @@ fn simple_ssr_test() {
</div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<!--leptos-view|leptos-tests-ssr.rs-8|open--><div \
id=\"_0-1\"><button id=\"_0-2\">-1</button><span \
assert!(rendered.into_view(cx).render_to_string(cx).contains(
"<div id=\"_0-1\"><button id=\"_0-2\">-1</button><span \
id=\"_0-3\">Value: \
<!--hk=_0-4o|leptos-dyn-child-start-->0<!\
--hk=_0-4c|leptos-dyn-child-end-->!</span><button \
id=\"_0-5\">+1</button></div><!--leptos-view|leptos-tests-ssr.\
rs-8|close-->"
);
id=\"_0-5\">+1</button></div>"
));
});
}
@@ -54,28 +51,13 @@ fn ssr_test_with_components() {
</div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<!--leptos-view|leptos-tests-ssr.rs-49|open--><div id=\"_0-1\" \
class=\"counters\"><!--hk=_0-1-0o|leptos-counter-start--><!\
--leptos-view|leptos-tests-ssr.rs-38|open--><div \
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
assert!(rendered.into_view(cx).render_to_string(cx).contains(
"<div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
id=\"_0-1-3\">Value: \
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
id=\"_0-1-5\">+1</button></div><!--leptos-view|leptos-tests-ssr.\
rs-38|close--><!--hk=_0-1-0c|leptos-counter-end--><!\
--hk=_0-1-5-0o|leptos-counter-start--><!\
--leptos-view|leptos-tests-ssr.rs-38|open--><div \
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
id=\"_0-1-5-3\">Value: \
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
id=\"_0-1-5-5\">+1</button></div><!\
--leptos-view|leptos-tests-ssr.rs-38|close--><!\
--hk=_0-1-5-0c|leptos-counter-end--></div><!\
--leptos-view|leptos-tests-ssr.rs-49|close-->"
);
id=\"_0-1-5\">+1</button></div>"
));
});
}
@@ -106,29 +88,13 @@ fn ssr_test_with_snake_case_components() {
</div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<!--leptos-view|leptos-tests-ssr.rs-101|open--><div id=\"_0-1\" \
class=\"counters\"><!\
--hk=_0-1-0o|leptos-snake-case-counter-start--><!\
--leptos-view|leptos-tests-ssr.rs-90|open--><div \
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
assert!(rendered.into_view(cx).render_to_string(cx).contains(
"<div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
id=\"_0-1-3\">Value: \
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
id=\"_0-1-5\">+1</button></div><!--leptos-view|leptos-tests-ssr.\
rs-90|close--><!--hk=_0-1-0c|leptos-snake-case-counter-end--><!\
--hk=_0-1-5-0o|leptos-snake-case-counter-start--><!\
--leptos-view|leptos-tests-ssr.rs-90|open--><div \
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
id=\"_0-1-5-3\">Value: \
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
id=\"_0-1-5-5\">+1</button></div><!\
--leptos-view|leptos-tests-ssr.rs-90|close--><!\
--hk=_0-1-5-0c|leptos-snake-case-counter-end--></div><!\
--leptos-view|leptos-tests-ssr.rs-101|close-->"
);
id=\"_0-1-5\">+1</button></div>"
));
});
}
@@ -144,12 +110,10 @@ fn test_classes() {
<div class="my big" class:a={move || value.get() > 10} class:red=true class:car={move || value.get() > 1}></div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<!--leptos-view|leptos-tests-ssr.rs-142|open--><div id=\"_0-1\" \
class=\"my big red \
car\"></div><!--leptos-view|leptos-tests-ssr.rs-142|close-->"
);
assert!(rendered
.into_view(cx)
.render_to_string(cx)
.contains("<div id=\"_0-1\" class=\"my big red car\"></div>"));
});
}
@@ -168,13 +132,10 @@ fn ssr_with_styles() {
</div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<!--leptos-view|leptos-tests-ssr.rs-164|open--><div id=\"_0-1\" \
class=\" myclass\"><button id=\"_0-2\" class=\"btn \
myclass\">-1</button></div><!--leptos-view|leptos-tests-ssr.\
rs-164|close-->"
);
assert!(rendered.into_view(cx).render_to_string(cx).contains(
"<div id=\"_0-1\" class=\" myclass\"><button id=\"_0-2\" \
class=\"btn myclass\">-1</button></div>"
));
});
}
@@ -190,11 +151,9 @@ fn ssr_option() {
<option/>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<!--leptos-view|leptos-tests-ssr.rs-188|open--><option \
id=\"_0-1\"></option><!--leptos-view|leptos-tests-ssr.\
rs-188|close-->"
);
assert!(rendered
.into_view(cx)
.render_to_string(cx)
.contains("<option id=\"_0-1\"></option>"));
});
}

View File

@@ -17,7 +17,7 @@ leptos_actix = { path = "../../../../integrations/actix", optional = true }
leptos_router = { path = "../../../../router", default-features = false }
log = "0.4"
simple_logger = "4"
wasm-bindgen = "0.2"
wasm-bindgen = "0.2.85"
serde = "1.0.159"
tokio = { version = "1.27.0", features = ["time"], optional = true }

View File

@@ -56,6 +56,7 @@ pub fn App(cx: Scope) -> impl IntoView {
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
<Route path="none" view=|cx| view! { cx, <None/> }/>
</Route>
// in-order
<Route
@@ -71,6 +72,7 @@ pub fn App(cx: Scope) -> impl IntoView {
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
<Route path="none" view=|cx| view! { cx, <None/> }/>
</Route>
// async
<Route
@@ -86,6 +88,7 @@ pub fn App(cx: Scope) -> impl IntoView {
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
<Route path="none" view=|cx| view! { cx, <None/> }/>
</Route>
</Routes>
</main>
@@ -101,6 +104,7 @@ fn SecondaryNav(cx: Scope) -> impl IntoView {
<A href="single">"Single"</A>
<A href="parallel">"Parallel"</A>
<A href="inside-component">"Inside Component"</A>
<A href="none">"No Resources"</A>
</nav>
}
}
@@ -217,3 +221,25 @@ fn InsideComponentChild(cx: Scope) -> impl IntoView {
</Suspense>
}
}
#[component]
fn None(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<div>
<Suspense fallback=|| "Loading 1...">
<div>"Children inside Suspense should hydrate properly."</div>
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
</Suspense>
<p>"Children following " <code>"<Suspense/>"</code> " should hydrate properly."</p>
<div>
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
</div>
</div>
}
}

View File

@@ -29,7 +29,7 @@ pub trait EventDescriptor: Clone {
}
}
/// Overrides the [`EventDescriptor::bubbles`] method to always return
/// Overrides the [`EventDescriptor::BUBBLES`] value to always return
/// `false`, which forces the event to not be globally delegated.
#[derive(Clone)]
#[allow(non_camel_case_types)]

View File

@@ -807,7 +807,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
/// Sets a style on an element.
///
/// **Note**: In the builder syntax, this will be overwritten by the `style`
/// attribute if you use `.attr("class", /* */)`. In the `view` macro, they
/// attribute if you use `.attr("style", /* */)`. In the `view` macro, they
/// are automatically re-ordered so that this over-writing does not happen.
#[track_caller]
pub fn style(

View File

@@ -240,13 +240,20 @@ impl View {
dont_escape_text: bool,
) {
match self {
View::Suspense(id, _) => {
View::Suspense(id, view) => {
let id = id.to_string();
if let Some(data) = cx.take_pending_fragment(&id) {
chunks.push_back(StreamChunk::Async {
chunks: data.in_order,
should_block: data.should_block,
});
} else {
// if not registered, means it was already resolved
View::CoreComponent(view).into_stream_chunks_helper(
cx,
chunks,
dont_escape_text,
);
}
}
View::Text(node) => {

View File

@@ -672,7 +672,7 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// Annotates a struct so that it can be used with your Component as a `slot`.
///
/// The `#[slot]` macro allows you to annotate plain Rust struct as component slots and use them
/// within your Leptos [component](crate::component!) properties. The struct can contain any number
/// within your Leptos [`component`](macro@crate::component) properties. The struct can contain any number
/// of fields. When you use the component somewhere else, the names of the slot fields are the
/// names of the properties you use in the [view](crate::view!) macro.
///

View File

@@ -39,8 +39,7 @@ impl<T> Clone for StoredValue<T> {
impl<T> Copy for StoredValue<T> {}
impl<T> StoredValue<T> {
/// Returns a clone of the signals current value, subscribing the effect
/// to this signal.
/// Returns a clone of the current stored value.
///
/// # Panics
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
@@ -70,7 +69,7 @@ impl<T> StoredValue<T> {
self.try_get_value().expect("could not get stored value")
}
/// Same as [`StoredValue::get`] but will not panic by default.
/// Same as [`StoredValue::get_value`] but will not panic by default.
#[track_caller]
pub fn try_get_value(&self) -> Option<T>
where
@@ -79,7 +78,7 @@ impl<T> StoredValue<T> {
self.try_with_value(T::clone)
}
/// Applies a function to the current stored value.
/// Applies a function to the current stored value and returns the result.
///
/// # Panics
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
@@ -105,8 +104,8 @@ impl<T> StoredValue<T> {
self.try_with_value(f).expect("could not get stored value")
}
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
/// the signal is still valid. [`None`] otherwise.
/// Same as [`StoredValue::with_value`] but returns [`Some(O)]` only if
/// the stored value has not yet been disposed. [`None`] otherwise.
pub fn try_with_value<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
with_runtime(self.runtime, |runtime| {
let value = {
@@ -161,8 +160,8 @@ impl<T> StoredValue<T> {
.expect("could not set stored value");
}
/// Same as [`Self::update`], but returns [`Some(O)`] if the
/// signal is still valid, [`None`] otherwise.
/// Same as [`Self::update_value`], but returns [`Some(O)`] if the
/// stored value has not yet been disposed, [`None`] otherwise.
pub fn try_update_value<O>(self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
with_runtime(self.runtime, |runtime| {
let values = runtime.stored_values.borrow();
@@ -195,8 +194,8 @@ impl<T> StoredValue<T> {
self.try_set_value(value);
}
/// Same as [`Self::set`], but returns [`None`] if the signal is
/// still valid, [`Some(T)`] otherwise.
/// Same as [`Self::set_value`], but returns [`None`] if the
/// stored value has not yet been disposed, [`Some(T)`] otherwise.
pub fn try_set_value(&self, value: T) -> Option<T> {
with_runtime(self.runtime, |runtime| {
let values = runtime.stored_values.borrow();

View File

@@ -11,7 +11,7 @@ use crate::{
/// Reactive Trigger, notifies reactive code to rerun.
///
/// See [`create_trigger`] for more.
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Trigger {
pub(crate) runtime: RuntimeId,
pub(crate) id: NodeId,

View File

@@ -95,8 +95,63 @@ where
let action = use_resolved_path(cx, move || action.clone())
.get()
.unwrap_or_default();
// multipart POST (setting Context-Type breaks the request)
if method == "post" && enctype == "multipart/form-data" {
ev.prevent_default();
ev.stop_propagation();
let on_response = on_response.clone();
spawn_local(async move {
let res = gloo_net::http::Request::post(&action)
.header("Accept", "application/json")
.redirect(RequestRedirect::Follow)
.body(form_data)
.send()
.await;
match res {
Err(e) => {
error!("<Form/> error while POSTing: {e:#?}");
if let Some(error) = error {
error.set(Some(Box::new(e)));
}
}
Ok(resp) => {
if let Some(version) = action_version {
version.update(|n| *n += 1);
}
if let Some(error) = error {
error.set(None);
}
if let Some(on_response) = on_response.clone() {
on_response(resp.as_raw());
}
// Check all the logical 3xx responses that might
// get returned from a server function
if resp.redirected() {
let resp_url = &resp.url();
match Url::try_from(resp_url.as_str()) {
Ok(url) => {
request_animation_frame(move || {
if let Err(e) = navigate(
&format!(
"{}{}",
url.pathname, url.search,
),
Default::default(),
) {
warn!("{}", e);
}
});
}
Err(e) => warn!("{}", e),
}
}
}
}
});
}
// POST
if method == "post" {
else if method == "post" {
ev.prevent_default();
ev.stop_propagation();