Compare commits

..

55 Commits

Author SHA1 Message Date
Greg Johnston
d6ee2a37f4 v0.5.3 2023-11-27 19:38:33 -05:00
Greg Johnston
18a92bbfd8 fix: improved rust-analyzer support in #[component] macro (#2075) 2023-11-27 19:37:43 -05:00
Greg Johnston
4e8c3accf2 fix: make prop serialization opt-in for devtools (closes #1952) (#2081) 2023-11-27 16:35:31 -05:00
Joseph Cruz
a8e25af523 ci(leptos): run ci on change instead of check (#2061)
* ci: run ci examples on leptos change

* chore(ci): simulate leptos source change

* ci(todo_app_sqlite_csr): increase retries

* ci: delete check examples workflow

* ci: rename ci examples workflow

* ci: run ci examples with stable toolchain

* chore(ci): remove simulated change

* ci: delete check stable workflow
2023-11-24 14:59:13 -05:00
Greg Johnston
d531848db5 fix: dispose previous route or outlet before rendering new one (closes #2070) (#2071) 2023-11-24 14:51:51 -05:00
hpepper
670f415565 docs: add instruction to install trunk to examples/README.md (#2064)
Co-authored-by: Henry Pepper <henry>
2023-11-24 14:06:02 -05:00
Greg Johnston
061213ca78 fix: correctly mark Trigger as clean when it is re-tracked (closes #1948, #2048) (#2059) 2023-11-22 09:29:25 -05:00
Greg Johnston
0ce4ee8a7a docs: add warning for nested Fn in attribute (see #2023) (#2045) 2023-11-22 07:35:20 -05:00
Greg Johnston
1cd6603da0 ci(examples): fix portal test (#2051) 2023-11-20 20:39:19 -05:00
Andrew Wheeler(Genusis)
453911e6fc examples: updated axum session to latest 0.9 in examples (#2049)
* updated axum_database_sessions to axum_session along with axum_sessions_auth to axum_session_auth

* updated to axum session 0.9
2023-11-20 20:33:31 -05:00
Greg Johnston
cb6267ad08 feat: <Provider/> component to fix context shadowing (closes #2038) (#2040) 2023-11-19 20:24:36 -05:00
Ken
4518d3c89f Have fetch example conform to docs guidance around using <ErrorBoundary> and <Transition> in conjunction (#2035)
* put `<ErrorBoundary>` inside `<Transition>`

* fix indentation
2023-11-18 08:24:30 -05:00
Greg Johnston
e47a619556 examples: add CSR with server functions example (closes #1975) (#2031) 2023-11-18 08:24:15 -05:00
Daniel Mantei
414f5fc393 docs: reorganize deployment section (#2036)
* Update mdbook-admonish book dependency

* Move "Optimizing Binary Size" to Deploy.. chapter

* Minor text updates to the Deployment section
2023-11-17 15:40:20 -05:00
martin frances
362e3bc603 chore: stop using std::fmt, instead used core::fmt. (#2033) 2023-11-17 15:36:13 -05:00
taohua
4d549f70c9 docs: fix misnamed form field in <Form/> example (#2024)
Co-authored-by: datewu <>
2023-11-17 15:27:06 -05:00
Chris
85dd726d43 docs: ActionForm examples for indexing into struct fields (#2017)
Co-authored-by: chrisp60 <gh@cperry.me>
2023-11-17 15:22:11 -05:00
blorbb
24febe11f3 feat: impl Default for TextProp (#2016) 2023-11-17 15:20:03 -05:00
Greg Johnston
64b1e9bed3 fix: use create_effect for <Portal/> to avoid hydration issues (closes #2010) (#2029) 2023-11-17 15:19:07 -05:00
Greg Johnston
68c91a732d fix: allow nested functions in Attribute (closes #2023) (#2027) 2023-11-15 11:00:15 -05:00
blorbb
8573f22d96 fix: re-export slice! macro (#2008) 2023-11-11 06:47:15 -05:00
Greg Johnston
61c7ff4256 docs: add note about context shadowing (closes #1986) (#2015) 2023-11-10 18:04:22 -05:00
Greg Johnston
860d887931 chore: remove duplicate benchmarks in leptos_reactive (#2014) 2023-11-10 15:53:32 -05:00
Chris
5e929a75fa feat: Action::new and Action::server (#1998) 2023-11-10 15:53:20 -05:00
Greg Johnston
d82cf0b76a docs: remove outdated APP_ENVIRONMENT variable (#2013) 2023-11-10 14:27:45 -05:00
Greg Johnston
cb7e07496a docs: fix CodeSandbox for resources (#2002) 2023-11-07 20:11:30 -05:00
Greg Johnston
17881c5c6e docs: fix chapter 10 CodeSandbox 2023-11-07 20:07:38 -05:00
Greg Johnston
2e816b26aa benchmarks: get benchmarks directory working with updated tachys 2023-11-07 20:03:35 -05:00
Gabriel Hansson
68d67c9e92 book: Fix <Body/> link in metadata.md (#1999) 2023-11-07 16:16:47 -05:00
Greg Johnston
0dea6fdcea fix: correctly reset island/not-island state of SSRed Suspense streaming into island (closes #1996) (#2000) 2023-11-07 16:16:27 -05:00
Greg Johnston
530dcff86a examples: remove incorrect CSR information for hackernews_js_fetch example (#1997) 2023-11-06 14:45:26 -05:00
Greg Johnston
9d9a4932b3 fix: run <ErrorBoundary/> in a child so siblings don't collide (closes #1987) (#1991) 2023-11-05 21:29:35 -05:00
Greg Johnston
bfb67d45e8 examples: fix style.css path (closes #1992) (#1994) 2023-11-05 21:29:17 -05:00
Greg Johnston
b1e8105442 fix: treat Suspense as containing a Set of resources, not a counter (closes Suspense only working with a single Resource (closes #1805, closes #1905) (#1985) 2023-11-04 11:03:36 -04:00
Greg Johnston
7aced17976 docs: clarify need to provide context to both rendering and server function handler (#1983) 2023-11-03 18:34:50 -04:00
Gabriel Hansson
191b40b2ac docs: point leptos_server docs.rs url to latest version. (#1982) 2023-11-03 17:05:42 -04:00
Gabriel Hansson
15ca5bec61 docs: fix <Transition/> url in 12_transition.md (#1980) 2023-11-03 17:00:26 -04:00
Gabriel Hansson
ba4d226004 docs: Fix 08_parent_child.md callback example code. (#1976) 2023-11-03 16:58:45 -04:00
Chris
3adfd334df fix: leptos_router::params_map! (#1973)
Fixing implementation comes with the benefit of knocking a crate out of
the deps tree (`common_macros`).
2023-11-02 16:29:50 -04:00
martin frances
d7ca5f2e96 chore: typed-builder and typed-builder-macro - bumped version numbers. (#1958) 2023-10-29 21:49:48 -04:00
Chris
67bdb3498f docs: switch feature flag stable to nightly (#1959) 2023-10-29 21:48:53 -04:00
Greg Johnston
9e9386b223 does this make clippy happy in CI? (#1965) 2023-10-29 21:48:33 -04:00
SleeplessOne1917
4029de2d42 feat: impl IntoAttribute for TextProp (#1925) 2023-10-27 17:10:09 -04:00
Greg Johnston
777095670e fix: add leptos_axum::build_static_routes (closes #1843) (#1855) 2023-10-27 17:09:52 -04:00
koopa
a11c6303e2 feat: allow arbitrary attributes for <A/> component (#1953) 2023-10-27 15:30:30 -04:00
Daniél Kerkmann
3394e316b7 docs: add ignoring #[server] macro for helix as well (#1951)
Update helix configuration for the newest version.
To be consistent, adding the `server` ignore entry to helix as well.
Also sorting the parameters alphabetically.
2023-10-27 13:55:00 -04:00
Ari Seyhun
4b0437394c feat: impl IntoAttribute for Cow<'static, str> (#1945) 2023-10-27 13:48:43 -04:00
Ari Seyhun
d10a566e48 feat: add new method to Trigger (#1935) 2023-10-27 13:48:15 -04:00
martin frances
e0cca3e7a3 workflows: bumped tj-actions/changed-files to @39. (#1942) 2023-10-27 13:47:56 -04:00
martin frances
0c8ab7c725 workflows: bump setup-node to version 4. (#1944) 2023-10-27 13:47:33 -04:00
Ari Seyhun
a2bef05a4b perf: IntoView and IntoAttribute for std::fmt::Arguments improvements (#1947)
* fix: use static str when possible in `std::fmt::Arguments` in views

* feat: impl `IntoAttribute` for `std::fmt::Arguments`
2023-10-27 13:42:27 -04:00
Greg Johnston
6361985fb1 fix: relax 'static bound on as_child_of_current_owner (#1955) 2023-10-27 13:20:34 -04:00
Greg Johnston
ad290f5ed2 chore: update README.md to remove note about 0.5 2023-10-24 22:12:12 -04:00
Greg Johnston
5f53a1459e v0.5.2 2023-10-24 21:03:29 -04:00
Greg Johnston
379623d548 chore: fix SSR tests (#1943) 2023-10-24 17:53:45 -04:00
127 changed files with 2356 additions and 988 deletions

View File

@@ -1,4 +1,4 @@
name: Check Examples
name: CI Examples
on:
push:
@@ -11,12 +11,10 @@ on:
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
get-examples-matrix:
uses: ./.github/workflows/get-examples-matrix.yml
test:
name: Check
name: CI
needs: [get-leptos-changed, get-examples-matrix]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
strategy:
@@ -25,5 +23,5 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -1,4 +1,4 @@
name: Check stable
name: CI Stable Examples
on:
push:
@@ -13,7 +13,7 @@ jobs:
uses: ./.github/workflows/get-leptos-changed.yml
test:
name: Check
name: CI
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
strategy:
@@ -22,5 +22,5 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
cargo_make_task: "ci"
toolchain: stable

View File

@@ -26,7 +26,7 @@ jobs:
- name: Get example project directories that changed
id: changed-dirs
uses: tj-actions/changed-files@v36
uses: tj-actions/changed-files@v39
with:
dir_names: true
dir_names_max_depth: "2"

View File

@@ -21,7 +21,7 @@ jobs:
- name: Get example files that changed
id: changed-files
uses: tj-actions/changed-files@v36
uses: tj-actions/changed-files@v39
with:
files: |
examples

View File

@@ -19,7 +19,7 @@ jobs:
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
uses: tj-actions/changed-files@v39
with:
files: |
integrations

View File

@@ -53,7 +53,7 @@ jobs:
run: trunk --version
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18

View File

@@ -1,26 +0,0 @@
name: CI Examples
on:
workflow_dispatch:
push:
tags:
- v*
schedule:
# Run once a day at 3:00 AM EST
- cron: "0 8 * * *"
jobs:
get-examples-matrix:
uses: ./.github/workflows/get-examples-matrix.yml
test:
name: CI
needs: [get-examples-matrix]
strategy:
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -26,22 +26,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.5.1"
version = "0.5.3"
[workspace.dependencies]
leptos = { path = "./leptos", version = "0.5.1" }
leptos_dom = { path = "./leptos_dom", version = "0.5.1" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.1" }
leptos_macro = { path = "./leptos_macro", version = "0.5.1" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.1" }
leptos_server = { path = "./leptos_server", version = "0.5.1" }
server_fn = { path = "./server_fn", version = "0.5.1" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.1" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.1" }
leptos_config = { path = "./leptos_config", version = "0.5.1" }
leptos_router = { path = "./router", version = "0.5.1" }
leptos_meta = { path = "./meta", version = "0.5.1" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.1" }
leptos = { path = "./leptos", version = "0.5.3" }
leptos_dom = { path = "./leptos_dom", version = "0.5.3" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.3" }
leptos_macro = { path = "./leptos_macro", version = "0.5.3" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.3" }
leptos_server = { path = "./leptos_server", version = "0.5.3" }
server_fn = { path = "./server_fn", version = "0.5.3" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.3" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.3" }
leptos_config = { path = "./leptos_config", version = "0.5.3" }
leptos_router = { path = "./router", version = "0.5.3" }
leptos_meta = { path = "./meta", version = "0.5.3" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.3" }
[profile.release]
codegen-units = 1

View File

@@ -48,10 +48,6 @@ pub fn main() {
}
```
### Important Note
This example, and the entire `main` branch, now reflect the upcoming `0.5.1` release. You can use `0.5.1` with the `0.5.1-beta` release on crates.io or by a git dependency on the `main` branch of this repo. [Click here for the 0.4.9 `README`](https://crates.io/crates/leptos).
## About the Framework
Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces.

View File

@@ -10,7 +10,10 @@ l0410 = { package = "leptos", version = "0.4.10", features = [
] }
leptos = { path = "../leptos", features = ["ssr", "nightly"] }
leptos_reactive = { path = "../leptos_reactive", features = ["ssr", "nightly"] }
tachydom = { git = "https://github.com/gbj/tachys", features = ["nightly"] }
tachydom = { git = "https://github.com/gbj/tachys", features = [
"nightly",
"leptos",
] }
tachy_maccy = { git = "https://github.com/gbj/tachys", features = ["nightly"] }
sycamore = { version = "0.8", features = ["ssr"] }
yew = { version = "0.20", features = ["ssr"] }

View File

@@ -30,8 +30,7 @@ fn leptos_ssr_bench(b: &mut Bencher) {
assert_eq!(
rendered,
"<main data-hk=\"0-0-1\"><h1 data-hk=\"0-0-2\">Welcome to our benchmark page.</h1><p data-hk=\"0-0-3\">Here&#x27;s some introductory text.</p><div data-hk=\"0-0-5\"><button data-hk=\"0-0-6\">-1</button><span data-hk=\"0-0-7\">Value: <!>1<!--hk=0-0-8-->!</span><button data-hk=\"0-0-9\">+1</button></div><!--hk=0-0-4--><div data-hk=\"0-0-11\"><button data-hk=\"0-0-12\">-1</button><span data-hk=\"0-0-13\">Value: <!>2<!--hk=0-0-14-->!</span><button data-hk=\"0-0-15\">+1</button></div><!--hk=0-0-10--><div data-hk=\"0-0-17\"><button data-hk=\"0-0-18\">-1</button><span data-hk=\"0-0-19\">Value: <!>3<!--hk=0-0-20-->!</span><button data-hk=\"0-0-21\">+1</button></div><!--hk=0-0-16--></main>"
);
"<main data-hk=\"0-0-0-1\"><h1 data-hk=\"0-0-0-2\">Welcome to our benchmark page.</h1><p data-hk=\"0-0-0-3\">Here&#x27;s some introductory text.</p><div data-hk=\"0-0-0-5\"><button data-hk=\"0-0-0-6\">-1</button><span data-hk=\"0-0-0-7\">Value: <!>1<!--hk=0-0-0-8-->!</span><button data-hk=\"0-0-0-9\">+1</button></div><!--hk=0-0-0-4--><div data-hk=\"0-0-0-11\"><button data-hk=\"0-0-0-12\">-1</button><span data-hk=\"0-0-0-13\">Value: <!>2<!--hk=0-0-0-14-->!</span><button data-hk=\"0-0-0-15\">+1</button></div><!--hk=0-0-0-10--><div data-hk=\"0-0-0-17\"><button data-hk=\"0-0-0-18\">-1</button><span data-hk=\"0-0-0-19\">Value: <!>3<!--hk=0-0-0-20-->!</span><button data-hk=\"0-0-0-21\">+1</button></div><!--hk=0-0-0-16--></main>" );
});
r.dispose();
}
@@ -40,10 +39,15 @@ fn leptos_ssr_bench(b: &mut Bencher) {
fn tachys_ssr_bench(b: &mut Bencher) {
use leptos::{create_runtime, create_signal, SignalGet, SignalUpdate};
use tachy_maccy::view;
use tachydom::view::Render;
use tachydom::view::{Render, RenderHtml};
use tachydom::html::element::ElementChild;
use tachydom::html::attribute::global::ClassAttribute;
use tachydom::html::attribute::global::GlobalAttributes;
use tachydom::html::attribute::global::OnAttribute;
use tachydom::renderer::dom::Dom;
let rt = create_runtime();
b.iter(|| {
fn counter(initial: i32) -> impl Render {
fn counter(initial: i32) -> impl Render<Dom> + RenderHtml<Dom> {
let (value, set_value) = create_signal(initial);
view! {
<div>
@@ -54,7 +58,6 @@ fn tachys_ssr_bench(b: &mut Bencher) {
}
}
let mut buf = String::with_capacity(1024);
let rendered = view! {
<main>
<h1>"Welcome to our benchmark page."</h1>
@@ -63,10 +66,9 @@ fn tachys_ssr_bench(b: &mut Bencher) {
{counter(2)}
{counter(3)}
</main>
};
rendered.to_html(&mut buf, &Default::default());
}.to_html();
assert_eq!(
buf,
rendered,
"<main><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><div><button>-1</button><span>Value: <!>1<!>!</span><button>+1</button></div><div><button>-1</button><span>Value: <!>2<!>!</span><button>+1</button></div><div><button>-1</button><span>Value: <!>3<!>!</span><button>+1</button></div></main>"
);
});

View File

@@ -27,15 +27,12 @@ fn tachys_todomvc_ssr(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
use crate::todomvc::tachys::*;
use tachydom::view::Render;
use tachydom::view::{Render, RenderHtml};
let mut buf = String::new();
let rendered = TodoMVC(Todos::new());
rendered.to_html(&mut buf, &Default::default());
let rendered = TodoMVC(Todos::new()).to_html();
assert_eq!(
buf,
"<main><section class=\"todoapp\"><header class=\"header\"><h1>todos</h1><input placeholder=\"What needs to be done?\" autofocus=\"\" class=\"new-todo\"></header><section class=\"main hidden\"><input id=\"toggle-all\" type=\"checkbox\" class=\"toggle-all\"><label for=\"toggle-all\">Mark all as complete</label><ul class=\"todo-list\"></ul></section><footer class=\"footer hidden\"><span class=\"todo-count\"><strong>0</strong><!> items<!> left</span><ul class=\"filters\"><li><a href=\"#/\" class=\"selected selected\">All</a></li><li><a href=\"#/active\" class=\"\">Active</a></li><li><a href=\"#/completed\" class=\"\">Completed</a></li></ul><button class=\"clear-completed hidden hidden\">Clear completed</button></footer></section><footer class=\"info\"><p>Double-click to edit a todo</p><p>Created by <a href=\"http://todomvc.com\">Greg Johnston</a></p><p>Part of <a href=\"http://todomvc.com\">TodoMVC</a></p></footer></main>"
);
rendered,
"<main><section class=\"todoapp\"><header class=\"header\"><h1>todos</h1><input placeholder=\"What needs to be done?\" autofocus class=\"new-todo\"></header><section class=\"main hidden\"><input id=\"toggle-all\" type=\"checkbox\" class=\"toggle-all\"><label for=\"toggle-all\">Mark all as complete</label><ul class=\"todo-list\"></ul></section><footer class=\"footer hidden\"><span class=\"todo-count\"><strong>0</strong><!> items<!> left</span><ul class=\"filters\"><li><a href=\"#/\" class=\"selected selected\">All</a></li><li><a href=\"#/active\" class=\"\">Active</a></li><li><a href=\"#/completed\" class=\"\">Completed</a></li></ul><button class=\"clear-completed hidden hidden\">Clear completed</button></footer></section><footer class=\"info\"><p>Double-click to edit a todo</p><p>Created by <a href=\"http://todomvc.com\">Greg Johnston</a></p><p>Part of <a href=\"http://todomvc.com\">TodoMVC</a></p></footer></main>" );
});
runtime.dispose();
}
@@ -94,12 +91,10 @@ fn tachys_todomvc_ssr_with_1000(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
use crate::todomvc::tachys::*;
use tachydom::view::Render;
use tachydom::view::{Render, RenderHtml};
let mut buf = String::new();
let rendered = TodoMVC(Todos::new_with_1000());
rendered.to_html(&mut buf, &Default::default());
assert!(buf.len() > 20_000)
let rendered = TodoMVC(Todos::new_with_1000()).to_html();
assert!(rendered.len() > 20_000)
});
runtime.dispose();
}

View File

@@ -1,7 +1,14 @@
pub use leptos_reactive::*;
use miniserde::*;
use tachy_maccy::view;
use tachydom::view::Render;
use tachydom::{
html::{
attribute::global::{ClassAttribute, GlobalAttributes, OnAttribute},
element::ElementChild,
},
renderer::dom::Dom,
view::{keyed::keyed, Render, RenderHtml},
};
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
@@ -103,7 +110,7 @@ impl Todo {
const ESCAPE_KEY: u32 = 27;
const ENTER_KEY: u32 = 13;
pub fn TodoMVC(todos: Todos) -> impl Render {
pub fn TodoMVC(todos: Todos) -> impl Render<Dom> + RenderHtml<Dom> {
let mut next_id = todos
.0
.iter()
@@ -178,7 +185,7 @@ pub fn TodoMVC(todos: Todos) -> impl Render {
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus=""
autofocus
/>
</header>
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
@@ -191,7 +198,9 @@ pub fn TodoMVC(todos: Todos) -> impl Render {
/>
<label r#for="toggle-all">"Mark all as complete"</label>
<ul class="todo-list">
{filtered_todos.get().into_iter().map(Todo).collect::<Vec<_>>()}
{move || {
keyed(filtered_todos.get(), |todo| todo.id, Todo)
}}
</ul>
</section>
<footer class="footer" class:hidden=move || todos.with(|t| t.is_empty())>
@@ -239,7 +248,7 @@ pub fn TodoMVC(todos: Todos) -> impl Render {
}
}
pub fn Todo(todo: Todo) -> impl Render {
pub fn Todo(todo: Todo) -> impl Render<Dom> + RenderHtml<Dom> {
let (editing, set_editing) = create_signal(false);
let set_todos = use_context::<WriteSignal<Todos>>().unwrap();
//let input = NodeRef::new();

View File

@@ -12,3 +12,4 @@ mdbook serve
```
It should be available at `http://localhost:3000`.

View File

@@ -7,4 +7,4 @@ runnable = false
[preprocessor.admonish]
command = "mdbook-admonish"
assets_version = "2.0.2" # do not edit: managed by `mdbook-admonish install`
assets_version = "3.0.1" # do not edit: managed by `mdbook-admonish install`

View File

@@ -1,31 +1,18 @@
@charset "UTF-8";
:root {
--md-admonition-icon--note:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/></svg>");
--md-admonition-icon--abstract:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>");
--md-admonition-icon--info:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>");
--md-admonition-icon--tip:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>");
--md-admonition-icon--success:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>");
--md-admonition-icon--question:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>");
--md-admonition-icon--warning:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>");
--md-admonition-icon--failure:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>");
--md-admonition-icon--danger:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>");
--md-admonition-icon--bug:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>");
--md-admonition-icon--example:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 0 1 .75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z'/></svg>");
--md-admonition-icon--quote:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z'/></svg>");
--md-details-icon:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z'/></svg>");
--md-admonition-icon--admonish-note: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/></svg>");
--md-admonition-icon--admonish-abstract: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>");
--md-admonition-icon--admonish-info: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>");
--md-admonition-icon--admonish-tip: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>");
--md-admonition-icon--admonish-success: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>");
--md-admonition-icon--admonish-question: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>");
--md-admonition-icon--admonish-warning: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>");
--md-admonition-icon--admonish-failure: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>");
--md-admonition-icon--admonish-danger: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>");
--md-admonition-icon--admonish-bug: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>");
--md-admonition-icon--admonish-example: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 0 1 .75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z'/></svg>");
--md-admonition-icon--admonish-quote: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z'/></svg>");
--md-details-icon: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z'/></svg>");
}
:is(.admonition) {
@@ -84,6 +71,8 @@ a.admonition-anchor-link::before {
padding-inline: 4.4rem 1.2rem;
font-weight: 700;
background-color: rgba(68, 138, 255, 0.1);
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
display: flex;
}
:is(.admonition-title, summary.admonition-title) p {
@@ -99,6 +88,8 @@ html :is(.admonition-title, summary.admonition-title):last-child {
width: 2rem;
height: 2rem;
background-color: #448aff;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
-webkit-mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
mask-repeat: no-repeat;
@@ -132,204 +123,204 @@ details[open].admonition > summary.admonition-title::after {
transform: rotate(90deg);
}
:is(.admonition):is(.note) {
:is(.admonition):is(.admonish-note) {
border-color: #448aff;
}
:is(.note) > :is(.admonition-title, summary.admonition-title) {
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(68, 138, 255, 0.1);
}
:is(.note) > :is(.admonition-title, summary.admonition-title)::before {
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #448aff;
mask-image: var(--md-admonition-icon--note);
-webkit-mask-image: var(--md-admonition-icon--note);
mask-image: var(--md-admonition-icon--admonish-note);
-webkit-mask-image: var(--md-admonition-icon--admonish-note);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.abstract, .summary, .tldr) {
:is(.admonition):is(.admonish-abstract, .admonish-summary, .admonish-tldr) {
border-color: #00b0ff;
}
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary.admonition-title) {
:is(.admonish-abstract, .admonish-summary, .admonish-tldr) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 176, 255, 0.1);
}
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary.admonition-title)::before {
:is(.admonish-abstract, .admonish-summary, .admonish-tldr) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00b0ff;
mask-image: var(--md-admonition-icon--abstract);
-webkit-mask-image: var(--md-admonition-icon--abstract);
mask-image: var(--md-admonition-icon--admonish-abstract);
-webkit-mask-image: var(--md-admonition-icon--admonish-abstract);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.info, .todo) {
:is(.admonition):is(.admonish-info, .admonish-todo) {
border-color: #00b8d4;
}
:is(.info, .todo) > :is(.admonition-title, summary.admonition-title) {
:is(.admonish-info, .admonish-todo) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 184, 212, 0.1);
}
:is(.info, .todo) > :is(.admonition-title, summary.admonition-title)::before {
:is(.admonish-info, .admonish-todo) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00b8d4;
mask-image: var(--md-admonition-icon--info);
-webkit-mask-image: var(--md-admonition-icon--info);
mask-image: var(--md-admonition-icon--admonish-info);
-webkit-mask-image: var(--md-admonition-icon--admonish-info);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.tip, .hint, .important) {
:is(.admonition):is(.admonish-tip, .admonish-hint, .admonish-important) {
border-color: #00bfa5;
}
:is(.tip, .hint, .important) > :is(.admonition-title, summary.admonition-title) {
:is(.admonish-tip, .admonish-hint, .admonish-important) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 191, 165, 0.1);
}
:is(.tip, .hint, .important) > :is(.admonition-title, summary.admonition-title)::before {
:is(.admonish-tip, .admonish-hint, .admonish-important) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00bfa5;
mask-image: var(--md-admonition-icon--tip);
-webkit-mask-image: var(--md-admonition-icon--tip);
mask-image: var(--md-admonition-icon--admonish-tip);
-webkit-mask-image: var(--md-admonition-icon--admonish-tip);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.success, .check, .done) {
:is(.admonition):is(.admonish-success, .admonish-check, .admonish-done) {
border-color: #00c853;
}
:is(.success, .check, .done) > :is(.admonition-title, summary.admonition-title) {
:is(.admonish-success, .admonish-check, .admonish-done) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 200, 83, 0.1);
}
:is(.success, .check, .done) > :is(.admonition-title, summary.admonition-title)::before {
:is(.admonish-success, .admonish-check, .admonish-done) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00c853;
mask-image: var(--md-admonition-icon--success);
-webkit-mask-image: var(--md-admonition-icon--success);
mask-image: var(--md-admonition-icon--admonish-success);
-webkit-mask-image: var(--md-admonition-icon--admonish-success);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.question, .help, .faq) {
:is(.admonition):is(.admonish-question, .admonish-help, .admonish-faq) {
border-color: #64dd17;
}
:is(.question, .help, .faq) > :is(.admonition-title, summary.admonition-title) {
:is(.admonish-question, .admonish-help, .admonish-faq) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(100, 221, 23, 0.1);
}
:is(.question, .help, .faq) > :is(.admonition-title, summary.admonition-title)::before {
:is(.admonish-question, .admonish-help, .admonish-faq) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #64dd17;
mask-image: var(--md-admonition-icon--question);
-webkit-mask-image: var(--md-admonition-icon--question);
mask-image: var(--md-admonition-icon--admonish-question);
-webkit-mask-image: var(--md-admonition-icon--admonish-question);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.warning, .caution, .attention) {
:is(.admonition):is(.admonish-warning, .admonish-caution, .admonish-attention) {
border-color: #ff9100;
}
:is(.warning, .caution, .attention) > :is(.admonition-title, summary.admonition-title) {
:is(.admonish-warning, .admonish-caution, .admonish-attention) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(255, 145, 0, 0.1);
}
:is(.warning, .caution, .attention) > :is(.admonition-title, summary.admonition-title)::before {
:is(.admonish-warning, .admonish-caution, .admonish-attention) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #ff9100;
mask-image: var(--md-admonition-icon--warning);
-webkit-mask-image: var(--md-admonition-icon--warning);
mask-image: var(--md-admonition-icon--admonish-warning);
-webkit-mask-image: var(--md-admonition-icon--admonish-warning);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.failure, .fail, .missing) {
:is(.admonition):is(.admonish-failure, .admonish-fail, .admonish-missing) {
border-color: #ff5252;
}
:is(.failure, .fail, .missing) > :is(.admonition-title, summary.admonition-title) {
:is(.admonish-failure, .admonish-fail, .admonish-missing) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(255, 82, 82, 0.1);
}
:is(.failure, .fail, .missing) > :is(.admonition-title, summary.admonition-title)::before {
:is(.admonish-failure, .admonish-fail, .admonish-missing) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #ff5252;
mask-image: var(--md-admonition-icon--failure);
-webkit-mask-image: var(--md-admonition-icon--failure);
mask-image: var(--md-admonition-icon--admonish-failure);
-webkit-mask-image: var(--md-admonition-icon--admonish-failure);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.danger, .error) {
:is(.admonition):is(.admonish-danger, .admonish-error) {
border-color: #ff1744;
}
:is(.danger, .error) > :is(.admonition-title, summary.admonition-title) {
:is(.admonish-danger, .admonish-error) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(255, 23, 68, 0.1);
}
:is(.danger, .error) > :is(.admonition-title, summary.admonition-title)::before {
:is(.admonish-danger, .admonish-error) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #ff1744;
mask-image: var(--md-admonition-icon--danger);
-webkit-mask-image: var(--md-admonition-icon--danger);
mask-image: var(--md-admonition-icon--admonish-danger);
-webkit-mask-image: var(--md-admonition-icon--admonish-danger);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.bug) {
:is(.admonition):is(.admonish-bug) {
border-color: #f50057;
}
:is(.bug) > :is(.admonition-title, summary.admonition-title) {
:is(.admonish-bug) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(245, 0, 87, 0.1);
}
:is(.bug) > :is(.admonition-title, summary.admonition-title)::before {
:is(.admonish-bug) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #f50057;
mask-image: var(--md-admonition-icon--bug);
-webkit-mask-image: var(--md-admonition-icon--bug);
mask-image: var(--md-admonition-icon--admonish-bug);
-webkit-mask-image: var(--md-admonition-icon--admonish-bug);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.example) {
:is(.admonition):is(.admonish-example) {
border-color: #7c4dff;
}
:is(.example) > :is(.admonition-title, summary.admonition-title) {
:is(.admonish-example) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(124, 77, 255, 0.1);
}
:is(.example) > :is(.admonition-title, summary.admonition-title)::before {
:is(.admonish-example) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #7c4dff;
mask-image: var(--md-admonition-icon--example);
-webkit-mask-image: var(--md-admonition-icon--example);
mask-image: var(--md-admonition-icon--admonish-example);
-webkit-mask-image: var(--md-admonition-icon--admonish-example);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.quote, .cite) {
:is(.admonition):is(.admonish-quote, .admonish-cite) {
border-color: #9e9e9e;
}
:is(.quote, .cite) > :is(.admonition-title, summary.admonition-title) {
:is(.admonish-quote, .admonish-cite) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(158, 158, 158, 0.1);
}
:is(.quote, .cite) > :is(.admonition-title, summary.admonition-title)::before {
:is(.admonish-quote, .admonish-cite) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #9e9e9e;
mask-image: var(--md-admonition-icon--quote);
-webkit-mask-image: var(--md-admonition-icon--quote);
mask-image: var(--md-admonition-icon--admonish-quote);
-webkit-mask-image: var(--md-admonition-icon--admonish-quote);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
@@ -340,7 +331,8 @@ details[open].admonition > summary.admonition-title::after {
background-color: var(--sidebar-bg);
}
.ayu :is(.admonition), .coal :is(.admonition) {
.ayu :is(.admonition),
.coal :is(.admonition) {
background-color: var(--theme-hover);
}

View File

@@ -45,8 +45,8 @@
- [Responses and Redirects](./server/27_response.md)
- [Progressive Enhancement and Graceful Degradation](./progressive_enhancement/README.md)
- [`<ActionForm/>`s](./progressive_enhancement/action_form.md)
- [Deployment](./deployment.md)
- [Deployment](./deployment/README.md)
- [Optimizing WASM Binary Size](./deployment/binary_size.md)
- [Guide: Islands](./islands.md)
- [Appendix: How Does the Reactive System Work?](./appendix_reactive_graph.md)
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)
- [Appendix: Some Small DX Improvements](./appendix_dx.md)

View File

@@ -13,8 +13,8 @@ VSCode `settings.json`:
```json
"rust-analyzer.procMacro.ignored": {
"leptos_macro": [
"server",
"component"
"component",
"server"
],
}
```
@@ -30,8 +30,8 @@ require('lspconfig').rust_analyzer.setup {
procMacro = {
ignored = {
leptos_macro = {
"server",
"component",
"server",
},
},
},
@@ -45,7 +45,9 @@ Helix, in `.helix/languages.toml`:
```toml
[[language]]
name = "rust"
config = { procMacro = {ignored = {leptos_macro = ["component"]}}}
[language-server.rust-analyzer]
config = { procMacro = { ignored = { leptos_macro = ["component", "server"] } } }
```
```admonish info

View File

@@ -47,9 +47,9 @@ view! {
Resources also provide a `refetch()` method that allows you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-resources-0-5-9jq86q?file=%2Fsrc%2Fmain.rs%3A1%2C2)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-resources-0-5-x6h5j6?file=%2Fsrc%2Fmain.rs%3A2%2C3)
<iframe src="https://codesandbox.io/p/sandbox/10-resources-0-5-9jq86q?file=%2Fsrc%2Fmain.rs%3A1%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<iframe src="https://codesandbox.io/p/sandbox/10-resources-0-5-9jq86q?file=%2Fsrc%2Fmain.rs%3A2%2C3" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>

View File

@@ -1,6 +1,6 @@
# `<Transition/>`
Youll notice in the `<Suspense/>` example that if you keep reloading the data, it keeps flickering back to `"Loading..."`. Sometimes this is fine. For other times, theres [`<Transition/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html).
Youll notice in the `<Suspense/>` example that if you keep reloading the data, it keeps flickering back to `"Loading..."`. Sometimes this is fine. For other times, theres [`<Transition/>`](https://docs.rs/leptos/latest/leptos/fn.Transition.html).
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.

View File

@@ -6,6 +6,7 @@ There are as many ways to deploy a web application as there are developers, let
1. Remember: Always deploy Rust apps built in `--release` mode, not debug mode. This has a huge effect on both performance and binary size.
2. Test locally in release mode as well. The framework applies certain optimizations in release mode that it does not apply in debug mode, so its possible for bugs to surface at this point. (If your app behaves differently or you do encounter a bug, its likely a framework-level bug and you should open a GitHub issue with a reproduction.)
3. See the chapter on "Optimizing WASM Binary Size" for additional tips and tricks to further improve the time-to-interactive metric for your WASM app on first load.
> We asked users to submit their deployment setups to help with this chapter. Ill quote from them below, but you can read the full thread [here](https://github.com/leptos-rs/leptos/issues/1152).
@@ -63,7 +64,6 @@ WORKDIR /app
# Set any required env variables and
ENV RUST_LOG="info"
ENV APP_ENVIRONMENT="production"
ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
ENV LEPTOS_SITE_ROOT="site"
EXPOSE 8080

View File

@@ -1,4 +1,4 @@
# Appendix: Optimizing WASM Binary Size
# Optimizing WASM Binary Size
One of the primary downsides of deploying a Rust/WebAssembly frontend app is that splitting a WASM file into smaller chunks to be dynamically loaded is significantly more difficult than splitting a JavaScript bundle. There have been experiments like [`wasm-split`](https://emscripten.org/docs/optimizing/Module-Splitting.html) in the Emscripten ecosystem but at present theres no way to split and dynamically load a Rust/`wasm-bindgen` binary. This means that the whole WASM binary needs to be loaded before your app becomes interactive. Because the WASM format is designed for streaming compilation, WASM files are much faster to compile per kilobyte than JavaScript files. (For a deeper look, you can [read this great article from the Mozilla team](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/) on streaming WASM compilation.)
@@ -59,13 +59,13 @@ And you'll need to add `panic = "abort"` to `[profile.release]` in `Cargo.toml`.
## Things to Avoid
There are certain crates that tend to inflate binary sizes. For example, the `regex` crate with its default features adds about 500kb to a WASM binary (largely because it has to pull in Unicode table data!) In a size-conscious setting, you might consider avoiding regexes in general, or even dropping down and calling browser APIs to use the built-in regex engine instead. (This is what `leptos_router` does on the few occasions it needs a regular expression.)
There are certain crates that tend to inflate binary sizes. For example, the `regex` crate with its default features adds about 500kb to a WASM binary (largely because it has to pull in Unicode table data!). In a size-conscious setting, you might consider avoiding regexes in general, or even dropping down and calling browser APIs to use the built-in regex engine instead. (This is what `leptos_router` does on the few occasions it needs a regular expression.)
In general, Rusts commitment to runtime performance is sometimes at odds with a commitment to a small binary. For example, Rust monomorphizes generic functions, meaning it creates a distinct copy of the function for each generic type its called with. This is significantly faster than dynamic dispatch, but increases binary size. Leptos tries to balance runtime performance with binary size considerations pretty carefully; but you might find that writing code that uses many generics tends to increase binary size. For example, if you have a generic component with a lot of code in its body and call it with four different types, remember that the compiler could include four copies of that same code. Refactoring to use a concrete inner function or helper can often maintain performance and ergonomics while reducing binary size.
## A Final Thought
Remember that in a server-rendered app, JS bundle size/WASM binary size affects only _one_ thing: time to interactivity on the first load. This is very important to a good user experiencenobody wants to click a button three times and have it do nothing because the interactive code is still loadingbut it is not the only important measure.
Remember that in a server-rendered app, JS bundle size/WASM binary size affects only _one_ thing: time to interactivity on the first load. This is very important to a good user experience: nobody wants to click a button three times and have it do nothing because the interactive code is still loadingbut it's not the only important measure.
Its especially worth remembering that streaming in a single WASM binary means all subsequent navigations are nearly instantaneous, depending only on any additional data loading. Precisely because your WASM binary is _not_ bundle split, navigating to a new route does not require loading additional JS/WASM, as it does in nearly every JavaScript framework. Is this copium? Maybe. Or maybe its just an honest trade-off between the two approaches!

View File

@@ -28,7 +28,7 @@ Theres a very simple way to determine whether you should use a capital-S `<Sc
## `<Body/>` and `<Html/>`
There are even a couple elements designed to make semantic HTML and styling easier. [`<Html/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) lets you set the `lang` and `dir` on your `<html>` tag from your application code. `<Html/>` and [`<Body/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) both have `class` props that let you set their respective `class` attributes, which is sometimes needed by CSS frameworks for styling.
There are even a couple elements designed to make semantic HTML and styling easier. [`<Html/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) lets you set the `lang` and `dir` on your `<html>` tag from your application code. `<Html/>` and [`<Body/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Body.html) both have `class` props that let you set their respective `class` attributes, which is sometimes needed by CSS frameworks for styling.
`<Body/>` and `<Html/>` both also have `attributes` props which can be used to set any number of additional attributes on them via the `attr:` syntax:

View File

@@ -56,3 +56,45 @@ let on_submit = move |ev| {
}
}
```
## Complex Inputs
Server function arguments that are structs with nested serializable fields should make use of indexing notation of `serde_qs`.
```rust
use leptos::*;
use leptos_router::*;
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct HeftyData {
first_name: String,
last_name: String,
}
#[component]
fn ComplexInput() -> impl IntoView {
let submit = Action::<VeryImportantFn, _>::server();
view! {
<ActionForm action=submit>
<input type="text" name="hefty_arg[first_name]" value="leptos"/>
<input
type="text"
name="hefty_arg[last_name]"
value="closures-everywhere"
/>
<input type="submit"/>
</ActionForm>
}
}
#[server]
async fn very_important_fn(
hefty_arg: HeftyData,
) -> Result<(), ServerFnError> {
assert_eq!(hefty_arg.first_name.as_str(), "leptos");
assert_eq!(hefty_arg.last_name.as_str(), "closures-everywhere");
Ok(())
}
```

View File

@@ -34,7 +34,7 @@ pub fn FormExample() -> impl IntoView {
view! {
<Form method="GET" action="">
<input type="search" name="search" value=search/>
<input type="search" name="q" value=search/>
<input type="submit"/>
</Form>
<Transition fallback=move || ()>
@@ -53,7 +53,7 @@ We can actually take it a step further and do something kind of clever:
```rust
view! {
<Form method="GET" action="">
<input type="search" name="search" value=search
<input type="search" name="q" value=search
oninput="this.form.requestSubmit()"
/>
</Form>

View File

@@ -158,4 +158,4 @@ In particular, youll sometimes see errors about the crate `mio` or missing th
You can use `create_effect` to specify that something should only run on the client, and not in the server. Is there a way to specify that something should run only on the server, and not the client?
In fact, there is. The next chapter will cover the topic of server functions in some detail. (In the meantime, you can check out their docs [here](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html).)
In fact, there is. The next chapter will cover the topic of server functions in some detail. (In the meantime, you can check out their docs [here](https://docs.rs/leptos_server/latest/leptos_server/index.html).)

View File

@@ -113,8 +113,7 @@ pub fn App() -> impl IntoView {
#[component]
pub fn ButtonB<F>(on_click: F) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
pub fn ButtonB(#[prop(into)] on_click: Callback<MouseEvent>) -> impl IntoView
F: Fn(MouseEvent) + 'static
{
view! {
<button on:click=on_click>

View File

@@ -40,6 +40,8 @@ Example projects depend on the following tools. Please install them as needed.
- [Cargo Make](https://sagiegurari.github.io/cargo-make/)
- Run `cargo install --force cargo-make`
- Setup a command alias like `alias cm='cargo make'` to reduce typing (**_Optional_**)
- [Trunk](https://github.com/thedodd/trunk)
- Run `cargo install trunk`
- [Node Version Manager](https://github.com/nvm-sh/nvm/) (**_Optional_**)
- [Node.js](https://nodejs.org/)
- [pnpm](https://pnpm.io/) (**_Optional_**)

View File

@@ -89,15 +89,15 @@ pub fn fetch_example() -> impl IntoView {
}
/>
</label>
<ErrorBoundary fallback>
<Transition fallback=move || {
view! { <div>"Loading (Suspense Fallback)..."</div> }
}>
<Transition fallback=move || {
view! { <div>"Loading (Suspense Fallback)..."</div> }
}>
<ErrorBoundary fallback>
<div>
{cats_view}
</div>
</Transition>
</ErrorBoundary>
</ErrorBoundary>
</Transition>
</div>
}
}

View File

@@ -1,14 +1,12 @@
# Leptos Hacker News Example with Axum
This example uses the basic Hacker News example as its basis, but shows how to run the server side as WASM running in a JS environment. In this example, Deno is used as the runtime.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
This example uses the basic Hacker News example as its basis, but shows how to run the server side as WASM running in a JS environment. In this example, Deno is used as the runtime.
## Server Side Rendering with Deno
To run the Deno version, run
```bash
deno task build
deno task start
deno task start
```

View File

@@ -1,8 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="css" href="/style.css"/>
</head>
<body></body>
</html>

View File

@@ -1,4 +1,4 @@
pub mod credentials;
pub mod navbar;
pub use self::{credentials::*, navbar::*};
pub use self::navbar::*;

View File

@@ -12,4 +12,5 @@ console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
wasm-bindgen = "0.2"
web-sys = "0.3"
web-sys = "0.3"
gloo-timers = { version = "0.3", features = ["futures"] }

View File

@@ -6,8 +6,12 @@ use leptos::*;
use portal::App;
use web_sys::HtmlButtonElement;
async fn next_tick() {
gloo_timers::future::TimeoutFuture::new(25).await;
}
#[wasm_bindgen_test]
fn portal() {
async fn portal() {
let document = leptos::document();
let body = document.body().unwrap();
@@ -24,7 +28,7 @@ fn portal() {
show_button.click();
// next_tick().await;
next_tick().await;
// check HTML
assert_eq!(

View File

@@ -25,16 +25,16 @@ tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8" }
sqlx = { version = "0.6.2", features = [
sqlx = { version = "0.7.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
wasm-bindgen = "0.2"
axum_session_auth = { version = "0.2.1", features = [
axum_session_auth = { version = "0.9.0", features = [
"sqlite-rustls",
], optional = true }
axum_session = { version = "0.2.3", features = [
axum_session = { version = "0.9.0", features = [
"sqlite-rustls",
], optional = true }
bcrypt = { version = "0.14", optional = true }

Binary file not shown.

View File

@@ -56,8 +56,7 @@ if #[cfg(feature = "ssr")] {
// Auth section
let session_config = SessionConfig::default().with_table_name("axum_sessions");
let auth_config = AuthConfig::<i64>::default();
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config);
session_store.initiate().await.unwrap();
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap();
sqlx::migrate!()
.run(&pool)

View File

@@ -0,0 +1,95 @@
[package]
name = "todo_app_sqlite_csr"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
leptos_integration_utils = { path = "../../integrations/utils", optional = true }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1", features = ["derive"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8" }
sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
wasm-bindgen = "0.2"
[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:sqlx",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
"dep:leptos_integration_utils",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "todo_app_sqlite_csr"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "cargo make test-ui"
end2end-dir = "e2e"
# 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
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["csr"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,12 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
]
[env]
CLIENT_PROCESS_NAME = "todo_app_sqlite_csr"
[tasks.test-ui]
cwd = "./e2e"
command = "cargo"
args = ["make", "test-ui", "${@}"]

View File

@@ -0,0 +1,15 @@
# Leptos Todo App Sqlite with CSR
This example shows how to combine client-side rendering with server functions, i.e., using server functions as a convenient way to create an ad hoc API, but without using server-side rendering and hydration.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## E2E Testing
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.

Binary file not shown.

View File

@@ -0,0 +1,18 @@
[package]
name = "todo_app_sqlite_csr_e2e"
version = "0.1.0"
edition = "2021"
[dev-dependencies]
anyhow = "1.0.72"
async-trait = "0.1.72"
cucumber = "0.19.1"
fantoccini = "0.19.3"
pretty_assertions = "1.4.0"
serde_json = "1.0.104"
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "time"] }
url = "2.4.0"
[[test]]
name = "app_suite"
harness = false # Allow Cucumber to print output instead of libtest

View File

@@ -0,0 +1,20 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.test]
env = { RUN_AUTOMATICALLY = false }
condition = { env_true = ["RUN_AUTOMATICALLY"] }
[tasks.ci]
[tasks.test-ui]
command = "cargo"
args = [
"test",
"--test",
"app_suite",
"--",
"--retry",
"5",
"--fail-fast",
"${@}",
]

View File

@@ -0,0 +1,34 @@
# E2E Testing
This example demonstrates e2e testing with Rust using executable requirements.
## Testing Stack
| | Role | Description |
|---|---|---|
| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests |
| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver |
| [Cargo Leptos ](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests |
| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome
## Testing Organization
Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements.
Here is a brief overview of how things fit together.
```bash
features
└── {action}_{object}.feature # Specify test scenarios
tests
├── fixtures
│ ├── action.rs # Perform a user action (click, type, etc.)
│ ├── check.rs # Assert what a user can see/not see
│ ├── find.rs # Query page elements
│ ├── mod.rs
│ └── world
│ ├── action_steps.rs # Map Gherkin steps to user actions
│ ├── check_steps.rs # Map Gherkin steps to user expectations
│ └── mod.rs
└── app_suite.rs # Test main
```

View File

@@ -0,0 +1,16 @@
@add_todo
Feature: Add Todo
Background:
Given I see the app
@add_todo-see
Scenario: Should see the todo
Given I set the todo as Buy Bread
When I click the Add button
Then I see the todo named Buy Bread
@add_todo-style
Scenario: Should see the pending todo
When I add a todo as Buy Oranges
Then I see the pending todo

View File

@@ -0,0 +1,18 @@
@delete_todo
Feature: Delete Todo
Background:
Given I see the app
@serial
@delete_todo-remove
Scenario: Should not see the deleted todo
Given I add a todo as Buy Yogurt
When I delete the todo named Buy Yogurt
Then I do not see the todo named Buy Yogurt
@serial
@delete_todo-message
Scenario: Should see the empty list message
When I empty the todo list
Then I see the empty list message is No tasks were found.

View File

@@ -0,0 +1,12 @@
@open_app
Feature: Open App
@open_app-title
Scenario: Should see the home page title
When I open the app
Then I see the page title is My Tasks
@open_app-label
Scenario: Should see the input label
When I open the app
Then I see the label of the input is Add a Todo

View File

@@ -0,0 +1,14 @@
mod fixtures;
use anyhow::Result;
use cucumber::World;
use fixtures::world::AppWorld;
#[tokio::main]
async fn main() -> Result<()> {
AppWorld::cucumber()
.fail_on_skipped()
.run_and_exit("./features")
.await;
Ok(())
}

View File

@@ -0,0 +1,60 @@
use super::{find, world::HOST};
use anyhow::Result;
use fantoccini::Client;
use std::result::Result::Ok;
use tokio::{self, time};
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
let url = format!("{}{}", HOST, path);
client.goto(&url).await?;
Ok(())
}
pub async fn add_todo(client: &Client, text: &str) -> Result<()> {
fill_todo(client, text).await?;
click_add_button(client).await?;
Ok(())
}
pub async fn fill_todo(client: &Client, text: &str) -> Result<()> {
let textbox = find::todo_input(client).await;
textbox.send_keys(text).await?;
Ok(())
}
pub async fn click_add_button(client: &Client) -> Result<()> {
let add_button = find::add_button(client).await;
add_button.click().await?;
Ok(())
}
pub async fn empty_todo_list(client: &Client) -> Result<()> {
let todos = find::todos(client).await;
for _todo in todos {
let _ = delete_first_todo(client).await?;
}
Ok(())
}
pub async fn delete_first_todo(client: &Client) -> Result<()> {
if let Some(element) = find::first_delete_button(client).await {
element.click().await.expect("Failed to delete todo");
time::sleep(time::Duration::from_millis(250)).await;
}
Ok(())
}
pub async fn delete_todo(client: &Client, text: &str) -> Result<()> {
if let Some(element) = find::delete_button(client, text).await {
element.click().await?;
time::sleep(time::Duration::from_millis(250)).await;
}
Ok(())
}

View File

@@ -0,0 +1,57 @@
use super::find;
use anyhow::{Ok, Result};
use fantoccini::{Client, Locator};
use pretty_assertions::assert_eq;
pub async fn text_on_element(
client: &Client,
selector: &str,
expected_text: &str,
) -> Result<()> {
let element = client
.wait()
.for_element(Locator::Css(selector))
.await
.expect(
format!("Element not found by Css selector `{}`", selector)
.as_str(),
);
let actual = element.text().await?;
assert_eq!(&actual, expected_text);
Ok(())
}
pub async fn todo_present(
client: &Client,
text: &str,
expected: bool,
) -> Result<()> {
let todo_present = is_todo_present(client, text).await;
assert_eq!(todo_present, expected);
Ok(())
}
async fn is_todo_present(client: &Client, text: &str) -> bool {
let todos = find::todos(client).await;
for todo in todos {
let todo_title = todo.text().await.expect("Todo title not found");
if todo_title == text {
return true;
}
}
false
}
pub async fn todo_is_pending(client: &Client) -> Result<()> {
if let None = find::pending_todo(client).await {
assert!(false, "Pending todo not found");
}
Ok(())
}

View File

@@ -0,0 +1,63 @@
use fantoccini::{elements::Element, Client, Locator};
pub async fn todo_input(client: &Client) -> Element {
let textbox = client
.wait()
.for_element(Locator::Css("input[name='title"))
.await
.expect("Todo textbox not found");
textbox
}
pub async fn add_button(client: &Client) -> Element {
let button = client
.wait()
.for_element(Locator::Css("input[value='Add']"))
.await
.expect("");
button
}
pub async fn first_delete_button(client: &Client) -> Option<Element> {
if let Ok(element) = client
.wait()
.for_element(Locator::Css("li:first-child input[value='X']"))
.await
{
return Some(element);
}
None
}
pub async fn delete_button(client: &Client, text: &str) -> Option<Element> {
let selector = format!("//*[text()='{text}']//input[@value='X']");
if let Ok(element) =
client.wait().for_element(Locator::XPath(&selector)).await
{
return Some(element);
}
None
}
pub async fn pending_todo(client: &Client) -> Option<Element> {
if let Ok(element) =
client.wait().for_element(Locator::Css(".pending")).await
{
return Some(element);
}
None
}
pub async fn todos(client: &Client) -> Vec<Element> {
let todos = client
.find_all(Locator::Css("li"))
.await
.expect("Todo List not found");
todos
}

View File

@@ -0,0 +1,4 @@
pub mod action;
pub mod check;
pub mod find;
pub mod world;

View File

@@ -0,0 +1,57 @@
use crate::fixtures::{action, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::{given, when};
#[given("I see the app")]
#[when("I open the app")]
async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::goto_path(client, "").await?;
Ok(())
}
#[given(regex = "^I add a todo as (.*)$")]
#[when(regex = "^I add a todo as (.*)$")]
async fn i_add_a_todo_titled(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
action::add_todo(client, text.as_str()).await?;
Ok(())
}
#[given(regex = "^I set the todo as (.*)$")]
async fn i_set_the_todo_as(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
action::fill_todo(client, &text).await?;
Ok(())
}
#[when(regex = "I click the Add button$")]
async fn i_click_the_button(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::click_add_button(client).await?;
Ok(())
}
#[when(regex = "^I delete the todo named (.*)$")]
async fn i_delete_the_todo_named(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
action::delete_todo(client, text.as_str()).await?;
Ok(())
}
#[given("the todo list is empty")]
#[when("I empty the todo list")]
async fn i_empty_the_todo_list(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::empty_todo_list(client).await?;
Ok(())
}

View File

@@ -0,0 +1,67 @@
use crate::fixtures::{check, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::then;
#[then(regex = "^I see the page title is (.*)$")]
async fn i_see_the_page_title_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::text_on_element(client, "h1", &text).await?;
Ok(())
}
#[then(regex = "^I see the label of the input is (.*)$")]
async fn i_see_the_label_of_the_input_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::text_on_element(client, "label", &text).await?;
Ok(())
}
#[then(regex = "^I see the todo named (.*)$")]
async fn i_see_the_todo_is_present(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::todo_present(client, text.as_str(), true).await?;
Ok(())
}
#[then("I see the pending todo")]
async fn i_see_the_pending_todo(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
check::todo_is_pending(client).await?;
Ok(())
}
#[then(regex = "^I see the empty list message is (.*)$")]
async fn i_see_the_empty_list_message_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::text_on_element(client, "ul p", &text).await?;
Ok(())
}
#[then(regex = "^I do not see the todo named (.*)$")]
async fn i_do_not_see_the_todo_is_present(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::todo_present(client, text.as_str(), false).await?;
Ok(())
}

View File

@@ -0,0 +1,39 @@
pub mod action_steps;
pub mod check_steps;
use anyhow::Result;
use cucumber::World;
use fantoccini::{
error::NewSessionError, wd::Capabilities, Client, ClientBuilder,
};
pub const HOST: &str = "http://127.0.0.1:3000";
#[derive(Debug, World)]
#[world(init = Self::new)]
pub struct AppWorld {
pub client: Client,
}
impl AppWorld {
async fn new() -> Result<Self, anyhow::Error> {
let webdriver_client = build_client().await?;
Ok(Self {
client: webdriver_client,
})
}
}
async fn build_client() -> Result<Client, NewSessionError> {
let mut cap = Capabilities::new();
let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap();
cap.insert("goog:chromeOptions".to_string(), arg);
let client = ClientBuilder::native()
.capabilities(cap)
.connect("http://localhost:4444")
.await?;
Ok(client)
}

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS todos
(
id INTEGER NOT NULL PRIMARY KEY,
title VARCHAR,
completed BOOLEAN
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,58 @@
use crate::errors::TodoAppError;
use leptos::{Errors, *};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
#[component]
pub fn ErrorTemplate(
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => create_rw_signal(e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),
},
};
// Get Errors from Signal
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<TodoAppError> = errors
.get()
.into_iter()
.filter_map(|(_, v)| v.downcast_ref::<TodoAppError>().cloned())
.collect();
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
#[cfg(feature = "ssr")]
{
let response = use_context::<ResponseOptions>();
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}
view! {
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.clone().into_iter().enumerate()}
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children=move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
}
}

View File

@@ -0,0 +1,21 @@
use http::status::StatusCode;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
pub enum TodoAppError {
#[error("Not Found")]
NotFound,
#[error("Internal Server Error")]
InternalServerError,
}
impl TodoAppError {
pub fn status_code(&self) -> StatusCode {
match self {
TodoAppError::NotFound => StatusCode::NOT_FOUND,
TodoAppError::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}

View File

@@ -0,0 +1,45 @@
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{Html, IntoResponse, Response as AxumResponse},
};
use leptos::LeptosOptions;
use leptos_integration_utils::html_parts_separated;
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_or_index_handler(
uri: Uri,
State(options): State<LeptosOptions>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
let (head, tail) = html_parts_separated(&options, None);
Html(format!("{head}</head><body>{tail}")).into_response()
}
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}

View File

@@ -0,0 +1,15 @@
pub mod error_template;
pub mod errors;
#[cfg(feature = "ssr")]
pub mod fallback;
pub mod todo;
#[cfg_attr(feature = "csr", wasm_bindgen::prelude::wasm_bindgen)]
pub fn hydrate() {
use crate::todo::*;
_ = console_log::init_with_level(log::Level::Error);
console_error_panic_hook::set_once();
leptos::mount_to_body(TodoApp);
}

View File

@@ -0,0 +1,52 @@
#[cfg(feature = "ssr")]
#[allow(unused)]
mod ssr_imports {
pub use axum::{
body::Body as AxumBody,
extract::{Path, State},
http::Request,
response::{Html, IntoResponse, Response},
routing::{get, post},
Router,
};
pub use leptos::*;
pub use leptos_axum::{generate_route_list, LeptosRoutes};
pub use todo_app_sqlite_csr::{
fallback::file_or_index_handler, todo::*, *,
};
}
#[cfg(feature = "ssr")]
#[cfg_attr(feature = "ssr", tokio::main)]
async fn main() {
use ssr_imports::*;
simple_logger::init_with_level(log::Level::Error)
.expect("couldn't initialize logging");
let _conn = db().await.expect("couldn't connect to DB");
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.fallback(file_or_index_handler)
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
logging::log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// This example cannot be built as a trunk standalone CSR-only app.
// Only the server may directly connect to the database.
}

View File

@@ -0,0 +1,195 @@
use crate::error_template::ErrorTemplate;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
#[cfg(feature = "ssr")]
use sqlx::{Connection, SqliteConnection};
#[cfg(feature = "ssr")]
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
}
#[server(GetTodos, "/api")]
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
let req_parts = use_context::<leptos_axum::RequestParts>();
if let Some(req_parts) = req_parts {
println!("Uri = {:?}", req_parts.uri);
}
use futures::TryStreamExt;
let mut conn = db().await?;
let mut todos = Vec::new();
let mut rows =
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows.try_next().await? {
todos.push(row);
}
// Add a random header(because why not)
// let mut res_headers = HeaderMap::new();
// res_headers.insert(SET_COOKIE, HeaderValue::from_str("fizz=buzz").unwrap());
// let res_parts = leptos_axum::ResponseParts {
// headers: res_headers,
// status: Some(StatusCode::IM_A_TEAPOT),
// };
// let res_options_outer = use_context::<leptos_axum::ResponseOptions>();
// if let Some(res_options) = res_options_outer {
// res_options.overwrite(res_parts).await;
// }
Ok(todos)
}
#[server(AddTodo, "/api")]
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
let mut conn = db().await?;
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
.bind(title)
.execute(&mut conn)
.await
{
Ok(_row) => Ok(()),
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
}
}
// The struct name and path prefix arguments are optional.
#[server]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
.bind(id)
.execute(&mut conn)
.await
.map(|_| ())?)
}
#[component]
pub fn TodoApp() -> impl IntoView {
//let id = use_context::<String>();
provide_meta_context();
view! {
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/todo_app_sqlite_csr.css"/>
<Router>
<header>
<h1>"My Tasks"</h1>
</header>
<main>
<Routes>
<Route path="" view=Todos/>
</Routes>
</main>
</Router>
}
}
#[component]
pub fn Todos() -> impl IntoView {
let add_todo = create_server_multi_action::<AddTodo>();
let delete_todo = create_server_action::<DeleteTodo>();
let submissions = add_todo.submissions();
// list of todos is loaded from the server in reaction to changes
let todos = create_resource(
move || (add_todo.version().get(), delete_todo.version().get()),
move |_| get_todos(),
);
view! {
<div>
<MultiActionForm action=add_todo>
<label>
"Add a Todo"
<input type="text" name="title"/>
</label>
<input type="submit" value="Add"/>
</MultiActionForm>
<Transition fallback=move || view! {<p>"Loading..."</p> }>
<ErrorBoundary fallback=|errors| view!{<ErrorTemplate errors=errors/>}>
{move || {
let existing_todos = {
move || {
todos.get()
.map(move |todos| match todos {
Err(e) => {
view! { <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view()
}
Ok(todos) => {
if todos.is_empty() {
view! { <p>"No tasks were found."</p> }.into_view()
} else {
todos
.into_iter()
.map(move |todo| {
view! {
<li>
{todo.title}
<ActionForm action=delete_todo>
<input type="hidden" name="id" value={todo.id}/>
<input type="submit" value="X"/>
</ActionForm>
</li>
}
})
.collect_view()
}
}
})
.unwrap_or_default()
}
};
let pending_todos = move || {
submissions
.get()
.into_iter()
.filter(|submission| submission.pending().get())
.map(|submission| {
view! {
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect_view()
};
view! {
<ul>
{existing_todos}
{pending_todos}
</ul>
}
}
}
</ErrorBoundary>
</Transition>
</div>
}
}

View File

@@ -0,0 +1,3 @@
.pending {
color: purple;
}

View File

@@ -165,6 +165,12 @@ pub fn handle_server_fns() -> Route {
/// This version allows you to pass in a closure that adds additional route data to the
/// context, allowing you to pass in info about the route or user from Actix, or other info.
///
/// **NOTE**: If your server functions expect a context, make sure to provide it both in
/// [`handle_server_fns_with_context`] **and** in [`leptos_routes_with_context`] (or whatever
/// rendering method you are using). During SSR, server functions are called by the rendering
/// method, while subsequent calls from the client are handled by the server function handler.
/// The same context needs to be provided to both handlers.
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]

View File

@@ -7,7 +7,7 @@
//! To run in this environment, you need to disable the default feature set and enable
//! the `wasm` feature on `leptos_axum` in your `Cargo.toml`.
//! ```toml
//! leptos_axum = { version = "0.5.1", default-features = false, features = ["wasm"] }
//! leptos_axum = { version = "0.5.3", default-features = false, features = ["wasm"] }
//! ```
//!
//! ## Features
@@ -251,6 +251,12 @@ macro_rules! spawn_task {
/// that takes in the data you'd like. See the [render_app_to_stream_with_context] docs for an example
/// of one that should work much like this one.
///
/// **NOTE**: If your server functions expect a context, make sure to provide it both in
/// [`handle_server_fns_with_context`] **and** in [`leptos_routes_with_context`] (or whatever
/// rendering method you are using). During SSR, server functions are called by the rendering
/// method, while subsequent calls from the client are handled by the server function handler.
/// The same context needs to be provided to both handlers.
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [RequestParts]
@@ -378,7 +384,7 @@ async fn handle_server_fns_inner(
Response::builder().status(StatusCode::BAD_REQUEST).body(
Full::from(format!(
"Could not find a server function at the route \
{fn_name}. \n\nIt's likely that either
{fn_name}. \n\nIt's likely that either
1. The API prefix you specify in the `#[server]` \
macro doesn't match the prefix at which your server \
function handler is mounted, or \n2. You are on a \
@@ -1099,38 +1105,41 @@ where
let full_path = format!("http://leptos.dev{path}");
let (tx, rx) = futures::channel::oneshot::channel();
let local_pool = get_leptos_pool();
local_pool.spawn_pinned(move || {
async move {
let app = {
let full_path = full_path.clone();
let (req, req_parts) = generate_request_and_parts(req).await;
move || {
provide_contexts(full_path, req_parts, req.into(), default_res_options);
app_fn().into_view()
}
};
let (stream, runtime) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|| "".into(),
add_context,
spawn_task!(async move {
let app = {
let full_path = full_path.clone();
let (req, req_parts) =
generate_request_and_parts(req).await;
move || {
provide_contexts(
full_path,
req_parts,
req.into(),
default_res_options,
);
app_fn().into_view()
}
};
// Extract the value of ResponseOptions from here
let res_options =
use_context::<ResponseOptions>().unwrap();
let (stream, runtime) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|| "".into(),
add_context,
);
let html = build_async_response(stream, &options, runtime).await;
// Extract the value of ResponseOptions from here
let res_options = use_context::<ResponseOptions>().unwrap();
let new_res_parts = res_options.0.read().clone();
let html =
build_async_response(stream, &options, runtime).await;
let mut writable = res_options2.0.write();
*writable = new_res_parts;
let new_res_parts = res_options.0.read().clone();
_ = tx.send(html);
}
let mut writable = res_options2.0.write();
*writable = new_res_parts;
_ = tx.send(html);
});
let html = rx.await.expect("to complete HTML rendering");
@@ -1323,6 +1332,29 @@ where
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
}
/// TODO docs
pub async fn build_static_routes<IV>(
options: &LeptosOptions,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
routes: &[RouteListing],
static_data_map: StaticDataMap,
) where
IV: IntoView + 'static,
{
let options = options.clone();
let routes = routes.to_owned();
spawn_task!(async move {
leptos_router::build_static_routes(
&options,
app_fn,
&routes,
&static_data_map,
)
.await
.expect("could not build static routes")
});
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
@@ -1535,8 +1567,7 @@ where
async move {
let (tx, rx) = futures::channel::oneshot::channel();
let local_pool = get_leptos_pool();
local_pool.spawn_pinned(move || async move {
spawn_task!(async move {
let res = incremental_static_route(
tokio::fs::read_to_string(static_file_path(
&options, &path,
@@ -1579,8 +1610,7 @@ where
async move {
let (tx, rx) = futures::channel::oneshot::channel();
let local_pool = get_leptos_pool();
local_pool.spawn_pinned(move || async move {
spawn_task!(async move {
let res = upfront_static_route(
tokio::fs::read_to_string(static_file_path(
&options, &path,
@@ -1805,8 +1835,8 @@ impl ExtractorHelper {
where
S: Sized,
F: Extractor<T, U, S>,
T: std::fmt::Debug + Send + FromRequestParts<S> + 'static,
T::Rejection: std::fmt::Debug + Send + 'static,
T: core::fmt::Debug + Send + FromRequestParts<S> + 'static,
T::Rejection: core::fmt::Debug + Send + 'static,
{
let mut parts = self.parts.lock().await;
let data = T::from_request_parts(&mut parts, &s).await?;
@@ -1849,8 +1879,8 @@ pub async fn extract<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,
T: core::fmt::Debug + Send + FromRequestParts<()> + 'static,
T::Rejection: core::fmt::Debug + Send + 'static,
{
extract_with_state((), f).await
}
@@ -1917,8 +1947,8 @@ pub async fn extract_with_state<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,
T: core::fmt::Debug + Send + FromRequestParts<S> + 'static,
T::Rejection: core::fmt::Debug + Send + 'static,
{
use_context::<ExtractorHelper>()
.expect(

View File

@@ -167,6 +167,12 @@ pub async fn handle_server_fns(req: Request) -> Result<Response> {
/// that takes in the data you'd like. See the [render_app_to_stream_with_context] docs for an example
/// of one that should work much like this one.
///
/// **NOTE**: If your server functions expect a context, make sure to provide it both in
/// [`handle_server_fns_with_context`] **and** in [`leptos_routes_with_context`] (or whatever
/// rendering method you are using). During SSR, server functions are called by the rendering
/// method, while subsequent calls from the client are handled by the server function handler.
/// The same context needs to be provided to both handlers.
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [RequestParts]

View File

@@ -16,12 +16,16 @@ leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
leptos_config = { workspace = true }
tracing = "0.1"
typed-builder = "0.16"
typed-builder-macro = "0.16"
typed-builder = "0.18"
typed-builder-macro = "0.18"
serde = { version = "1", optional = true }
serde_json = { version = "1", optional = true }
server_fn = { workspace = true }
web-sys = { version = "0.3.63", features = ["ShadowRoot", "ShadowRootInit", "ShadowRootMode"] }
web-sys = { version = "0.3.63", features = [
"ShadowRoot",
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = { version = "0.2", optional = true }
[features]

View File

@@ -2,7 +2,8 @@ use crate::Children;
use leptos_dom::{Errors, HydrationCtx, IntoView};
use leptos_macro::{component, view};
use leptos_reactive::{
create_rw_signal, provide_context, signal_prelude::*, RwSignal,
create_rw_signal, provide_context, run_as_child, signal_prelude::*,
RwSignal,
};
/// When you render a `Result<_, _>` in your view, in the `Err` case it will
@@ -15,6 +16,7 @@ use leptos_reactive::{
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # let runtime = create_runtime();
/// # if false {
/// let (value, set_value) = create_signal(Ok(0));
/// let on_input =
/// move |ev| set_value.set(event_target_value(&ev).parse::<i32>());
@@ -28,6 +30,7 @@ use leptos_reactive::{
/// </ErrorBoundary>
/// }
/// # ;
/// # }
/// # runtime.dispose();
/// ```
///
@@ -59,21 +62,22 @@ where
F: Fn(RwSignal<Errors>) -> IV + 'static,
IV: IntoView,
{
let before_children = HydrationCtx::next_error();
run_as_child(move || {
let before_children = HydrationCtx::next_error();
let errors: RwSignal<Errors> = create_rw_signal(Errors::default());
let errors: RwSignal<Errors> = create_rw_signal(Errors::default());
provide_context(errors);
provide_context(errors);
// Run children so that they render and execute resources
_ = HydrationCtx::next_error();
let children = children();
HydrationCtx::continue_from(before_children);
// Run children so that they render and execute resources
_ = HydrationCtx::next_error();
let children = children();
HydrationCtx::continue_from(before_children);
#[cfg(all(debug_assertions, feature = "hydrate"))]
{
use leptos_dom::View;
if children.nodes.iter().any(|child| {
#[cfg(all(debug_assertions, feature = "hydrate"))]
{
use leptos_dom::View;
if children.nodes.iter().any(|child| {
matches!(child, View::Suspense(_, _))
|| matches!(child, View::Component(repr) if repr.name() == "Transition")
}) {
@@ -84,20 +88,21 @@ where
\nview! {{ \
\n <Suspense fallback=todo!()>\n <ErrorBoundary fallback=todo!()>\n {{move || {{ /* etc. */")
}
}
}
let children = children.into_view();
let errors_empty = create_memo(move |_| errors.with(Errors::is_empty));
let children = children.into_view();
let errors_empty = create_memo(move |_| errors.with(Errors::is_empty));
move || {
if errors_empty.get() {
children.clone().into_view()
} else {
view! {
move || {
if errors_empty.get() {
children.clone().into_view()
} else {
view! {
{fallback(errors)}
<leptos-error-boundary style="display: none">{children.clone()}</leptos-error-boundary>
}
.into_view()
}
}
}
})
}

View File

@@ -180,7 +180,7 @@ pub mod error {
pub use leptos_macro::template;
#[cfg(not(all(target_arch = "wasm32", feature = "template_macro")))]
pub use leptos_macro::view as template;
pub use leptos_macro::{component, island, server, slot, view, Params};
pub use leptos_macro::{component, island, server, slice, slot, view, Params};
pub use leptos_reactive::*;
pub use leptos_server::{
self, create_action, create_multi_action, create_server_action,
@@ -192,9 +192,11 @@ mod error_boundary;
pub use error_boundary::*;
mod animated_show;
mod for_loop;
mod provider;
mod show;
pub use animated_show::*;
pub use for_loop::*;
pub use provider::*;
#[cfg(feature = "experimental-islands")]
pub use serde;
#[cfg(feature = "experimental-islands")]
@@ -202,10 +204,8 @@ pub use serde_json;
pub use show::*;
pub use suspense_component::*;
mod suspense_component;
mod text_prop;
mod transition;
pub use text_prop::TextProp;
#[cfg(any(debug_assertions, feature = "ssr"))]
#[doc(hidden)]
pub use tracing;

View File

@@ -29,13 +29,13 @@ pub fn Portal(
) -> impl IntoView {
cfg_if! { if #[cfg(all(target_arch = "wasm32", any(feature = "hydrate", feature = "csr")))] {
use leptos_dom::{document, Mountable};
use leptos_reactive::{create_render_effect, on_cleanup};
use leptos_reactive::{create_effect, on_cleanup};
use wasm_bindgen::JsCast;
let mount = mount
.unwrap_or_else(|| document().body().expect("body to exist").unchecked_into());
create_render_effect(move |_| {
create_effect(move |_| {
let tag = if is_svg { "g" } else { "div" };
let container = document()

40
leptos/src/provider.rs Normal file
View File

@@ -0,0 +1,40 @@
use leptos::*;
#[component]
/// Uses the context API to [`provide_context`] to its children and descendants,
/// without overwriting any contexts of the same type in its own reactive scope.
///
/// This prevents issues related to “context shadowing.”
///
/// ```rust
/// # use leptos::*;
/// #[component]
/// pub fn App() -> impl IntoView {
/// // each Provider will only provide the value to its children
/// view! {
/// <Provider value=1u8>
/// // correctly gets 1 from context
/// {use_context::<u8>().unwrap_or(0)}
/// </Provider>
/// <Provider value=2u8>
/// // correctly gets 2 from context
/// {use_context::<u8>().unwrap_or(0)}
/// </Provider>
/// // does not find any u8 in context
/// {use_context::<u8>().unwrap_or(0)}
/// }
/// }
/// ```
pub fn Provider<T>(
/// The value to be provided via context.
value: T,
children: Children,
) -> impl IntoView
where
T: Clone + 'static,
{
run_as_child(move || {
provide_context(value);
children()
})
}

View File

@@ -1,7 +1,7 @@
use leptos::ViewFn;
use leptos_dom::{DynChild, HydrationCtx, IntoView};
use leptos_macro::component;
#[cfg(feature = "hydrate")]
#[allow(unused)]
use leptos_reactive::SharedContext;
#[cfg(any(feature = "csr", feature = "hydrate"))]
use leptos_reactive::SignalGet;
@@ -9,7 +9,7 @@ use leptos_reactive::{
create_memo, provide_context, SignalGetUntracked, SuspenseContext,
};
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
use leptos_reactive::{with_owner, Owner, SharedContext};
use leptos_reactive::{with_owner, Owner};
use std::rc::Rc;
/// If any [`Resource`](leptos_reactive::Resource) is read in the `children` of this
@@ -70,6 +70,11 @@ pub fn Suspense<V>(
where
V: IntoView + 'static,
{
#[cfg(all(
feature = "experimental-islands",
not(any(feature = "csr", feature = "hydrate"))
))]
let no_hydrate = SharedContext::no_hydrate();
let orig_children = children;
let context = SuspenseContext::new();
@@ -140,7 +145,7 @@ where
{
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
if context.none_pending() {
with_owner(owner, move || {
//HydrationCtx::continue_from(current_id);
DynChild::new(move || children_rendered.clone())
@@ -167,6 +172,14 @@ where
leptos_reactive::set_current_runtime(
runtime,
);
#[cfg(feature = "experimental-islands")]
{
SharedContext::set_no_hydrate(
no_hydrate,
);
}
with_owner(owner, {
move || {
HydrationCtx::continue_from(
@@ -191,6 +204,14 @@ where
leptos_reactive::set_current_runtime(
runtime,
);
#[cfg(feature = "experimental-islands")]
{
SharedContext::set_no_hydrate(
no_hydrate,
);
}
with_owner(owner, {
move || {
HydrationCtx::continue_from(

View File

@@ -1,59 +0,0 @@
use leptos_reactive::Oco;
use std::{fmt::Debug, rc::Rc};
/// Describes a value that is either a static or a reactive string, i.e.,
/// a [`String`], a [`&str`], or a reactive `Fn() -> String`.
#[derive(Clone)]
pub struct TextProp(Rc<dyn Fn() -> Oco<'static, str>>);
impl TextProp {
/// Accesses the current value of the property.
#[inline(always)]
pub fn get(&self) -> Oco<'static, str> {
(self.0)()
}
}
impl Debug for TextProp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("TextProp").finish()
}
}
impl From<String> for TextProp {
fn from(s: String) -> Self {
let s: Oco<'_, str> = Oco::Counted(Rc::from(s));
TextProp(Rc::new(move || s.clone()))
}
}
impl From<&'static str> for TextProp {
fn from(s: &'static str) -> Self {
let s: Oco<'_, str> = s.into();
TextProp(Rc::new(move || s.clone()))
}
}
impl From<Rc<str>> for TextProp {
fn from(s: Rc<str>) -> Self {
let s: Oco<'_, str> = s.into();
TextProp(Rc::new(move || s.clone()))
}
}
impl From<Oco<'static, str>> for TextProp {
fn from(s: Oco<'static, str>) -> Self {
TextProp(Rc::new(move || s.clone()))
}
}
impl<F, S> From<F> for TextProp
where
F: Fn() -> S + 'static,
S: Into<Oco<'static, str>>,
{
#[inline(always)]
fn from(s: F) -> Self {
TextProp(Rc::new(move || s().into()))
}
}

View File

@@ -139,10 +139,9 @@ pub fn Transition(
}
child_runs.set(child_runs.get() + 1);
let pending = suspense_context.pending_resources;
create_isomorphic_effect(move |_| {
if let Some(set_pending) = set_pending {
set_pending.set(pending.get() > 0)
set_pending.set(!suspense_context.none_pending())
}
});
frag

View File

@@ -13,7 +13,7 @@ config = { version = "0.13.3", default-features = false, features = ["toml"] }
regex = "1.7.0"
serde = { version = "1.0.151", features = ["derive"] }
thiserror = "1.0.38"
typed-builder = "0.16"
typed-builder = "0.18"
[dev-dependencies]
tokio = { version = "1", features = ["rt", "macros"] }

View File

@@ -172,7 +172,8 @@ ssr = ["leptos_reactive/ssr"]
nightly = ["leptos_reactive/nightly"]
nonce = ["dep:base64", "dep:getrandom", "dep:rand"]
experimental-islands = ["leptos_reactive/experimental-islands"]
trace-component-props = []
[package.metadata.cargo-all-features]
denylist = ["nightly"]
denylist = ["nightly", "trace-component-props"]
skip_feature_sets = [["web", "ssr"]]

View File

@@ -324,8 +324,8 @@ macro_rules! generate_event_types {
)*
}
impl ::std::fmt::Debug for GenericEventHandler {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
impl ::core::fmt::Debug for GenericEventHandler {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
match self {
$(
Self::[< $($event:camel)+ >](event, _) => f

View File

@@ -458,8 +458,8 @@ where
/// A handle that can be called to remove a global event listener.
pub struct WindowListenerHandle(Box<dyn FnOnce()>);
impl std::fmt::Debug for WindowListenerHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for WindowListenerHandle {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("WindowListenerHandle").finish()
}
}

View File

@@ -84,7 +84,7 @@ pub struct HydrationKey {
}
impl Display for HydrationKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}-{}-{}-{}",

View File

@@ -38,11 +38,11 @@ pub use events::{typed as ev, typed::EventHandler};
pub use html::HtmlElement;
use html::{AnyElement, ElementDescriptor};
pub use hydration::{HydrationCtx, HydrationKey};
use leptos_reactive::Oco;
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
use leptos_reactive::{Oco, TextProp};
pub use macro_helpers::*;
pub use node_ref::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -229,6 +229,12 @@ where
}
}
impl IntoView for TextProp {
fn into_view(self) -> View {
self.get().into_view()
}
}
/// Collects an iterator or collection into a [`View`].
pub trait CollectView {
/// Collects an iterator or collection into a [`View`].
@@ -1173,6 +1179,19 @@ where
}
}
impl IntoView for core::fmt::Arguments<'_> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "info", name = "#text", skip_all)
)]
fn into_view(self) -> View {
match self.as_str() {
Some(s) => s.into_view(),
None => self.to_string().into_view(),
}
}
}
macro_rules! viewable_primitive {
($($child_type:ty),* $(,)?) => {
$(
@@ -1226,7 +1245,6 @@ viewable_primitive![
std::num::NonZeroIsize,
std::num::NonZeroUsize,
std::panic::Location<'_>,
std::fmt::Arguments<'_>,
];
cfg_if! {

View File

@@ -1,8 +1,8 @@
use leptos_reactive::Oco;
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
use leptos_reactive::{Oco, TextProp};
use std::{borrow::Cow, rc::Rc};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::UnwrapThrowExt;
@@ -88,8 +88,8 @@ impl PartialEq for Attribute {
}
}
impl std::fmt::Debug for Attribute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for Attribute {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
Self::Fn(_) => f.debug_tuple("Fn").finish(),
@@ -165,10 +165,10 @@ impl IntoAttribute for &'static str {
impl_into_attr_boxed! {}
}
impl IntoAttribute for Rc<str> {
impl IntoAttribute for Cow<'static, str> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(Oco::Counted(self))
Attribute::String(self.into())
}
impl_into_attr_boxed! {}
@@ -183,6 +183,15 @@ impl IntoAttribute for Oco<'static, str> {
impl_into_attr_boxed! {}
}
impl IntoAttribute for Rc<str> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(Oco::Counted(self))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for bool {
#[inline(always)]
fn into_attribute(self) -> Attribute {
@@ -261,6 +270,25 @@ impl IntoAttribute for Option<Box<dyn IntoAttribute>> {
impl_into_attr_boxed! {}
}
impl IntoAttribute for TextProp {
fn into_attribute(self) -> Attribute {
self.get().into_attribute()
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for core::fmt::Arguments<'_> {
fn into_attribute(self) -> Attribute {
match self.as_str() {
Some(s) => s.into_attribute(),
None => self.to_string().into_attribute(),
}
}
impl_into_attr_boxed! {}
}
/* impl IntoAttribute for Box<dyn IntoAttribute> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
@@ -358,11 +386,14 @@ attr_signal_type_optional!(MaybeProp<T>);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]
#[track_caller]
pub fn attribute_helper(
el: &web_sys::Element,
name: Oco<'static, str>,
value: Attribute,
) {
#[cfg(debug_assertions)]
let called_at = std::panic::Location::caller();
use leptos_reactive::create_render_effect;
match value {
Attribute::Fn(f) => {
@@ -370,12 +401,26 @@ pub fn attribute_helper(
create_render_effect(move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(&el, &name, new.clone(), true);
attribute_expression(
&el,
&name,
new.clone(),
true,
#[cfg(debug_assertions)]
called_at,
);
}
new
});
}
_ => attribute_expression(el, &name, value, false),
_ => attribute_expression(
el,
&name,
value,
false,
#[cfg(debug_assertions)]
called_at,
),
};
}
@@ -386,6 +431,7 @@ pub(crate) fn attribute_expression(
attr_name: &str,
value: Attribute,
force: bool,
#[cfg(debug_assertions)] called_at: &'static std::panic::Location<'static>,
) {
use crate::HydrationCtx;
@@ -422,7 +468,28 @@ pub(crate) fn attribute_expression(
el.remove_attribute(attr_name).unwrap_throw();
}
}
_ => panic!("Remove nested Fn in Attribute"),
Attribute::Fn(f) => {
let mut v = f();
crate::debug_warn!(
"At {called_at}, you are providing a dynamic attribute \
with a nested function. For example, you might have a \
closure that returns another function instead of a \
value. This creates some added overhead. If possible, \
you should instead provide a function that returns a \
value instead.",
);
while let Attribute::Fn(f) = v {
v = f();
}
attribute_expression(
el,
attr_name,
v,
force,
#[cfg(debug_assertions)]
called_at,
);
}
}
}
}

View File

@@ -27,8 +27,8 @@ impl PartialEq for Style {
}
}
impl std::fmt::Debug for Style {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for Style {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Value(arg0) => f.debug_tuple("Value").field(arg0).finish(),
Self::Fn(_) => f.debug_tuple("Fn").finish(),

View File

@@ -2,6 +2,7 @@ mod into_attribute;
mod into_class;
mod into_property;
mod into_style;
#[cfg(feature = "trace-component-props")]
#[doc(hidden)]
pub mod tracing_property;
pub use into_attribute::*;

View File

@@ -54,7 +54,7 @@ impl Deref for Nonce {
}
impl Display for Nonce {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}

View File

@@ -131,8 +131,8 @@ pub struct MacroInvocation {
template: LNode,
}
impl std::fmt::Debug for MacroInvocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for MacroInvocation {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("MacroInvocation")
.field("id", &self.id)
.finish_non_exhaustive()

View File

@@ -30,7 +30,7 @@ tracing = "0.1.37"
[dev-dependencies]
log = "0.4"
typed-builder = "0.16"
typed-builder = "0.18"
trybuild = "1"
leptos = { path = "../leptos" }
insta = "1.29"
@@ -43,7 +43,8 @@ ssr = ["server_fn_macro/ssr"]
nightly = ["server_fn_macro/nightly"]
tracing = []
experimental-islands = []
trace-component-props = []
[package.metadata.cargo-all-features]
denylist = ["nightly", "tracing"]
denylist = ["nightly", "tracing", "trace-component-props"]
skip_feature_sets = [["csr", "hydrate"], ["hydrate", "csr"], ["hydrate", "ssr"]]

View File

@@ -118,8 +118,6 @@ impl ToTokens for Model {
let no_props = props.is_empty();
let mut body = body.to_owned();
// check for components that end ;
if !is_transparent {
let ends_semi =
@@ -139,7 +137,6 @@ impl ToTokens for Model {
}
}
body.sig.ident = format_ident!("__{}", body.sig.ident);
#[allow(clippy::redundant_clone)] // false positive
let body_name = body.sig.ident.clone();
@@ -203,7 +200,7 @@ impl ToTokens for Model {
#[cfg(debug_assertions)]
let _guard = span.entered();
},
if no_props {
if no_props || !cfg!(feature = "trace-component-props") {
quote! {}
} else {
quote! {
@@ -234,6 +231,7 @@ impl ToTokens for Model {
quote! {}
};
let body_name = unmodified_fn_name_from_fn_name(&body_name);
let body_expr = if *is_island {
quote! {
::leptos::SharedContext::with_hydration(move || {
@@ -367,7 +365,6 @@ impl ToTokens for Model {
.collect::<TokenStream>();
let body = quote! {
#body
#destructure_props
#tracing_span_expr
#component
@@ -547,10 +544,10 @@ impl Model {
/// used to improve IDEs and rust-analyzer's auto-completion behavior in case
/// of a syntax error.
pub struct DummyModel {
attrs: Vec<Attribute>,
vis: Visibility,
sig: Signature,
body: TokenStream,
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub sig: Signature,
pub body: TokenStream,
}
impl Parse for DummyModel {
@@ -588,7 +585,21 @@ impl ToTokens for DummyModel {
let mut sig = sig.clone();
sig.inputs.iter_mut().for_each(|arg| {
if let FnArg::Typed(ty) = arg {
ty.attrs.clear();
ty.attrs.retain(|attr| match &attr.meta {
Meta::List(list) => list
.path
.segments
.first()
.map(|n| n.ident != "prop")
.unwrap_or(true),
Meta::NameValue(name_value) => name_value
.path
.segments
.first()
.map(|n| n.ident != "doc")
.unwrap_or(true),
_ => true,
});
}
});
sig
@@ -1162,3 +1173,7 @@ fn is_valid_into_view_return_type(ty: &ReturnType) -> bool {
.iter()
.any(|test| ty == test)
}
pub fn unmodified_fn_name_from_fn_name(ident: &Ident) -> Ident {
Ident::new(&format!("__{ident}"), ident.span())
}

View File

@@ -4,11 +4,12 @@
#[macro_use]
extern crate proc_macro_error;
use component::DummyModel;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenTree};
use quote::ToTokens;
use rstml::{node::KeyedAttribute, parse};
use syn::parse_macro_input;
use syn::{parse_macro_input, spanned::Spanned, token::Pub, Visibility};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum Mode {
@@ -31,6 +32,7 @@ impl Default for Mode {
mod params;
mod view;
use crate::component::unmodified_fn_name_from_fn_name;
use view::{client_template::render_template, render_view};
mod component;
mod server;
@@ -598,21 +600,30 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
false
};
let parse_result = syn::parse::<component::Model>(s.clone());
let mut dummy = syn::parse::<DummyModel>(s.clone());
let parse_result = syn::parse::<component::Model>(s);
if let Ok(model) = parse_result {
model
.is_transparent(is_transparent)
.into_token_stream()
.into()
if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
let expanded = model.is_transparent(is_transparent).into_token_stream();
unexpanded.sig.ident =
unmodified_fn_name_from_fn_name(&unexpanded.sig.ident);
quote! {
#expanded
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
#unexpanded
}
} else if let Ok(mut dummy) = dummy {
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
quote! {
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
#dummy
}
} else {
// When the input syntax is invalid, e.g. while typing, we let
// the dummy model output tokens similar to the input, which improves
// IDEs and rust-analyzer's auto-complete capabilities.
parse_macro_input!(s as component::DummyModel)
.into_token_stream()
.into()
quote! {}
}
.into()
}
/// Defines a component as an interactive island when you are using the
@@ -688,28 +699,36 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// ```
#[proc_macro_error::proc_macro_error]
#[proc_macro_attribute]
pub fn island(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_transparent = if !args.is_empty() {
let transparent = parse_macro_input!(args as syn::Ident);
pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let mut dummy = syn::parse::<DummyModel>(s.clone());
let parse_result = syn::parse::<component::Model>(s);
if transparent != "transparent" {
abort!(
transparent,
"only `transparent` is supported";
help = "try `#[island(transparent)]` or `#[island]`"
);
if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
let expanded = model.is_island().into_token_stream();
if !matches!(unexpanded.vis, Visibility::Public(_)) {
unexpanded.vis = Visibility::Public(Pub {
span: unexpanded.vis.span(),
})
}
unexpanded.sig.ident =
unmodified_fn_name_from_fn_name(&unexpanded.sig.ident);
quote! {
#expanded
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
#unexpanded
}
} else if let Ok(mut dummy) = dummy {
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
quote! {
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
#dummy
}
true
} else {
false
};
parse_macro_input!(s as component::Model)
.is_transparent(is_transparent)
.is_island()
.into_token_stream()
.into()
quote! {}
}
.into()
}
/// Annotates a struct so that it can be used with your Component as a `slot`.
@@ -971,9 +990,9 @@ pub(crate) fn attribute_value(attr: &KeyedAttribute) -> &syn::Expr {
}
}
/// Generates a `lens` into struct with a default getter and setter
/// Generates a `slice` into a struct with a default getter and setter.
///
/// Can be used to access deeply nested fields within a global state object
/// Can be used to access deeply nested fields within a global state object.
///
/// ```rust
/// # use leptos::{create_runtime, create_rw_signal};

View File

@@ -1,59 +1,55 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::quote;
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
punctuated::Punctuated,
token::Dot,
Token, Type,
Token,
};
struct SliceMacroInput {
pub root: Ident,
pub path: Punctuated<Type, Dot>,
root: syn::Ident,
path: Punctuated<syn::Ident, Token![.]>,
}
impl Parse for SliceMacroInput {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let root = syn::Ident::parse(input)?;
let _dot = <Token![.]>::parse(input)?;
let path = input.parse_terminated(Type::parse, Token![.])?;
fn parse(input: ParseStream) -> syn::Result<Self> {
let root: syn::Ident = input.parse()?;
input.parse::<Token![.]>()?;
// do not accept trailing punctuation
let path: Punctuated<syn::Ident, Token![.]> =
Punctuated::parse_separated_nonempty(input)?;
if path.is_empty() {
return Err(syn::Error::new(input.span(), "Expected identifier"));
return Err(input.error("expected identifier"));
}
if path.trailing_punct() {
return Err(syn::Error::new(
input.span(),
"Unexpected trailing `.`",
));
if !input.is_empty() {
return Err(input.error("unexpected token"));
}
Ok(SliceMacroInput { root, path })
Ok(Self { root, path })
}
}
impl From<SliceMacroInput> for TokenStream {
fn from(val: SliceMacroInput) -> Self {
let root = val.root;
let path = val.path;
impl ToTokens for SliceMacroInput {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let root = &self.root;
let path = &self.path;
quote! {
tokens.extend(quote! {
::leptos::create_slice(
#root,
|st: &_| st.#path.clone(),
|st: &mut _, n| st.#path = n
)
}
.into()
})
}
}
pub fn slice_impl(tokens: TokenStream) -> TokenStream {
let input = parse_macro_input!(tokens as SliceMacroInput);
input.into()
input.into_token_stream().into()
}

View File

@@ -14,7 +14,7 @@ error: expected `.`
|
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
error: Expected identifier
error: unexpected end of input, expected identifier
--> tests/slice/red.rs:25:18
|
25 | let (_, _) = slice!(outer_signal.);
@@ -22,7 +22,7 @@ error: Expected identifier
|
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
error: Unexpected trailing `.`
error: unexpected end of input, expected identifier
--> tests/slice/red.rs:27:18
|
27 | let (_, _) = slice!(outer_signal.inner.);

View File

@@ -13,7 +13,6 @@ fn unknown_prop_option(#[prop(hello)] test: bool) -> impl IntoView {
#[component]
fn optional_and_optional_no_strip(
,
#[prop(optional, optional_no_strip)] conflicting: bool,
) -> impl IntoView {
_ = conflicting;
@@ -21,7 +20,6 @@ fn optional_and_optional_no_strip(
#[component]
fn optional_and_strip_option(
,
#[prop(optional, strip_option)] conflicting: bool,
) -> impl IntoView {
_ = conflicting;
@@ -29,23 +27,18 @@ fn optional_and_strip_option(
#[component]
fn optional_no_strip_and_strip_option(
,
#[prop(optional_no_strip, strip_option)] conflicting: bool,
) -> impl IntoView {
_ = conflicting;
}
#[component]
fn default_without_value(
,
#[prop(default)] default: bool,
) -> impl IntoView {
fn default_without_value(#[prop(default)] default: bool) -> impl IntoView {
_ = default;
}
#[component]
fn default_with_invalid_value(
,
#[prop(default= |)] default: bool,
) -> impl IntoView {
_ = default;

View File

@@ -1,33 +1,3 @@
error: expected parameter name, found `,`
--> tests/ui/component.rs:16:5
|
16 | ,
| ^ expected parameter name
error: expected parameter name, found `,`
--> tests/ui/component.rs:24:5
|
24 | ,
| ^ expected parameter name
error: expected parameter name, found `,`
--> tests/ui/component.rs:32:5
|
32 | ,
| ^ expected parameter name
error: expected parameter name, found `,`
--> tests/ui/component.rs:40:5
|
40 | ,
| ^ expected parameter name
error: expected parameter name, found `,`
--> tests/ui/component.rs:48:5
|
48 | ,
| ^ expected parameter name
error: return type is incorrect
--> tests/ui/component.rs:4:1
|
@@ -50,32 +20,34 @@ error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `de
10 | fn unknown_prop_option(#[prop(hello)] test: bool) -> impl IntoView {
| ^^^^^
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
--> tests/ui/component.rs:16:5
error: `optional` conflicts with mutually exclusive `optional_no_strip`
--> tests/ui/component.rs:16:12
|
16 | ,
| ^
16 | #[prop(optional, optional_no_strip)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
--> tests/ui/component.rs:24:5
error: `optional` conflicts with mutually exclusive `strip_option`
--> tests/ui/component.rs:23:12
|
24 | ,
| ^
23 | #[prop(optional, strip_option)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
--> tests/ui/component.rs:32:5
error: `optional_no_strip` conflicts with mutually exclusive `strip_option`
--> tests/ui/component.rs:30:12
|
32 | ,
| ^
30 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
--> tests/ui/component.rs:40:5
error: unexpected end of input, expected assignment `=`
--> tests/ui/component.rs:36:40
|
40 | ,
| ^
36 | fn default_without_value(#[prop(default)] default: bool) -> impl IntoView {
| ^
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
--> tests/ui/component.rs:48:5
error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
= help: try `#[prop(default=5 * 10)]`
--> tests/ui/component.rs:42:22
|
48 | ,
| ^
42 | #[prop(default= |)] default: bool,
| ^

View File

@@ -28,7 +28,9 @@ serde-wasm-bindgen = "0.5"
serde_json = "1"
base64 = "0.21"
thiserror = "1"
tokio = { version = "1", features = ["rt"], optional = true, default-features = false }
tokio = { version = "1", features = [
"rt",
], optional = true, default-features = false }
tracing = "0.1"
wasm-bindgen = { version = "0.2", optional = true }
wasm-bindgen-futures = { version = "0.4", optional = true }
@@ -46,10 +48,6 @@ pin-project = "1"
paste = "1"
[dev-dependencies]
criterion = { version = "0.5.1", features = ["html_reports"] }
reactive-signals = { version = "0.1.0-alpha.4", features = ["profile"] }
l021 = { package = "leptos", version = "0.2.1" }
sycamore = { version = "0.8", features = ["ssr"] }
log = "0.4"
tokio-test = "0.4"
leptos = { path = "../leptos" }
@@ -119,15 +117,3 @@ skip_feature_sets = [
"rkyv",
],
]
[[bench]]
name = "deep_update"
harness = false
[[bench]]
name = "fan_out"
harness = false
[[bench]]
name = "narrow_down"
harness = false

View File

@@ -1,110 +0,0 @@
use criterion::{criterion_group, criterion_main, Criterion};
fn rs_deep_update(c: &mut Criterion) {
use reactive_signals::{
runtimes::ClientRuntime, signal, types::Func, Signal,
};
c.bench_function("rs_deep_update", |b| {
b.iter(|| {
let sc = ClientRuntime::bench_root_scope();
let signal = signal!(sc, 0);
let mut memos = Vec::<Signal<Func<i32>, ClientRuntime>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(signal!(sc, move || prev.get() + 1))
} else {
memos.push(signal!(sc, move || signal.get() + 1))
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
});
});
}
fn l021_deep_update(c: &mut Criterion) {
use l021::*;
c.bench_function("l021_deep_update", |b| {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
})
.dispose()
});
runtime.dispose();
});
}
fn sycamore_deep_update(c: &mut Criterion) {
use sycamore::reactive::*;
c.bench_function("sycamore_deep_update", |b| {
b.iter(|| {
let d = create_scope(|cx| {
let signal = create_signal(cx, 0);
let mut memos = Vec::<&ReadSignal<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move || *prev.get() + 1));
} else {
memos.push(create_memo(cx, move || *signal.get() + 1));
}
}
signal.set(1);
assert_eq!(*memos[999].get(), 1001);
});
unsafe { d.dispose() };
});
});
}
fn leptos_deep_update(c: &mut Criterion) {
use leptos::*;
let runtime = create_runtime();
c.bench_function("leptos_deep_update", |b| {
b.iter(|| {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
})
.dispose()
});
});
runtime.dispose();
}
criterion_group!(
deep,
rs_deep_update,
l021_deep_update,
sycamore_deep_update,
leptos_deep_update
);
criterion_main!(deep);

View File

@@ -1,91 +0,0 @@
use criterion::{criterion_group, criterion_main, Criterion};
fn rs_fan_out(c: &mut Criterion) {
use reactive_signals::{runtimes::ClientRuntime, signal};
c.bench_function("rs_fan_out", |b| {
b.iter(|| {
let cx = ClientRuntime::bench_root_scope();
let sig = signal!(cx, 0);
let memos = (0..1000)
.map(|_| signal!(cx, move || sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
});
});
}
fn l021_fan_out(c: &mut Criterion) {
use l021::*;
c.bench_function("l021_fan_out", |b| {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let sig = create_rw_signal(cx, 0);
let memos = (0..1000)
.map(|_| create_memo(cx, move |_| sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
})
.dispose()
});
runtime.dispose();
});
}
fn sycamore_fan_out(c: &mut Criterion) {
use sycamore::reactive::*;
c.bench_function("sycamore_fan_out", |b| {
b.iter(|| {
let d = create_scope(|cx| {
let sig = create_signal(cx, 0);
let memos = (0..1000)
.map(|_| create_memo(cx, move || sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 0);
sig.set(1);
assert_eq!(
memos.iter().map(|m| *(*m.get())).sum::<i32>(),
1000
);
});
unsafe { d.dispose() };
});
});
}
fn leptos_fan_out(c: &mut Criterion) {
use leptos_reactive::*;
let runtime = create_runtime();
c.bench_function("leptos_fan_out", |b| {
b.iter(|| {
create_scope(runtime, |cx| {
let sig = create_rw_signal(cx, 0);
let memos = (0..1000)
.map(|_| create_memo(cx, move |_| sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
})
.dispose()
});
});
runtime.dispose();
}
criterion_group!(
fan_out,
rs_fan_out,
l021_fan_out,
sycamore_fan_out,
leptos_fan_out
);
criterion_main!(fan_out);

View File

@@ -1,90 +0,0 @@
use criterion::{criterion_group, criterion_main, Criterion};
use std::{cell::Cell, rc::Rc};
fn rs_narrow_down(c: &mut Criterion) {
use reactive_signals::{runtimes::ClientRuntime, signal};
c.bench_function("rs_narrow_down", |b| {
b.iter(|| {
let cx = ClientRuntime::bench_root_scope();
let sigs =
Rc::new((0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>());
let memo = signal!(cx, {
let sigs = Rc::clone(&sigs);
move || sigs.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo.get(), 499500);
});
});
}
fn l021_narrow_down(c: &mut Criterion) {
use l021::*;
c.bench_function("l021_narrow_down", |b| {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
})
.dispose()
});
runtime.dispose();
});
}
fn sycamore_narrow_down(c: &mut Criterion) {
use sycamore::reactive::*;
c.bench_function("sycamore_narrow_down", |b| {
b.iter(|| {
let d = create_scope(|cx| {
let sigs = Rc::new(
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
);
let memo = create_memo(cx, {
let sigs = Rc::clone(&sigs);
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
});
assert_eq!(*memo.get(), 499500);
});
unsafe { d.dispose() };
});
});
}
fn leptos_narrow_down(c: &mut Criterion) {
use leptos_reactive::*;
let runtime = create_runtime();
c.bench_function("leptos_narrow_down", |b| {
b.iter(|| {
create_scope(runtime, |cx| {
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
})
.dispose()
});
});
runtime.dispose();
}
criterion_group!(
narrow_down,
rs_narrow_down,
l021_narrow_down,
sycamore_narrow_down,
leptos_narrow_down
);
criterion_main!(narrow_down);

View File

@@ -9,8 +9,8 @@ use std::any::{Any, TypeId};
/// arguments to a function or properties of a component.
///
/// Context works similarly to variable scope: a context that is provided higher in
/// the component tree can be used lower down, but a context that is provided lower
/// in the tree cannot be used higher up.
/// the reactive graph can be used lower down, but a context that is provided lower
/// down cannot be used higher up.
///
/// ```
/// use leptos::*;
@@ -43,6 +43,117 @@ use std::any::{Any, TypeId};
/// let set_value = use_context::<ValueSetter>().unwrap().0;
/// }
/// ```
///
/// ## Warning: Shadowing Context Correctly
///
/// The reactive graph exists alongside the component tree. Generally
/// speaking, context provided by a parent component can be accessed by its children
/// and other descendants, and not vice versa. But components do not exist at
/// runtime: a parent and children that are all rendered unconditionally exist in the same
/// reactive scope.
///
/// This can have unexpected effects on context: namely, children can sometimes override
/// contexts provided by their parents, including for their siblings, if they “shadow” context
/// by providing another context of the same kind.
/// ```rust
/// use leptos::*;
///
/// #[component]
/// fn Parent() -> impl IntoView {
/// provide_context("parent_context");
/// view! {
/// <Child /> // this is receiving "parent_context" as expected
/// <Child /> // but this is receiving "child_context" instead of "parent_context"!
/// }
/// }
///
/// #[component]
/// fn Child() -> impl IntoView {
/// // first, we receive context from parent (just before the override)
/// let context = expect_context::<&'static str>();
/// // then we provide context under the same type
/// provide_context("child_context");
/// view! {
/// <div>{format!("child (context: {context})")}</div>
/// }
/// }
/// ```
/// In this case, neither of the children is rendered dynamically, so there is no wrapping
/// effect created around either. All three components here have the same reactive owner, so
/// providing a new context of the same type in the first `<Child/>` overrides the context
/// that was provided in `<Parent/>`, meaning that the second `<Child/>` receives the context
/// from its sibling instead.
///
/// ### Solution
///
/// If you are using the full Leptos framework, you can use the [`Provider`](leptos::Provider)
/// component to solve this issue.
///
/// ```rust
/// # use leptos::*;
/// #[component]
/// fn Child() -> impl IntoView {
/// let context = expect_context::<&'static str>();
/// // creates a new reactive node, which means the context will
/// // only be provided to its children, not modified in the parent
/// view! {
/// <Provider value="child_context">
/// <div>{format!("child (context: {context})")}</div>
/// </Provider>
/// }
/// }
/// ```
///
/// ### Alternate Solution
///
/// This can also be solved by introducing some additional reactivity. In this case, its simplest
/// to simply make the body of `<Child/>` a function, which means it will be wrapped in a
/// new reactive node when rendered:
/// ```rust
/// # use leptos::*;
/// #[component]
/// fn Child() -> impl IntoView {
/// let context = expect_context::<&'static str>();
/// // creates a new reactive node, which means the context will
/// // only be provided to its children, not modified in the parent
/// move || {
/// provide_context("child_context");
/// view! {
/// <div>{format!("child (context: {context})")}</div>
/// }
/// }
/// }
/// ```
///
/// This is equivalent to the difference between two different forms of variable shadowing
/// in ordinary Rust:
/// ```rust
/// // shadowing in a flat hierarchy overrides value for siblings
/// // <Parent/>: declares variable
/// let context = "parent_context";
/// // First <Child/>: consumes variable, then shadows
/// println!("{context:?}");
/// let context = "child_context";
/// // Second <Child/>: consumes variable, then shadows
/// println!("{context:?}");
/// let context = "child_context";
///
/// // but shadowing in nested scopes works as expected
/// // <Parent/>
/// let context = "parent_context";
///
/// // First <Child/>
/// {
/// println!("{context:?}");
/// let context = "child_context";
/// }
///
/// // Second <Child/>
/// {
/// println!("{context:?}");
/// let context = "child_context";
/// }
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "debug", skip_all,)
@@ -82,7 +193,7 @@ where
/// arguments to a function or properties of a component.
///
/// Context works similarly to variable scope: a context that is provided higher in
/// the component tree can be used lower down, but a context that is provided lower
/// the reactive graph can be used lower down, but a context that is provided lower
/// in the tree cannot be used higher up.
///
/// ```
@@ -154,7 +265,7 @@ where
/// arguments to a function or properties of a component.
///
/// Context works similarly to variable scope: a context that is provided higher in
/// the component tree can be used lower down, but a context that is provided lower
/// the reactive graph can be used lower down, but a context that is provided lower
/// in the tree cannot be used higher up.
///
/// ```

View File

@@ -2,7 +2,7 @@
use crate::Owner;
use crate::{
runtime::PinnedFuture, suspense::StreamChunk, with_runtime, ResourceId,
SuspenseContext,
SignalGet, SuspenseContext,
};
use futures::stream::FuturesUnordered;
#[cfg(feature = "experimental-islands")]
@@ -84,9 +84,9 @@ impl SharedContext {
let pending = context
.pending_serializable_resources
.read_only()
.try_with(|n| *n)
.unwrap_or(0);
if pending == 0 {
.try_get()
.unwrap_or_default();
if pending.is_empty() {
_ = tx1.unbounded_send(());
_ = tx2.unbounded_send(());
_ = tx3.unbounded_send(());
@@ -242,8 +242,8 @@ pub struct FragmentData {
pub local_only: bool,
}
impl std::fmt::Debug for SharedContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for SharedContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SharedContext").finish()
}
}

Some files were not shown because too many files have changed in this diff Show More