mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 18:22:34 -05:00
Compare commits
41 Commits
v0.5.2
...
context-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7cfe788b2 | ||
|
|
414f5fc393 | ||
|
|
362e3bc603 | ||
|
|
4d549f70c9 | ||
|
|
85dd726d43 | ||
|
|
24febe11f3 | ||
|
|
64b1e9bed3 | ||
|
|
68c91a732d | ||
|
|
8573f22d96 | ||
|
|
61c7ff4256 | ||
|
|
860d887931 | ||
|
|
5e929a75fa | ||
|
|
d82cf0b76a | ||
|
|
cb7e07496a | ||
|
|
17881c5c6e | ||
|
|
2e816b26aa | ||
|
|
68d67c9e92 | ||
|
|
0dea6fdcea | ||
|
|
530dcff86a | ||
|
|
9d9a4932b3 | ||
|
|
bfb67d45e8 | ||
|
|
b1e8105442 | ||
|
|
7aced17976 | ||
|
|
191b40b2ac | ||
|
|
15ca5bec61 | ||
|
|
ba4d226004 | ||
|
|
3adfd334df | ||
|
|
d7ca5f2e96 | ||
|
|
67bdb3498f | ||
|
|
9e9386b223 | ||
|
|
4029de2d42 | ||
|
|
777095670e | ||
|
|
a11c6303e2 | ||
|
|
3394e316b7 | ||
|
|
4b0437394c | ||
|
|
d10a566e48 | ||
|
|
e0cca3e7a3 | ||
|
|
0c8ab7c725 | ||
|
|
a2bef05a4b | ||
|
|
6361985fb1 | ||
|
|
ad290f5ed2 |
@@ -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"
|
||||
|
||||
2
.github/workflows/get-example-changed.yml
vendored
2
.github/workflows/get-example-changed.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/get-leptos-changed.yml
vendored
2
.github/workflows/get-leptos-changed.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/run-cargo-make-task.yml
vendored
2
.github/workflows/run-cargo-make-task.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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'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'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>"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -12,3 +12,4 @@ mdbook serve
|
||||
```
|
||||
|
||||
It should be available at `http://localhost:3000`.
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# `<Transition/>`
|
||||
|
||||
You’ll 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, there’s [`<Transition/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html).
|
||||
You’ll 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, there’s [`<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.
|
||||
|
||||
|
||||
@@ -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 it’s possible for bugs to surface at this point. (If your app behaves differently or you do encounter a bug, it’s 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. I’ll 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
|
||||
@@ -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 there’s 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, Rust’s 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 it’s 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 experience—nobody wants to click a button three times and have it do nothing because the interactive code is still loading—but 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 loading — but it's not the only important measure.
|
||||
|
||||
It’s 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 it’s just an honest trade-off between the two approaches!
|
||||
|
||||
@@ -28,7 +28,7 @@ There’s 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:
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -158,4 +158,4 @@ In particular, you’ll 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).)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
@@ -1,4 +1,4 @@
|
||||
pub mod credentials;
|
||||
pub mod navbar;
|
||||
|
||||
pub use self::{credentials::*, navbar::*};
|
||||
pub use self::navbar::*;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
40
leptos/src/provider.rs
Normal 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()
|
||||
})
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
"{}-{}-{}-{}",
|
||||
|
||||
@@ -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! {
|
||||
|
||||
@@ -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 {
|
||||
@@ -422,7 +450,13 @@ 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();
|
||||
while let Attribute::Fn(f) = v {
|
||||
v = f();
|
||||
}
|
||||
attribute_expression(el, attr_name, v, force);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -971,9 +971,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};
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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.);
|
||||
|
||||
@@ -48,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" }
|
||||
@@ -121,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
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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, it’s 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.
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ where
|
||||
// client
|
||||
create_render_effect({
|
||||
let r = Rc::clone(&r);
|
||||
move |_| r.load(false)
|
||||
move |_| r.load(false, id)
|
||||
});
|
||||
|
||||
Resource {
|
||||
@@ -402,10 +402,9 @@ where
|
||||
S: PartialEq + Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
_ = id;
|
||||
SUPPRESS_RESOURCE_LOAD.with(|s| {
|
||||
if !s.get() {
|
||||
r.load(false)
|
||||
r.load(false, id)
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -483,7 +482,7 @@ where
|
||||
} else {
|
||||
// Server didn't mark the resource as pending, so load it on the
|
||||
// client
|
||||
r.load(false);
|
||||
r.load(false, id);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -528,7 +527,7 @@ where
|
||||
let location = std::panic::Location::caller();
|
||||
with_runtime(|runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.with(f, location)
|
||||
resource.with(f, location, self.id)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
@@ -598,7 +597,7 @@ where
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
#[cfg(debug_assertions)]
|
||||
let prev = SpecialNonReactiveZone::enter();
|
||||
resource.refetch();
|
||||
resource.refetch(self.id);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
SpecialNonReactiveZone::exit(prev);
|
||||
@@ -708,9 +707,10 @@ impl<S, T> SignalUpdate for Resource<S, T> {
|
||||
for suspense_context in
|
||||
resource.suspense_contexts.borrow().iter()
|
||||
{
|
||||
suspense_context.decrement(
|
||||
suspense_context.decrement_for_resource(
|
||||
resource.serializable
|
||||
!= ResourceSerialization::Local,
|
||||
self.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -748,7 +748,7 @@ where
|
||||
let location = std::panic::Location::caller();
|
||||
match with_runtime(|runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.with_maybe(f, location)
|
||||
resource.with_maybe(f, location, self.id)
|
||||
})
|
||||
})
|
||||
.expect("runtime to be alive")
|
||||
@@ -783,7 +783,7 @@ where
|
||||
let location = std::panic::Location::caller();
|
||||
with_runtime(|runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.with_maybe(f, location)
|
||||
resource.with_maybe(f, location, self.id)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
@@ -835,7 +835,7 @@ where
|
||||
let location = std::panic::Location::caller();
|
||||
with_runtime(|runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.read(location)
|
||||
resource.read(location, self.id)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
@@ -1155,11 +1155,15 @@ where
|
||||
T: 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn read(&self, location: &'static Location<'static>) -> Option<T>
|
||||
pub fn read(
|
||||
&self,
|
||||
location: &'static Location<'static>,
|
||||
id: ResourceId,
|
||||
) -> Option<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.with(T::clone, location)
|
||||
self.with(T::clone, location, id)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -1167,6 +1171,7 @@ where
|
||||
&self,
|
||||
f: impl FnOnce(&T) -> U,
|
||||
location: &'static Location<'static>,
|
||||
id: ResourceId,
|
||||
) -> Option<U> {
|
||||
let global_suspense_cx = use_context::<GlobalSuspenseContext>();
|
||||
let suspense_cx = use_context::<SuspenseContext>();
|
||||
@@ -1177,7 +1182,14 @@ where
|
||||
.ok()?
|
||||
.flatten();
|
||||
|
||||
self.handle_result(location, global_suspense_cx, suspense_cx, v, false)
|
||||
self.handle_result(
|
||||
location,
|
||||
global_suspense_cx,
|
||||
suspense_cx,
|
||||
v,
|
||||
false,
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -1185,6 +1197,7 @@ where
|
||||
&self,
|
||||
f: impl FnOnce(&Option<T>) -> U,
|
||||
location: &'static Location<'static>,
|
||||
id: ResourceId,
|
||||
) -> Option<U> {
|
||||
let global_suspense_cx = use_context::<GlobalSuspenseContext>();
|
||||
let suspense_cx = use_context::<SuspenseContext>();
|
||||
@@ -1197,6 +1210,7 @@ where
|
||||
suspense_cx,
|
||||
Some(v),
|
||||
!was_loaded,
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1207,6 +1221,7 @@ where
|
||||
suspense_cx: Option<SuspenseContext>,
|
||||
v: Option<U>,
|
||||
force_suspend: bool,
|
||||
id: ResourceId,
|
||||
) -> Option<U> {
|
||||
let suspense_contexts = self.suspense_contexts.clone();
|
||||
let has_value = v.is_some();
|
||||
@@ -1276,8 +1291,9 @@ where
|
||||
// because the context has been tracked here
|
||||
// on the first read, resource is already loading without having incremented
|
||||
if !has_value || force_suspend {
|
||||
s.increment(
|
||||
s.increment_for_resource(
|
||||
serializable != ResourceSerialization::Local,
|
||||
id,
|
||||
);
|
||||
if serializable == ResourceSerialization::Blocking {
|
||||
s.should_block.set_value(true);
|
||||
@@ -1295,9 +1311,10 @@ where
|
||||
contexts.insert(*s);
|
||||
|
||||
if !has_value || force_suspend {
|
||||
s.increment(
|
||||
s.increment_for_resource(
|
||||
serializable
|
||||
!= ResourceSerialization::Local,
|
||||
id,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1314,15 +1331,15 @@ where
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn refetch(&self) {
|
||||
self.load(true);
|
||||
pub fn refetch(&self, id: ResourceId) {
|
||||
self.load(true, id);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn load(&self, refetching: bool) {
|
||||
fn load(&self, refetching: bool, id: ResourceId) {
|
||||
// doesn't refetch if already refetching
|
||||
if refetching && self.scheduled.get() {
|
||||
return;
|
||||
@@ -1359,8 +1376,9 @@ where
|
||||
let suspense_contexts = self.suspense_contexts.clone();
|
||||
|
||||
for suspense_context in suspense_contexts.borrow().iter() {
|
||||
suspense_context.increment(
|
||||
suspense_context.increment_for_resource(
|
||||
self.serializable != ResourceSerialization::Local,
|
||||
id,
|
||||
);
|
||||
if self.serializable == ResourceSerialization::Blocking {
|
||||
suspense_context.should_block.set_value(true);
|
||||
@@ -1384,8 +1402,9 @@ where
|
||||
}
|
||||
|
||||
for suspense_context in suspense_contexts.borrow().iter() {
|
||||
suspense_context.decrement(
|
||||
suspense_context.decrement_for_resource(
|
||||
serializable != ResourceSerialization::Local,
|
||||
id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,7 +675,7 @@ impl Runtime {
|
||||
}
|
||||
|
||||
impl Debug for Runtime {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("Runtime").finish()
|
||||
}
|
||||
}
|
||||
@@ -751,7 +751,7 @@ pub struct RuntimeId;
|
||||
/// ## Panics
|
||||
/// Panics if there is no current reactive runtime.
|
||||
pub fn as_child_of_current_owner<T, U>(
|
||||
f: impl Fn(T) -> U + 'static,
|
||||
f: impl Fn(T) -> U,
|
||||
) -> impl Fn(T) -> (U, Disposer)
|
||||
where
|
||||
T: 'static,
|
||||
|
||||
@@ -111,11 +111,11 @@ where
|
||||
f: Rc<dyn Fn(&T, &T) -> bool>,
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for Selector<T>
|
||||
impl<T> core::fmt::Debug for Selector<T>
|
||||
where
|
||||
T: PartialEq + Eq + Clone + Hash + 'static,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("Selector").finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::{
|
||||
create_isomorphic_effect, on_cleanup, runtime::untrack, store_value, Memo,
|
||||
ReadSignal, RwSignal, SignalDispose, SignalGet, SignalGetUntracked,
|
||||
Oco, ReadSignal, RwSignal, SignalDispose, SignalGet, SignalGetUntracked,
|
||||
SignalStream, SignalWith, SignalWithUntracked, StoredValue,
|
||||
};
|
||||
use std::{fmt::Debug, rc::Rc};
|
||||
|
||||
/// Helper trait for converting `Fn() -> T` closures into
|
||||
/// [`Signal<T>`].
|
||||
@@ -90,8 +91,8 @@ impl<T> Clone for Signal<T> {
|
||||
|
||||
impl<T> Copy for Signal<T> {}
|
||||
|
||||
impl<T> std::fmt::Debug for Signal<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl<T> core::fmt::Debug for Signal<T> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let mut s = f.debug_struct("Signal");
|
||||
s.field("inner", &self.inner);
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
@@ -457,8 +458,8 @@ impl<T> Clone for SignalTypes<T> {
|
||||
|
||||
impl<T> Copy for SignalTypes<T> {}
|
||||
|
||||
impl<T> std::fmt::Debug for SignalTypes<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl<T> core::fmt::Debug for SignalTypes<T> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::ReadSignal(arg0) => {
|
||||
f.debug_tuple("ReadSignal").field(arg0).finish()
|
||||
@@ -1297,3 +1298,75 @@ impl<T: Clone> Fn<()> for MaybeProp<T> {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 core::fmt::Formatter<'_>) -> core::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<T> From<T> for MaybeProp<TextProp>
|
||||
where
|
||||
T: Into<Oco<'static, str>>,
|
||||
{
|
||||
fn from(s: T) -> Self {
|
||||
Self(Some(MaybeSignal::from(Some(s.into().into()))))
|
||||
}
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TextProp {
|
||||
fn default() -> Self {
|
||||
Self(Rc::new(|| Oco::Borrowed("")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,11 +234,11 @@ impl<T> Clone for SignalSetterTypes<T> {
|
||||
|
||||
impl<T> Copy for SignalSetterTypes<T> {}
|
||||
|
||||
impl<T> std::fmt::Debug for SignalSetterTypes<T>
|
||||
impl<T> core::fmt::Debug for SignalSetterTypes<T>
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
T: core::fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Write(arg0) => {
|
||||
f.debug_tuple("WriteSignal").field(arg0).finish()
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//! Types that handle asynchronous data loading via `<Suspense/>`.
|
||||
|
||||
use crate::{
|
||||
create_isomorphic_effect, create_memo, create_rw_signal, create_signal,
|
||||
oco::Oco, queue_microtask, signal::SignalGet, store_value, Memo,
|
||||
ReadSignal, RwSignal, SignalSet, SignalUpdate, StoredValue, WriteSignal,
|
||||
batch, create_isomorphic_effect, create_memo, create_rw_signal,
|
||||
create_signal, oco::Oco, queue_microtask, store_value, Memo, ReadSignal,
|
||||
ResourceId, RwSignal, SignalSet, SignalUpdate, SignalWith, StoredValue,
|
||||
WriteSignal,
|
||||
};
|
||||
use futures::Future;
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::{cell::RefCell, collections::VecDeque, pin::Pin, rc::Rc};
|
||||
|
||||
/// Tracks [`Resource`](crate::Resource)s that are read under a suspense context,
|
||||
@@ -15,7 +17,11 @@ pub struct SuspenseContext {
|
||||
/// The number of resources that are currently pending.
|
||||
pub pending_resources: ReadSignal<usize>,
|
||||
set_pending_resources: WriteSignal<usize>,
|
||||
pub(crate) pending_serializable_resources: RwSignal<usize>,
|
||||
// NOTE: For correctness reasons, we really need to move to this
|
||||
// However, for API stability reasons, I need to keep the counter-incrementing version too
|
||||
pub(crate) pending: RwSignal<FxHashSet<ResourceId>>,
|
||||
pub(crate) pending_serializable_resources: RwSignal<FxHashSet<ResourceId>>,
|
||||
pub(crate) pending_serializable_resources_count: RwSignal<usize>,
|
||||
pub(crate) local_status: StoredValue<Option<LocalStatus>>,
|
||||
pub(crate) should_block: StoredValue<bool>,
|
||||
}
|
||||
@@ -82,12 +88,12 @@ impl SuspenseContext {
|
||||
pub fn to_future(&self) -> impl Future<Output = ()> {
|
||||
use futures::StreamExt;
|
||||
|
||||
let pending_resources = self.pending_resources;
|
||||
let pending = self.pending;
|
||||
let (tx, mut rx) = futures::channel::mpsc::channel(1);
|
||||
let tx = RefCell::new(tx);
|
||||
queue_microtask(move || {
|
||||
create_isomorphic_effect(move |_| {
|
||||
if pending_resources.get() == 0 {
|
||||
if pending.with(|p| p.is_empty()) {
|
||||
_ = tx.borrow_mut().try_send(());
|
||||
}
|
||||
});
|
||||
@@ -96,17 +102,22 @@ impl SuspenseContext {
|
||||
rx.next().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reactively checks whether there are no pending resources in the suspense.
|
||||
pub fn none_pending(&self) -> bool {
|
||||
self.pending.with(|p| p.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for SuspenseContext {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.pending_resources.id.hash(state);
|
||||
self.pending.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for SuspenseContext {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.pending_resources.id == other.pending_resources.id
|
||||
self.pending.id == other.pending.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,14 +126,19 @@ impl Eq for SuspenseContext {}
|
||||
impl SuspenseContext {
|
||||
/// Creates an empty suspense context.
|
||||
pub fn new() -> Self {
|
||||
let (pending_resources, set_pending_resources) = create_signal(0);
|
||||
let pending_serializable_resources = create_rw_signal(0);
|
||||
let (pending_resources, set_pending_resources) = create_signal(0); // can be removed when possible
|
||||
let pending_serializable_resources =
|
||||
create_rw_signal(Default::default());
|
||||
let pending_serializable_resources_count = create_rw_signal(0); // can be removed when possible
|
||||
let local_status = store_value(None);
|
||||
let should_block = store_value(false);
|
||||
let pending = create_rw_signal(Default::default());
|
||||
Self {
|
||||
pending,
|
||||
pending_resources,
|
||||
set_pending_resources,
|
||||
pending_serializable_resources,
|
||||
pending_serializable_resources_count,
|
||||
local_status,
|
||||
should_block,
|
||||
}
|
||||
@@ -131,7 +147,7 @@ impl SuspenseContext {
|
||||
/// Notifies the suspense context that a new resource is now pending.
|
||||
pub fn increment(&self, serializable: bool) {
|
||||
let setter = self.set_pending_resources;
|
||||
let serializable_resources = self.pending_serializable_resources;
|
||||
let serializable_resources = self.pending_serializable_resources_count;
|
||||
let local_status = self.local_status;
|
||||
setter.update(|n| *n += 1);
|
||||
if serializable {
|
||||
@@ -161,7 +177,7 @@ impl SuspenseContext {
|
||||
/// Notifies the suspense context that a resource has resolved.
|
||||
pub fn decrement(&self, serializable: bool) {
|
||||
let setter = self.set_pending_resources;
|
||||
let serializable_resources = self.pending_serializable_resources;
|
||||
let serializable_resources = self.pending_serializable_resources_count;
|
||||
setter.update(|n| {
|
||||
if *n > 0 {
|
||||
*n -= 1
|
||||
@@ -176,16 +192,83 @@ impl SuspenseContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies the suspense context that a new resource is now pending.
|
||||
pub(crate) fn increment_for_resource(
|
||||
&self,
|
||||
serializable: bool,
|
||||
resource: ResourceId,
|
||||
) {
|
||||
let pending = self.pending;
|
||||
let serializable_resources = self.pending_serializable_resources;
|
||||
let local_status = self.local_status;
|
||||
batch(move || {
|
||||
pending.update(|n| {
|
||||
n.insert(resource);
|
||||
});
|
||||
if serializable {
|
||||
serializable_resources.update(|n| {
|
||||
n.insert(resource);
|
||||
});
|
||||
local_status.update_value(|status| {
|
||||
*status = Some(match status {
|
||||
None => LocalStatus::SerializableOnly,
|
||||
Some(LocalStatus::LocalOnly) => LocalStatus::LocalOnly,
|
||||
Some(LocalStatus::Mixed) => LocalStatus::Mixed,
|
||||
Some(LocalStatus::SerializableOnly) => {
|
||||
LocalStatus::SerializableOnly
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
local_status.update_value(|status| {
|
||||
*status = Some(match status {
|
||||
None => LocalStatus::LocalOnly,
|
||||
Some(LocalStatus::LocalOnly) => LocalStatus::LocalOnly,
|
||||
Some(LocalStatus::Mixed) => LocalStatus::Mixed,
|
||||
Some(LocalStatus::SerializableOnly) => {
|
||||
LocalStatus::Mixed
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Notifies the suspense context that a resource has resolved.
|
||||
pub fn decrement_for_resource(
|
||||
&self,
|
||||
serializable: bool,
|
||||
resource: ResourceId,
|
||||
) {
|
||||
let setter = self.pending;
|
||||
let serializable_resources = self.pending_serializable_resources;
|
||||
batch(move || {
|
||||
setter.update(|n| {
|
||||
n.remove(&resource);
|
||||
});
|
||||
if serializable {
|
||||
serializable_resources.update(|n| {
|
||||
n.remove(&resource);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Resets the counter of pending resources.
|
||||
pub fn clear(&self) {
|
||||
self.set_pending_resources.set(0);
|
||||
self.pending_serializable_resources.set(0);
|
||||
batch(move || {
|
||||
self.set_pending_resources.set(0);
|
||||
self.pending.update(|p| p.clear());
|
||||
self.pending_serializable_resources.update(|p| p.clear());
|
||||
});
|
||||
}
|
||||
|
||||
/// Tests whether all of the pending resources have resolved.
|
||||
pub fn ready(&self) -> Memo<bool> {
|
||||
let pending = self.pending_resources;
|
||||
create_memo(move |_| pending.try_with(|n| *n == 0).unwrap_or(false))
|
||||
let pending = self.pending;
|
||||
create_memo(move |_| {
|
||||
pending.try_with(|n| n.is_empty()).unwrap_or(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +291,8 @@ pub enum StreamChunk {
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for StreamChunk {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for StreamChunk {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
StreamChunk::Sync(data) => write!(f, "StreamChunk::Sync({data:?})"),
|
||||
StreamChunk::Async { .. } => write!(f, "StreamChunk::Async(_)"),
|
||||
|
||||
@@ -18,6 +18,47 @@ pub struct Trigger {
|
||||
}
|
||||
|
||||
impl Trigger {
|
||||
/// Creates a [`Trigger`](crate::Trigger), a kind of reactive primitive.
|
||||
///
|
||||
/// A trigger is a data-less signal with the sole purpose
|
||||
/// of notifying other reactive code of a change. This can be useful
|
||||
/// for when using external data not stored in signals, for example.
|
||||
///
|
||||
/// This is identical to [`create_trigger`].
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// use std::{cell::RefCell, fmt::Write, rc::Rc};
|
||||
///
|
||||
/// let external_data = Rc::new(RefCell::new(1));
|
||||
/// let output = Rc::new(RefCell::new(String::new()));
|
||||
///
|
||||
/// let rerun_on_data = Trigger::new();
|
||||
///
|
||||
/// let o = output.clone();
|
||||
/// let e = external_data.clone();
|
||||
/// create_effect(move |_| {
|
||||
/// // can be `rerun_on_data()` on nightly
|
||||
/// rerun_on_data.track();
|
||||
/// write!(o.borrow_mut(), "{}", *e.borrow());
|
||||
/// *e.borrow_mut() += 1;
|
||||
/// });
|
||||
/// # if !cfg!(feature = "ssr") {
|
||||
/// assert_eq!(*output.borrow(), "1");
|
||||
///
|
||||
/// rerun_on_data.notify(); // reruns the above effect
|
||||
///
|
||||
/// assert_eq!(*output.borrow(), "12");
|
||||
/// # }
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
#[track_caller]
|
||||
pub fn new() -> Self {
|
||||
create_trigger()
|
||||
}
|
||||
|
||||
/// Notifies any reactive code where this trigger is tracked to rerun.
|
||||
///
|
||||
/// ## Panics
|
||||
@@ -97,6 +138,12 @@ pub fn create_trigger() -> Trigger {
|
||||
Runtime::current().create_trigger()
|
||||
}
|
||||
|
||||
impl Default for Trigger {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SignalGet for Trigger {
|
||||
type Value = ();
|
||||
|
||||
|
||||
@@ -96,6 +96,68 @@ where
|
||||
self.0.with_value(|a| a.dispatch(input))
|
||||
}
|
||||
|
||||
/// Create an [Action].
|
||||
///
|
||||
/// [Action] is a type of [Signal] which represent imperative calls to
|
||||
/// an asynchronous function. Where a [Resource] is driven as a function
|
||||
/// of a [Signal], [Action]s are [Action::dispatch]ed by events or handlers.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
///
|
||||
/// let act = Action::new(|n: &u8| {
|
||||
/// let n = n.to_owned();
|
||||
/// async move { n * 2 }
|
||||
/// });
|
||||
/// # if false {
|
||||
/// act.dispatch(3);
|
||||
/// assert_eq!(act.value().get(), Some(6));
|
||||
///
|
||||
/// // Remember that async functions already return a future if they are
|
||||
/// // not `await`ed. You can save keystrokes by leaving out the `async move`
|
||||
///
|
||||
/// let act2 = Action::new(|n: &String| yell(n.to_owned()));
|
||||
/// act2.dispatch(String::from("i'm in a doctest"));
|
||||
/// assert_eq!(act2.value().get(), Some("I'M IN A DOCTEST".to_string()));
|
||||
/// # }
|
||||
///
|
||||
/// async fn yell(n: String) -> String {
|
||||
/// n.to_uppercase()
|
||||
/// }
|
||||
///
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn new<F, Fu>(action_fn: F) -> Self
|
||||
where
|
||||
F: Fn(&I) -> Fu + 'static,
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
let version = create_rw_signal(0);
|
||||
let input = create_rw_signal(None);
|
||||
let value = create_rw_signal(None);
|
||||
let pending = create_rw_signal(false);
|
||||
let pending_dispatches = Rc::new(Cell::new(0));
|
||||
let action_fn = Rc::new(move |input: &I| {
|
||||
let fut = action_fn(input);
|
||||
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
|
||||
});
|
||||
|
||||
Action(store_value(ActionState {
|
||||
version,
|
||||
url: None,
|
||||
input,
|
||||
value,
|
||||
pending,
|
||||
pending_dispatches,
|
||||
action_fn,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Whether the action has been dispatched and is currently waiting for its future to be resolved.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
@@ -105,6 +167,66 @@ where
|
||||
self.0.with_value(|a| a.pending.read_only())
|
||||
}
|
||||
|
||||
/// Create an [Action] to imperatively call a [server_fn::server] function.
|
||||
///
|
||||
/// The struct representing your server function's arguments should be
|
||||
/// provided to the [Action]. Unless specified as an argument to the server
|
||||
/// macro, the generated struct is your function's name converted to CamelCase.
|
||||
///
|
||||
/// ```rust
|
||||
/// # // Not in a localset, so this would always panic.
|
||||
/// # if false {
|
||||
/// # use leptos::*;
|
||||
/// # let rt = create_runtime();
|
||||
///
|
||||
/// // The type argument can be on the right of the equal sign.
|
||||
/// let act = Action::<Add, _>::server();
|
||||
/// let args = Add { lhs: 5, rhs: 7 };
|
||||
/// act.dispatch(args);
|
||||
/// assert_eq!(act.value().get(), Some(Ok(12)));
|
||||
///
|
||||
/// // Or on the left of the equal sign.
|
||||
/// let act: Action<Sub, _> = Action::server();
|
||||
/// let args = Sub { lhs: 20, rhs: 5 };
|
||||
/// act.dispatch(args);
|
||||
/// assert_eq!(act.value().get(), Some(Ok(15)));
|
||||
///
|
||||
/// let not_dispatched = Action::<Add, _>::server();
|
||||
/// assert_eq!(not_dispatched.value().get(), None);
|
||||
///
|
||||
/// #[server]
|
||||
/// async fn add(lhs: u8, rhs: u8) -> Result<u8, ServerFnError> {
|
||||
/// Ok(lhs + rhs)
|
||||
/// }
|
||||
///
|
||||
/// #[server]
|
||||
/// async fn sub(lhs: u8, rhs: u8) -> Result<u8, ServerFnError> {
|
||||
/// Ok(lhs - rhs)
|
||||
/// }
|
||||
///
|
||||
/// # rt.dispose();
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn server() -> Action<I, Result<I::Output, ServerFnError>>
|
||||
where
|
||||
I: ServerFn<Output = O> + Clone,
|
||||
{
|
||||
// The server is able to call the function directly
|
||||
#[cfg(feature = "ssr")]
|
||||
let action_function = |args: &I| I::call_fn(args.clone(), ());
|
||||
|
||||
// When not on the server send a fetch to request the fn call.
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let action_function = |args: &I| I::call_fn_client(args.clone(), ());
|
||||
|
||||
// create the action
|
||||
Action::new(action_function).using_server_fn::<I>()
|
||||
}
|
||||
|
||||
/// Updates whether the action is currently pending. If the action has been dispatched
|
||||
/// multiple times, and some of them are still pending, it will *not* update the `pending`
|
||||
/// signal.
|
||||
@@ -339,25 +461,7 @@ where
|
||||
F: Fn(&I) -> Fu + 'static,
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
let version = create_rw_signal(0);
|
||||
let input = create_rw_signal(None);
|
||||
let value = create_rw_signal(None);
|
||||
let pending = create_rw_signal(false);
|
||||
let pending_dispatches = Rc::new(Cell::new(0));
|
||||
let action_fn = Rc::new(move |input: &I| {
|
||||
let fut = action_fn(input);
|
||||
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
|
||||
});
|
||||
|
||||
Action(store_value(ActionState {
|
||||
version,
|
||||
url: None,
|
||||
input,
|
||||
value,
|
||||
pending,
|
||||
pending_dispatches,
|
||||
action_fn,
|
||||
}))
|
||||
Action::new(action_fn)
|
||||
}
|
||||
|
||||
/// Creates an [Action] that can be used to call a server function.
|
||||
@@ -382,9 +486,5 @@ pub fn create_server_action<S>() -> Action<S, Result<S::Output, ServerFnError>>
|
||||
where
|
||||
S: Clone + ServerFn,
|
||||
{
|
||||
#[cfg(feature = "ssr")]
|
||||
let c = move |args: &S| S::call_fn(args.clone(), ());
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let c = move |args: &S| S::call_fn_client(args.clone(), ());
|
||||
create_action(c).using_server_fn::<S>()
|
||||
Action::<S, _>::server()
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ impl BodyContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for BodyContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for BodyContext {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_tuple("TitleContext").finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ impl HtmlContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for HtmlContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for HtmlContext {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_tuple("TitleContext").finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,8 +106,8 @@ pub struct MetaTagsContext {
|
||||
>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MetaTagsContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for MetaTagsContext {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("MetaTagsContext").finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ impl TitleContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TitleContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for TitleContext {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_tuple("TitleContext").finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ leptos_integration_utils = { workspace = true, optional = true }
|
||||
leptos_meta = { workspace = true, optional = true }
|
||||
cached = { version = "0.45.0", optional = true }
|
||||
cfg-if = "1"
|
||||
common_macros = "0.1"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
lazy_static = "1"
|
||||
linear-map = { version = "1", features = ["serde_impl"] }
|
||||
|
||||
@@ -365,6 +365,47 @@ fn current_window_origin() -> String {
|
||||
/// **Note:** `<ActionForm/>` only works with server functions that use the
|
||||
/// default `Url` encoding. This is to ensure that `<ActionForm/>` works correctly
|
||||
/// both before and after WASM has loaded.
|
||||
///
|
||||
/// ## 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(())
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
|
||||
@@ -96,6 +96,10 @@ pub fn A<H>(
|
||||
/// Sets the `id` attribute on the underlying `<a>` tag, making it easier to target.
|
||||
#[prop(optional, into)]
|
||||
id: Option<Oco<'static, str>>,
|
||||
/// Arbitrary attributes to add to the `<a>`. Attributes can be added with the
|
||||
/// `attr:` syntax in the `view` macro.
|
||||
#[prop(attrs)]
|
||||
attributes: Vec<(&'static str, Attribute)>,
|
||||
/// The nodes or elements to be shown inside the link.
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
@@ -115,6 +119,7 @@ where
|
||||
class: Option<AttributeValue>,
|
||||
#[allow(unused)] active_class: Option<Oco<'static, str>>,
|
||||
id: Option<Oco<'static, str>>,
|
||||
#[allow(unused)] attributes: Vec<(&'static str, Attribute)>,
|
||||
children: Children,
|
||||
) -> View {
|
||||
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
|
||||
@@ -147,74 +152,90 @@ where
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
// if we have `active_class`, the SSR optimization doesn't play nicely
|
||||
// if we have `active_class` or arbitrary attributes,
|
||||
// the SSR optimization doesn't play nicely
|
||||
// so we use the builder instead
|
||||
if let Some(active_class) = active_class {
|
||||
let mut a = leptos::html::a()
|
||||
.attr("href", move || href.get().unwrap_or_default())
|
||||
.attr("target", target)
|
||||
.attr("aria-current", move || {
|
||||
if is_active.get() {
|
||||
Some("page")
|
||||
} else {
|
||||
None
|
||||
let needs_builder =
|
||||
active_class.is_some() || !attributes.is_empty();
|
||||
if needs_builder {
|
||||
let mut a = leptos::html::a()
|
||||
.attr("href", move || href.get().unwrap_or_default())
|
||||
.attr("target", target)
|
||||
.attr("aria-current", move || {
|
||||
if is_active.get() {
|
||||
Some("page")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.attr(
|
||||
"class",
|
||||
class.map(|class| class.into_attribute_boxed()),
|
||||
);
|
||||
|
||||
if let Some(active_class) = active_class {
|
||||
for class_name in active_class.split_ascii_whitespace()
|
||||
{
|
||||
a = a.class(class_name.to_string(), move || {
|
||||
is_active.get()
|
||||
})
|
||||
}
|
||||
})
|
||||
.attr(
|
||||
"class",
|
||||
class.map(|class| class.into_attribute_boxed()),
|
||||
);
|
||||
}
|
||||
|
||||
for class_name in active_class.split_ascii_whitespace() {
|
||||
a = a.class(class_name.to_string(), move || is_active.get())
|
||||
a = a.attr("id", id).child(children());
|
||||
|
||||
for (attr_name, attr_value) in attributes {
|
||||
a = a.attr(attr_name, attr_value);
|
||||
}
|
||||
|
||||
a
|
||||
}
|
||||
|
||||
a.attr("id", id).child(children()).into_view()
|
||||
}
|
||||
// but keep the nice SSR optimization in most cases
|
||||
else {
|
||||
view! {
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
target=target
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=class
|
||||
id=id
|
||||
>
|
||||
{children()}
|
||||
</a>
|
||||
// but keep the nice SSR optimization in most cases
|
||||
else {
|
||||
view! {
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
target=target
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=class
|
||||
id=id
|
||||
>
|
||||
{children()}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
}
|
||||
|
||||
// the non-SSR version doesn't need the SSR optimizations
|
||||
// DRY here to avoid WASM binary size bloat
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
let a = view! {
|
||||
let mut a = view! {
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
target=target
|
||||
prop:state={state.map(|s| s.to_js_value())}
|
||||
prop:replace={replace}
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
prop:state=state.map(|s| s.to_js_value())
|
||||
prop:replace=replace
|
||||
aria-current=move || if is_active.get() { Some("a") } else { None }
|
||||
class=class
|
||||
id=id
|
||||
>
|
||||
{children()}
|
||||
</a>
|
||||
};
|
||||
|
||||
if let Some(active_class) = active_class {
|
||||
let mut a = a;
|
||||
for class_name in active_class.split_ascii_whitespace() {
|
||||
a = a.class(class_name.to_string(), move || is_active.get())
|
||||
}
|
||||
a
|
||||
} else {
|
||||
a
|
||||
}
|
||||
.into_view()
|
||||
|
||||
for (attr_name, attr_value) in attributes {
|
||||
a = a.attr(attr_name, attr_value);
|
||||
}
|
||||
|
||||
a.into_view()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +249,7 @@ where
|
||||
class,
|
||||
active_class,
|
||||
id,
|
||||
attributes,
|
||||
children,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn Redirect<P>(
|
||||
options: Option<NavigateOptions>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
P: std::fmt::Display + 'static,
|
||||
P: core::fmt::Display + 'static,
|
||||
{
|
||||
// resolve relative path
|
||||
let path = use_resolved_path(move || path.to_string());
|
||||
@@ -68,8 +68,8 @@ pub struct ServerRedirectFunction {
|
||||
f: Rc<dyn Fn(&str)>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ServerRedirectFunction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for ServerRedirectFunction {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("ServerRedirectFunction").finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
@@ -71,7 +72,7 @@ pub fn Route<E, F, P>(
|
||||
where
|
||||
E: IntoView,
|
||||
F: Fn() -> E + 'static,
|
||||
P: std::fmt::Display,
|
||||
P: core::fmt::Display,
|
||||
{
|
||||
define_route(
|
||||
children,
|
||||
@@ -121,7 +122,7 @@ pub fn ProtectedRoute<P, E, F, C>(
|
||||
where
|
||||
E: IntoView,
|
||||
F: Fn() -> E + 'static,
|
||||
P: std::fmt::Display + 'static,
|
||||
P: core::fmt::Display + 'static,
|
||||
C: Fn() -> bool + 'static,
|
||||
{
|
||||
use crate::Redirect;
|
||||
@@ -162,8 +163,7 @@ pub fn StaticRoute<E, F, P, S>(
|
||||
/// or `|| view! { <MyComponent/>` } or even, for a component with no props, `MyComponent`).
|
||||
view: F,
|
||||
/// Creates a map of the params that should be built for a particular route.
|
||||
#[prop(optional)]
|
||||
static_params: Option<S>,
|
||||
static_params: S,
|
||||
/// The static route mode
|
||||
#[prop(optional)]
|
||||
mode: StaticMode,
|
||||
@@ -178,8 +178,11 @@ pub fn StaticRoute<E, F, P, S>(
|
||||
where
|
||||
E: IntoView,
|
||||
F: Fn() -> E + 'static,
|
||||
P: std::fmt::Display,
|
||||
S: Fn() -> Pin<Box<dyn Future<Output = StaticParamsMap>>> + 'static,
|
||||
P: core::fmt::Display,
|
||||
S: Fn() -> Pin<Box<dyn Future<Output = StaticParamsMap> + Send + Sync>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
define_route(
|
||||
children,
|
||||
@@ -189,7 +192,7 @@ where
|
||||
&[Method::Get],
|
||||
data,
|
||||
Some(mode),
|
||||
static_params.map(|s| Rc::new(s) as _),
|
||||
Some(Arc::new(static_params)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -404,8 +407,8 @@ impl PartialEq for RouteContextInner {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RouteContextInner {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for RouteContextInner {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("RouteContextInner")
|
||||
.field("path", &self.path)
|
||||
.field("ParamsMap", &self.params)
|
||||
|
||||
@@ -66,8 +66,8 @@ pub(crate) struct RouterContextInner {
|
||||
pub(crate) path_stack: StoredValue<Vec<String>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RouterContextInner {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for RouterContextInner {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("RouterContextInner")
|
||||
.field("location", &self.location)
|
||||
.field("base", &self.base)
|
||||
|
||||
@@ -15,7 +15,7 @@ use std::{
|
||||
hash::{Hash, Hasher},
|
||||
path::PathBuf,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
@@ -161,7 +161,7 @@ impl StaticPath<'_, '_> {}
|
||||
pub struct ResolvedStaticPath(pub String);
|
||||
|
||||
impl Display for ResolvedStaticPath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
@@ -304,10 +304,12 @@ pub fn purge_all_static_routes<IV>(
|
||||
purge_dir_of_static_files(Path::new(&options.site_root).to_path_buf())
|
||||
}
|
||||
|
||||
pub type StaticData = Rc<StaticDataFn>;
|
||||
pub type StaticData = Arc<StaticDataFn>;
|
||||
|
||||
pub type StaticDataFn =
|
||||
dyn Fn() -> Pin<Box<dyn Future<Output = StaticParamsMap>>> + 'static;
|
||||
pub type StaticDataFn = dyn Fn() -> Pin<Box<dyn Future<Output = StaticParamsMap> + Send + Sync>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static;
|
||||
|
||||
pub type StaticDataMap = HashMap<String, Option<StaticData>>;
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ pub use location::*;
|
||||
pub use params::*;
|
||||
pub use state::*;
|
||||
|
||||
impl std::fmt::Debug for RouterIntegrationContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for RouterIntegrationContext {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("RouterIntegrationContext").finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,31 +72,41 @@ impl Default for ParamsMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// A declarative way of creating a [`ParamsMap`].
|
||||
/// Create a [`ParamsMap`] in a declarative style.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_router::params_map;
|
||||
/// let map = params_map! {
|
||||
/// "id" => "1"
|
||||
/// "crate" => "leptos",
|
||||
/// 42 => true, // where key & val: core::fmt::Display
|
||||
/// };
|
||||
/// assert_eq!(map.get("id"), Some(&"1".to_string()));
|
||||
/// assert_eq!(map.get("missing"), None)
|
||||
/// assert_eq!(map.get("crate"), Some(&"leptos".to_string()));
|
||||
/// assert_eq!(map.get("42"), Some(&true.to_string()))
|
||||
/// ```
|
||||
// Original implementation included the below credits.
|
||||
//
|
||||
// Adapted from hash_map! in common_macros crate
|
||||
// Copyright (c) 2019 Philipp Korber
|
||||
// https://github.com/rustonaut/common_macros/blob/master/src/lib.rs
|
||||
#[macro_export]
|
||||
macro_rules! params_map {
|
||||
($($key:expr => $val:expr),* ,) => (
|
||||
$crate::ParamsMap!($($key => $val),*)
|
||||
);
|
||||
($($key:expr => $val:expr),*) => ({
|
||||
let start_capacity = common_macros::const_expr_count!($($key);*);
|
||||
// Fast path avoids allocation.
|
||||
() => { $crate::ParamsMap::with_capacity(0) };
|
||||
|
||||
// Counting repitions by n = 0 ( + 1 )*
|
||||
//
|
||||
// https://github.com/rust-lang/rust/issues/83527
|
||||
// When stabilized you can use "metavaribale exprs" instead
|
||||
//
|
||||
// `$key | $val` must be included in the repetition to be valid, it is
|
||||
// stringified to null out any possible side-effects.
|
||||
($($key:expr => $val:expr),* $(,)?) => {{
|
||||
let n = 0 $(+ { _ = stringify!($key); 1 })*;
|
||||
#[allow(unused_mut)]
|
||||
let mut map = linear_map::LinearMap::with_capacity(start_capacity);
|
||||
let mut map = $crate::ParamsMap::with_capacity(n);
|
||||
$( map.insert($key.to_string(), $val.to_string()); )*
|
||||
$crate::ParamsMap(map)
|
||||
});
|
||||
map
|
||||
}};
|
||||
}
|
||||
|
||||
/// A simple method of deserializing key-value data (like route params or URL search)
|
||||
|
||||
@@ -177,8 +177,7 @@
|
||||
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
|
||||
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
|
||||
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
|
||||
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
|
||||
//! of calling signals as functions. Enable this feature to support `stable` Rust.
|
||||
//! - `nightly`: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
|
||||
//!
|
||||
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
||||
//! which mode your app is operating in.
|
||||
|
||||
@@ -27,8 +27,8 @@ pub struct RouteDefinition {
|
||||
pub static_params: Option<StaticData>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RouteDefinition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for RouteDefinition {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("RouteDefinition")
|
||||
.field("path", &self.path)
|
||||
.field("children", &self.children)
|
||||
|
||||
@@ -72,7 +72,7 @@ pub enum ServerFnError {
|
||||
MissingArg(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ServerFnError {
|
||||
impl core::fmt::Display for ServerFnError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
|
||||
Reference in New Issue
Block a user