Compare commits

..

18 Commits

Author SHA1 Message Date
Greg Johnston
8f067dcde7 chore: clear release-mode warnings 2023-08-25 17:16:00 -04:00
Greg Johnston
ad6eb58fe1 fix: <Transition/> fallback in CSR 2023-08-25 17:12:01 -04:00
Greg Johnston
3f3ab1c3c8 remove unnecessary parens 2023-08-25 16:49:26 -04:00
Greg Johnston
9adae32847 examples: improve hackernews behavior 2023-08-25 16:00:47 -04:00
Greg Johnston
b8098e7992 fix: <Transition/> fallback on non-initial page loads 2023-08-25 16:00:47 -04:00
Greg Johnston
bef4d0dd3b fix: resource loading signal pattern for subsequent hydration page loads 2023-08-25 16:00:47 -04:00
Matt Cuneo
a789100e22 feat: allow autoreload websocket connection to work outside of localhost (#1548)
* Updated client reloading to use window.location.protocol/host to determine websocket connection. Added optional config reload_external_port to provide further control of the client websocket connection. These changes allow reloading while accessing the served site from outside of localhost.
2023-08-25 15:54:22 -04:00
Greg Johnston
abeca70625 fix: correct logic for resource loading signal when read outside suspense (#1586) 2023-08-25 11:46:54 -04:00
rkuklik
cc293b1170 feat: generic event handler types to make it easier to create collections of event handlers (#1444) 2023-08-25 11:41:16 -04:00
Greg Johnston
8ab62c17c6 feat: add Fn traits for resources on nightly (#1587) 2023-08-25 11:20:29 -04:00
Joseph Cruz
cf14e857ca refactor(check-stable): use matrix (#1543) (#1583)
* refactor(check-stable): use matrix

* chore: simulate leptos change

* chore: remove simulated change
2023-08-25 10:30:00 -04:00
Greg Johnston
c322ef38fd feat: signal traits should take associated types instead of generics (#1578) 2023-08-25 10:29:24 -04:00
Greg Johnston
c9cc493063 fix: fourth argument to server functions (#1585) 2023-08-25 10:28:54 -04:00
Joseph Cruz
fb48f7f117 fix(counters_stable): restore wasm tests (#1581) (#1582) 2023-08-24 16:33:01 -04:00
尹吉峰
c344e54cf6 feat: return an Effect from create_effect that can be disposed (#1571) 2023-08-24 10:24:10 -04:00
Greg Johnston
7306ecccbc feat: make struct name and path optional for server functions (#1573) 2023-08-24 10:22:35 -04:00
Greg Johnston
b98174db7a feat: support passing signals directly as attributes, classes, styles, and props on stable (#1577) 2023-08-24 10:22:14 -04:00
Greg Johnston
e48f66694d fix: runtime disposal time in render_to_string_async (#1574) 2023-08-24 10:22:00 -04:00
51 changed files with 1329 additions and 474 deletions

View File

@@ -26,3 +26,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
toolchain: nightly

View File

@@ -2,50 +2,25 @@ name: Check stable
on:
push:
branches: [main]
branches:
- main
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
branches:
- main
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
test:
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
name: Check
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- stable
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Run cargo check on all examples
run: cargo make --profile=github-actions check-stable
directory: [examples/counters_stable, examples/counter_without_macros]
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
toolchain: stable

View File

@@ -29,3 +29,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -41,3 +41,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -9,6 +9,9 @@ on:
cargo_make_task:
required: true
type: string
toolchain:
required: true
type: string
env:
CARGO_TERM_COLOR: always
@@ -16,14 +19,8 @@ env:
jobs:
test:
name: Run ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
runs-on: ubuntu-latest
steps:
# Setup environment
@@ -32,7 +29,7 @@ jobs:
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
toolchain: ${{ inputs.toolchain }}
override: true
components: rustfmt

View File

@@ -23,3 +23,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -5,12 +5,15 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
leptos_meta = { path = "../../meta", features = ["csr"] }
log = "0.4"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
wasm-bindgen = "0.2.87"
wasm-bindgen-test = "0.3.37"
pretty_assertions = "1.4.0"
[dev-dependencies.web-sys]
features = [

View File

@@ -1,5 +1,6 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
{ path = "../cargo-make/trunk_server.toml" },
{ path = "../cargo-make/playwright-test.toml" },
]

View File

@@ -1,8 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Counters (Stable)</title>
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
</head>
<body></body>
</html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs />
</head>
<body></body>
</html>

View File

@@ -0,0 +1,106 @@
use leptos::*;
use leptos_meta::*;
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
struct CounterUpdater {
set_counters: WriteSignal<CounterHolder>,
}
#[component]
pub fn Counters() -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(0);
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
provide_context(CounterUpdater { set_counters });
let add_counter = move |_| {
let id = next_counter_id.get();
let sig = create_signal(0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
let add_many_counters = move |_| {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(0);
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
set_counters.update(|counters| counters.clear());
};
view! {
<Title text="Counters (Stable)" />
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<button on:click=add_many_counters>
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"
</button>
<p>
"Total: "
<span data-testid="total">{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
.sum::<i32>()
.to_string()
}</span>
" from "
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |(id, (value, set_value))| {
view! {
<Counter id value set_value/>
}
}
/>
</ul>
</div>
}
}
#[component]
fn Counter(
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context().unwrap();
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
view! {
<li>
<button data-testid="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input data-testid="counter_input" type="text"
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{value}</span>
<button data-testid="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button data-testid="remove_counter" on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>
}
}

View File

@@ -1,3 +1,4 @@
use counters_stable::Counters;
use leptos::*;
fn main() {
@@ -5,106 +6,3 @@ fn main() {
console_error_panic_hook::set_once();
mount_to_body(|| view! { <Counters/> })
}
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
struct CounterUpdater {
set_counters: WriteSignal<CounterHolder>,
}
#[component]
pub fn Counters() -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(0);
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
provide_context(CounterUpdater { set_counters });
let add_counter = move |_| {
let id = next_counter_id.get();
let sig = create_signal(0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
let add_many_counters = move |_| {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(0);
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
set_counters.update(|counters| counters.clear());
};
view! {
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<button on:click=add_many_counters>
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"
</button>
<p>
"Total: "
<span data-testid="total">{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
.sum::<i32>()
.to_string()
}</span>
" from "
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |(id, (value, set_value))| {
view! {
<Counter id value set_value/>
}
}
/>
</ul>
</div>
}
}
#[component]
fn Counter(
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context().unwrap();
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
view! {
<li>
<button id="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input type="text"
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{value}</span>
<button id="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>
}
}

View File

@@ -0,0 +1,17 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_number_of_counters() {
// Given
ui::view_counters();
// When
ui::add_1k_counters();
ui::add_1k_counters();
ui::add_1k_counters();
// Then
assert_eq!(ui::counters(), 3000);
}

View File

@@ -0,0 +1,17 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_number_of_counters() {
// Given
ui::view_counters();
// When
ui::add_counter();
ui::add_counter();
ui::add_counter();
// Then
assert_eq!(ui::counters(), 3);
}

View File

@@ -0,0 +1,19 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_reset_the_counts() {
// Given
ui::view_counters();
ui::add_counter();
ui::add_counter();
ui::add_counter();
// When
ui::clear_counters();
// Then
assert_eq!(ui::total(), 0);
assert_eq!(ui::counters(), 0);
}

View File

@@ -0,0 +1,18 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_decrease_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
// When
ui::decrement_counter(1);
ui::decrement_counter(1);
ui::decrement_counter(1);
// Then
assert_eq!(ui::total(), -3);
}

View File

@@ -0,0 +1,112 @@
use counters_stable::Counters;
use leptos::*;
use wasm_bindgen::JsCast;
use web_sys::{Element, Event, EventInit, HtmlElement, HtmlInputElement};
// Actions
pub fn add_1k_counters() {
find_by_text("Add 1000 Counters").click();
}
pub fn add_counter() {
find_by_text("Add Counter").click();
}
pub fn clear_counters() {
find_by_text("Clear Counters").click();
}
pub fn decrement_counter(index: u32) {
counter_html_element(index, "decrement_count").click();
}
pub fn enter_count(index: u32, count: i32) {
let input = counter_input_element(index, "counter_input");
input.set_value(count.to_string().as_str());
let mut event_init = EventInit::new();
event_init.bubbles(true);
let event = Event::new_with_event_init_dict("input", &event_init).unwrap();
input.dispatch_event(&event).unwrap();
}
pub fn increment_counter(index: u32) {
counter_html_element(index, "increment_count").click();
}
pub fn remove_counter(index: u32) {
counter_html_element(index, "remove_counter").click();
}
pub fn view_counters() {
remove_existing_counters();
mount_to_body(|| view! { <Counters/> });
}
// Results
pub fn counters() -> i32 {
data_test_id("counters").parse::<i32>().unwrap()
}
pub fn title() -> String {
leptos::document().title()
}
pub fn total() -> i32 {
data_test_id("total").parse::<i32>().unwrap()
}
// Internal
fn counter_element(index: u32, text: &str) -> Element {
let selector =
format!("li:nth-child({}) [data-testid=\"{}\"]", index, text);
leptos::document()
.query_selector(&selector)
.unwrap()
.unwrap()
}
fn counter_html_element(index: u32, text: &str) -> HtmlElement {
counter_element(index, text)
.dyn_into::<HtmlElement>()
.unwrap()
}
fn counter_input_element(index: u32, text: &str) -> HtmlInputElement {
counter_element(index, text)
.dyn_into::<HtmlInputElement>()
.unwrap()
}
fn data_test_id(id: &str) -> String {
let selector = format!("[data-testid=\"{}\"]", id);
leptos::document()
.query_selector(&selector)
.unwrap()
.expect("counters not found")
.text_content()
.unwrap()
}
fn find_by_text(text: &str) -> HtmlElement {
let xpath = format!("//*[text()='{}']", text);
let document = leptos::document();
document
.evaluate(&xpath, &document)
.unwrap()
.iterate_next()
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
}
fn remove_existing_counters() {
if let Some(counter) =
leptos::document().query_selector("body div").unwrap()
{
counter.remove();
}
}

View File

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

View File

@@ -0,0 +1,18 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
// When
ui::increment_counter(1);
ui::increment_counter(1);
ui::increment_counter(1);
// Then
assert_eq!(ui::total(), 3);
}

View File

@@ -0,0 +1,16 @@
use wasm_bindgen_test::*;
// Test Suites
pub mod add_1k_counters;
pub mod add_counter;
pub mod clear_counters;
pub mod decrement_counter;
pub mod enter_count;
pub mod increment_counter;
pub mod remove_counter;
pub mod view_counters;
pub mod fixtures;
pub use fixtures::*;
wasm_bindgen_test_configure!(run_in_browser);

View File

@@ -0,0 +1,18 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_decrement_the_number_of_counters() {
// Given
ui::view_counters();
ui::add_counter();
ui::add_counter();
ui::add_counter();
// When
ui::remove_counter(2);
// Then
assert_eq!(ui::counters(), 2);
}

View File

@@ -0,0 +1,22 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_see_the_initial_counts() {
// When
ui::view_counters();
// Then
assert_eq!(ui::total(), 0);
assert_eq!(ui::counters(), 0);
}
#[wasm_bindgen_test]
fn should_see_the_title() {
// When
ui::view_counters();
// Then
assert_eq!(ui::title(), "Counters (Stable)");
}

View File

@@ -36,8 +36,8 @@ pub fn Stories() -> impl IntoView {
let (pending, set_pending) = create_signal(false);
let hide_more_link = move || {
pending()
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|| pending()
};
view! {
@@ -65,20 +65,16 @@ pub fn Stories() -> impl IntoView {
}}
</span>
<span>"page " {page}</span>
<Transition
fallback=move || view! { <p>"Loading..."</p> }
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</Transition>
"more >"
</a>
</span>
</div>
<main class="news-list">
<div>

View File

@@ -25,48 +25,45 @@ pub fn Story() -> impl IntoView {
};
view! {
<>
<Suspense fallback=|| view! { "Loading..." }>
<Meta name="description" content=meta_description/>
<Suspense fallback=|| view! { "Loading..." }>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(story) => view! {
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move |comment| view! { <Comment comment /> }
/>
</ul>
</div>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(story) => view! {
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>
}})
}
</Suspense>
</>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move |comment| view! { <Comment comment /> }
/>
</ul>
</div>
</div>
}})}
</Suspense>
}
}

View File

@@ -36,12 +36,11 @@ pub fn Stories() -> impl IntoView {
let (pending, set_pending) = create_signal(false);
let hide_more_link = move || {
pending()
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|| pending()
};
view! {
<div class="news-view">
<div class="news-list-nav">
<span>
@@ -65,20 +64,16 @@ pub fn Stories() -> impl IntoView {
}}
</span>
<span>"page " {page}</span>
<Transition
fallback=move || view! { <p>"Loading..."</p> }
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</Transition>
"more >"
</a>
</span>
</div>
<main class="news-list">
<div>

View File

@@ -25,48 +25,45 @@ pub fn Story() -> impl IntoView {
};
view! {
<>
<Suspense fallback=|| view! { "Loading..." }>
<Meta name="description" content=meta_description/>
<Suspense fallback=|| view! { "Loading..." }>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(story) => view! {
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move | comment| view! { <Comment comment /> }
/>
</ul>
</div>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(story) => view! {
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>
}})
}
</Suspense>
</>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move | comment| view! { <Comment comment /> }
/>
</ul>
</div>
</div>
}})}
</Suspense>
}
}

View File

@@ -27,6 +27,7 @@ use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use std::{fmt::Display, future::Future, sync::Arc};
#[cfg(debug_assertions)]
use tracing::instrument;
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.

View File

@@ -7,14 +7,18 @@ extern crate tracing;
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
let site_ip = &options.site_addr.ip().to_string();
let reload_port = options.reload_port;
let reload_port = match options.reload_external_port {
Some(val) => val,
None => options.reload_port,
};
match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin=""{nonce_str}>(function () {{
{}
let ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
let protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
let host = window.location.hostname;
let ws = new WebSocket(protocol + host + ':{reload_port}/live_reload');
ws.onmessage = (ev) => {{
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();

View File

@@ -1,8 +1,8 @@
use leptos_dom::{Fragment, HydrationCtx, IntoView, View};
use leptos_macro::component;
use leptos_reactive::{
create_isomorphic_effect, use_context, SignalGet, SignalSetter,
SuspenseContext,
create_isomorphic_effect, create_rw_signal, use_context, RwSignal,
SignalGet, SignalSet, SignalSetter, SuspenseContext,
};
use std::{
cell::{Cell, RefCell},
@@ -83,7 +83,7 @@ where
{
let prev_children = Rc::new(RefCell::new(None::<View>));
let first_run = Rc::new(std::cell::Cell::new(true));
let first_run = create_rw_signal(true);
let child_runs = Cell::new(0);
let held_suspense_context = Rc::new(RefCell::new(None::<SuspenseContext>));
@@ -91,17 +91,18 @@ where
crate::SuspenseProps::builder()
.fallback({
let prev_child = Rc::clone(&prev_children);
let first_run = Rc::clone(&first_run);
move || {
let suspense_context = use_context::<SuspenseContext>()
.expect("there to be a SuspenseContext");
let was_first_run =
cfg!(feature = "csr") && first_run.get();
let is_first_run =
is_first_run(&first_run, &suspense_context);
is_first_run(first_run, &suspense_context);
first_run.set(false);
if let Some(prev_children) = &*prev_child.borrow() {
if is_first_run {
if is_first_run || was_first_run {
fallback().into_view()
} else {
prev_children.clone()
@@ -127,12 +128,11 @@ where
{
*prev_children.borrow_mut() = Some(frag.clone());
}
if is_first_run(&first_run, &suspense_context) {
if is_first_run(first_run, &suspense_context) {
let has_local_only = suspense_context.has_local_only()
|| cfg!(feature = "csr");
if (!has_local_only || child_runs.get() > 0)
&& (cfg!(feature = "csr")
|| HydrationCtx::is_hydrating())
&& !cfg!(feature = "csr")
{
first_run.set(false);
}
@@ -152,7 +152,7 @@ where
}
fn is_first_run(
first_run: &Rc<Cell<bool>>,
first_run: RwSignal<bool>,
suspense_context: &SuspenseContext,
) -> bool {
if cfg!(feature = "csr") {

View File

@@ -56,6 +56,11 @@ pub struct LeptosOptions {
#[builder(default = default_reload_port())]
#[serde(default = "default_reload_port")]
pub reload_port: u32,
/// The port the Websocket watcher listens on when on the client, e.g., when behind a reverse proxy.
/// Defaults to match reload_port
#[builder(default)]
#[serde(default)]
pub reload_external_port: Option<u32>,
}
impl LeptosOptions {
@@ -84,6 +89,12 @@ impl LeptosOptions {
.parse()?,
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?
.parse()?,
reload_external_port: match env_wo_default(
"LEPTOS_RELOAD_EXTERNAL_PORT",
)? {
Some(val) => Some(val.parse()?),
None => None,
},
})
}
}
@@ -107,7 +118,13 @@ fn default_site_addr() -> SocketAddr {
fn default_reload_port() -> u32 {
3001
}
fn env_wo_default(key: &str) -> Result<Option<String>, LeptosConfigError> {
match std::env::var(key) {
Ok(val) => Ok(Some(val)),
Err(VarError::NotPresent) => Ok(None),
Err(e) => Err(LeptosConfigError::EnvVarError(format!("{key}: {e}"))),
}
}
fn env_w_default(
key: &str,
default: &str,

View File

@@ -1,4 +1,4 @@
use crate::{env_w_default, from_str, Env, LeptosOptions};
use crate::{env_w_default, env_wo_default, from_str, Env, LeptosOptions};
use std::{net::SocketAddr, str::FromStr};
#[test]
@@ -29,6 +29,17 @@ fn env_w_default_test() {
);
}
#[test]
fn env_wo_default_test() {
std::env::set_var("LEPTOS_CONFIG_ENV_TEST", "custom");
assert_eq!(
env_wo_default("LEPTOS_CONFIG_ENV_TEST").unwrap(),
Some(String::from("custom"))
);
std::env::remove_var("LEPTOS_CONFIG_ENV_TEST");
assert_eq!(env_wo_default("LEPTOS_CONFIG_ENV_TEST").unwrap(), None);
}
#[test]
fn try_from_env_test() {
// Test config values from environment variables
@@ -37,6 +48,7 @@ fn try_from_env_test() {
std::env::set_var("LEPTOS_SITE_PKG_DIR", "my_pkg");
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80");
std::env::set_var("LEPTOS_RELOAD_PORT", "8080");
std::env::set_var("LEPTOS_RELOAD_EXTERNAL_PORT", "8080");
let config = LeptosOptions::try_from_env().unwrap();
assert_eq!(config.output_name, "app_test");
@@ -48,4 +60,5 @@ fn try_from_env_test() {
SocketAddr::from_str("0.0.0.0:80").unwrap()
);
assert_eq!(config.reload_port, 8080);
assert_eq!(config.reload_external_port, Some(8080));
}

View File

@@ -17,6 +17,7 @@ site-root = "my_target/site"
site-pkg-dir = "my_pkg"
site-addr = "0.0.0.0:80"
reload-port = "8080"
reload-external-port = "8080"
env = "PROD"
"#;
@@ -27,6 +28,7 @@ _site-root = "my_target/site"
_site-pkg-dir = "my_pkg"
_site-addr = "0.0.0.0:80"
_reload-port = "8080"
_reload-external-port = "8080"
_env = "PROD"
"#;
@@ -54,6 +56,7 @@ async fn get_configuration_from_file_ok() {
SocketAddr::from_str("0.0.0.0:80").unwrap()
);
assert_eq!(config.reload_port, 8080);
assert_eq!(config.reload_external_port, Some(8080));
}
#[tokio::test]
@@ -101,6 +104,7 @@ async fn get_config_from_file_ok() {
SocketAddr::from_str("0.0.0.0:80").unwrap()
);
assert_eq!(config.reload_port, 8080);
assert_eq!(config.reload_external_port, Some(8080));
}
#[tokio::test]
@@ -136,6 +140,7 @@ fn get_config_from_str_content() {
SocketAddr::from_str("0.0.0.0:80").unwrap()
);
assert_eq!(config.reload_port, 8080);
assert_eq!(config.reload_external_port, Some(8080));
}
#[tokio::test]
@@ -146,6 +151,7 @@ async fn get_config_from_env() {
std::env::set_var("LEPTOS_SITE_PKG_DIR", "my_pkg");
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80");
std::env::set_var("LEPTOS_RELOAD_PORT", "8080");
std::env::set_var("LEPTOS_RELOAD_EXTERNAL_PORT", "8080");
let config = get_configuration(None).await.unwrap().leptos_options;
assert_eq!(config.output_name, "app-test");
@@ -157,12 +163,14 @@ async fn get_config_from_env() {
SocketAddr::from_str("0.0.0.0:80").unwrap()
);
assert_eq!(config.reload_port, 8080);
assert_eq!(config.reload_external_port, Some(8080));
// Test default config values
std::env::remove_var("LEPTOS_SITE_ROOT");
std::env::remove_var("LEPTOS_SITE_PKG_DIR");
std::env::remove_var("LEPTOS_SITE_ADDR");
std::env::remove_var("LEPTOS_RELOAD_PORT");
std::env::set_var("LEPTOS_RELOAD_EXTERNAL_PORT", "443");
let config = get_configuration(None).await.unwrap().leptos_options;
assert_eq!(config.site_root, "target/site");
@@ -172,6 +180,7 @@ async fn get_config_from_env() {
SocketAddr::from_str("127.0.0.1:3000").unwrap()
);
assert_eq!(config.reload_port, 3001);
assert_eq!(config.reload_external_port, Some(443));
}
#[test]
@@ -186,4 +195,5 @@ fn leptos_options_builder_default() {
SocketAddr::from_str("127.0.0.1:3000").unwrap()
);
assert_eq!(conf.reload_port, 3001);
assert_eq!(conf.reload_external_port, None);
}

View File

@@ -31,7 +31,7 @@ pub trait EventDescriptor: Clone {
/// Overrides the [`EventDescriptor::BUBBLES`] value to always return
/// `false`, which forces the event to not be globally delegated.
#[derive(Clone)]
#[derive(Clone, Debug)]
#[allow(non_camel_case_types)]
pub struct undelegated<Ev: EventDescriptor>(pub Ev);
@@ -52,6 +52,7 @@ impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
}
/// A custom event.
#[derive(Debug)]
pub struct Custom<E: FromWasmAbi = web_sys::Event> {
name: Cow<'static, str>,
options: Option<web_sys::AddEventListenerOptions>,
@@ -125,34 +126,255 @@ impl<E: FromWasmAbi> Custom<E> {
}
}
/// Type that can respond to DOM events
pub trait DOMEventResponder: Sized {
/// Adds handler to specified event
fn add<E: EventDescriptor + 'static>(
self,
event: E,
handler: impl FnMut(E::EventType) + 'static,
) -> Self;
/// Same as [add](DOMEventResponder::add), but with [`EventHandler`]
#[inline]
fn add_handler(self, handler: impl EventHandler) -> Self {
handler.attach(self)
}
}
impl<T> DOMEventResponder for crate::HtmlElement<T>
where
T: crate::html::ElementDescriptor + 'static,
{
#[inline(always)]
fn add<E: EventDescriptor + 'static>(
self,
event: E,
handler: impl FnMut(E::EventType) + 'static,
) -> Self {
self.on(event, handler)
}
}
impl DOMEventResponder for crate::View {
#[inline(always)]
fn add<E: EventDescriptor + 'static>(
self,
event: E,
handler: impl FnMut(E::EventType) + 'static,
) -> Self {
self.on(event, handler)
}
}
/// Type that can be used to handle DOM events
pub trait EventHandler {
/// Attaches event listener to any target that can respond to DOM events
fn attach<T: DOMEventResponder>(self, target: T) -> T;
}
impl<T, const N: usize> EventHandler for [T; N]
where
T: EventHandler,
{
#[inline]
fn attach<R: DOMEventResponder>(self, target: R) -> R {
let mut target = target;
for item in self {
target = item.attach(target);
}
target
}
}
impl<T> EventHandler for Option<T>
where
T: EventHandler,
{
#[inline]
fn attach<R: DOMEventResponder>(self, target: R) -> R {
match self {
Some(event_handler) => event_handler.attach(target),
None => target,
}
}
}
macro_rules! tc {
($($ty:ident),*) => {
impl<$($ty),*> EventHandler for ($($ty,)*)
where
$($ty: EventHandler),*
{
#[inline]
fn attach<RES: DOMEventResponder>(self, target: RES) -> RES {
::paste::paste! {
let (
$(
[<$ty:lower>],)*
) = self;
$(
let target = [<$ty:lower>].attach(target);
)*
target
}
}
}
};
}
tc!(A);
tc!(A, B);
tc!(A, B, C);
tc!(A, B, C, D);
tc!(A, B, C, D, E);
tc!(A, B, C, D, E, F);
tc!(A, B, C, D, E, F, G);
tc!(A, B, C, D, E, F, G, H);
tc!(A, B, C, D, E, F, G, H, I);
tc!(A, B, C, D, E, F, G, H, I, J);
tc!(A, B, C, D, E, F, G, H, I, J, K);
tc!(A, B, C, D, E, F, G, H, I, J, K, L);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y);
#[rustfmt::skip]
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z);
macro_rules! collection_callback {
{$(
$collection:ident
),* $(,)?} => {
$(
impl<T> EventHandler for $collection<T>
where
T: EventHandler
{
#[inline]
fn attach<R: DOMEventResponder>(self, target: R) -> R {
let mut target = target;
for item in self {
target = item.attach(target);
}
target
}
}
)*
};
}
use std::collections::{BTreeSet, BinaryHeap, HashSet, LinkedList, VecDeque};
collection_callback! {
Vec,
BTreeSet,
BinaryHeap,
HashSet,
LinkedList,
VecDeque,
}
macro_rules! generate_event_types {
{$(
$( #[$does_not_bubble:ident] )?
$event:ident : $web_sys_event:ident
$( $event:ident )+ : $web_event:ident
),* $(,)?} => {
$(
#[doc = concat!("The `", stringify!($event), "` event, which receives [", stringify!($web_sys_event), "](web_sys::", stringify!($web_sys_event), ") as its argument.")]
#[derive(Copy, Clone)]
::paste::paste! {
$(
#[doc = "The `" [< $($event)+ >] "` event, which receives [" $web_event "](web_sys::" $web_event ") as its argument."]
#[derive(Copy, Clone, Debug)]
#[allow(non_camel_case_types)]
pub struct $event;
pub struct [<$( $event )+ >];
impl EventDescriptor for $event {
type EventType = web_sys::$web_sys_event;
impl EventDescriptor for [< $($event)+ >] {
type EventType = web_sys::$web_event;
#[inline(always)]
fn name(&self) -> Cow<'static, str> {
stringify!($event).into()
stringify!([< $($event)+ >]).into()
}
#[inline(always)]
fn event_delegation_key(&self) -> Cow<'static, str> {
concat!("$$$", stringify!($event)).into()
concat!("$$$", stringify!([< $($event)+ >])).into()
}
const BUBBLES: bool = true $(&& generate_event_types!($does_not_bubble))?;
}
)*
)*
/// An enum holding all basic event types with their respective handlers.
///
/// It currently omits [`Custom`] and [`undelegated`] variants.
#[non_exhaustive]
pub enum GenericEventHandler {
$(
#[doc = "Variant mapping [`struct@" [< $($event)+ >] "`] to its event handler type."]
[< $($event:camel)+ >]([< $($event)+ >], Box<dyn FnMut($web_event) + 'static>),
)*
}
impl ::std::fmt::Debug for GenericEventHandler {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match self {
$(
Self::[< $($event:camel)+ >](event, _) => f
.debug_tuple(stringify!([< $($event:camel)+ >]))
.field(&event)
.field(&::std::any::type_name::<Box<dyn FnMut($web_event) + 'static>>())
.finish(),
)*
}
}
}
impl EventHandler for GenericEventHandler {
fn attach<T: DOMEventResponder>(self, target: T) -> T {
match self {
$(
Self::[< $($event:camel)+ >](event, handler) => target.add(event, handler),
)*
}
}
}
$(
impl<F> From<([< $($event)+ >], F)> for GenericEventHandler
where
F: FnMut($web_event) + 'static
{
fn from(value: ([< $($event)+ >], F)) -> Self {
Self::[< $($event:camel)+ >](value.0, Box::new(value.1))
}
}
// NOTE: this could become legal in future and would save us from useless allocations
//impl<F> From<([< $($event)+ >], Box<F>)> for GenericEventHandler
//where
// F: FnMut($web_event) + 'static
//{
// fn from(value: ([< $($event)+ >], Box<F>)) -> Self {
// Self::[< $($event:camel)+ >](value.0, value.1)
// }
//}
impl<F> EventHandler for ([< $($event)+ >], F)
where
F: FnMut($web_event) + 'static
{
fn attach<L: DOMEventResponder>(self, target: L) -> L {
target.add(self.0, self.1)
}
}
)*
}
};
(does_not_bubble) => { false }
@@ -163,36 +385,36 @@ generate_event_types! {
// WindowEventHandlersEventMap
// =========================================================
#[does_not_bubble]
afterprint: Event,
after print: Event,
#[does_not_bubble]
beforeprint: Event,
before print: Event,
#[does_not_bubble]
beforeunload: BeforeUnloadEvent,
before unload: BeforeUnloadEvent,
#[does_not_bubble]
gamepadconnected: GamepadEvent,
gamepad connected: GamepadEvent,
#[does_not_bubble]
gamepaddisconnected: GamepadEvent,
hashchange: HashChangeEvent,
gamepad disconnected: GamepadEvent,
hash change: HashChangeEvent,
#[does_not_bubble]
languagechange: Event,
language change: Event,
#[does_not_bubble]
message: MessageEvent,
#[does_not_bubble]
messageerror: MessageEvent,
message error: MessageEvent,
#[does_not_bubble]
offline: Event,
#[does_not_bubble]
online: Event,
#[does_not_bubble]
pagehide: PageTransitionEvent,
page hide: PageTransitionEvent,
#[does_not_bubble]
pageshow: PageTransitionEvent,
popstate: PopStateEvent,
rejectionhandled: PromiseRejectionEvent,
page show: PageTransitionEvent,
pop state: PopStateEvent,
rejection handled: PromiseRejectionEvent,
#[does_not_bubble]
storage: StorageEvent,
#[does_not_bubble]
unhandledrejection: PromiseRejectionEvent,
unhandled rejection: PromiseRejectionEvent,
#[does_not_bubble]
unload: Event,
@@ -201,38 +423,38 @@ generate_event_types! {
// =========================================================
#[does_not_bubble]
abort: UiEvent,
animationcancel: AnimationEvent,
animationend: AnimationEvent,
animationiteration: AnimationEvent,
animationstart: AnimationEvent,
auxclick: MouseEvent,
beforeinput: InputEvent,
animation cancel: AnimationEvent,
animation end: AnimationEvent,
animation iteration: AnimationEvent,
animation start: AnimationEvent,
aux click: MouseEvent,
before input: InputEvent,
#[does_not_bubble]
blur: FocusEvent,
#[does_not_bubble]
canplay: Event,
can play: Event,
#[does_not_bubble]
canplaythrough: Event,
can play through: Event,
change: Event,
click: MouseEvent,
#[does_not_bubble]
close: Event,
compositionend: CompositionEvent,
compositionstart: CompositionEvent,
compositionupdate: CompositionEvent,
contextmenu: MouseEvent,
composition end: CompositionEvent,
composition start: CompositionEvent,
composition update: CompositionEvent,
context menu: MouseEvent,
#[does_not_bubble]
cuechange: Event,
dblclick: MouseEvent,
cue change: Event,
dbl click: MouseEvent,
drag: DragEvent,
dragend: DragEvent,
dragenter: DragEvent,
dragleave: DragEvent,
dragover: DragEvent,
dragstart: DragEvent,
drag end: DragEvent,
drag enter: DragEvent,
drag leave: DragEvent,
drag over: DragEvent,
drag start: DragEvent,
drop: DragEvent,
#[does_not_bubble]
durationchange: Event,
duration change: Event,
#[does_not_bubble]
emptied: Event,
#[does_not_bubble]
@@ -242,110 +464,110 @@ generate_event_types! {
#[does_not_bubble]
focus: FocusEvent,
#[does_not_bubble]
focusin: FocusEvent,
focus in: FocusEvent,
#[does_not_bubble]
focusout: FocusEvent,
formdata: Event, // web_sys does not include `FormDataEvent`
focus out: FocusEvent,
form data: Event, // web_sys does not include `FormDataEvent`
#[does_not_bubble]
gotpointercapture: PointerEvent,
got pointer capture: PointerEvent,
input: Event,
#[does_not_bubble]
invalid: Event,
keydown: KeyboardEvent,
keypress: KeyboardEvent,
keyup: KeyboardEvent,
key down: KeyboardEvent,
key press: KeyboardEvent,
key up: KeyboardEvent,
#[does_not_bubble]
load: Event,
#[does_not_bubble]
loadeddata: Event,
loaded data: Event,
#[does_not_bubble]
loadedmetadata: Event,
loaded metadata: Event,
#[does_not_bubble]
loadstart: Event,
lostpointercapture: PointerEvent,
mousedown: MouseEvent,
load start: Event,
lost pointer capture: PointerEvent,
mouse down: MouseEvent,
#[does_not_bubble]
mouseenter: MouseEvent,
mouse enter: MouseEvent,
#[does_not_bubble]
mouseleave: MouseEvent,
mousemove: MouseEvent,
mouseout: MouseEvent,
mouseover: MouseEvent,
mouseup: MouseEvent,
mouse leave: MouseEvent,
mouse move: MouseEvent,
mouse out: MouseEvent,
mouse over: MouseEvent,
mouse up: MouseEvent,
#[does_not_bubble]
pause: Event,
#[does_not_bubble]
play: Event,
#[does_not_bubble]
playing: Event,
pointercancel: PointerEvent,
pointerdown: PointerEvent,
pointer cancel: PointerEvent,
pointer down: PointerEvent,
#[does_not_bubble]
pointerenter: PointerEvent,
pointer enter: PointerEvent,
#[does_not_bubble]
pointerleave: PointerEvent,
pointermove: PointerEvent,
pointerout: PointerEvent,
pointerover: PointerEvent,
pointerup: PointerEvent,
pointer leave: PointerEvent,
pointer move: PointerEvent,
pointer out: PointerEvent,
pointer over: PointerEvent,
pointer up: PointerEvent,
#[does_not_bubble]
progress: ProgressEvent,
#[does_not_bubble]
ratechange: Event,
rate change: Event,
reset: Event,
#[does_not_bubble]
resize: UiEvent,
#[does_not_bubble]
scroll: Event,
#[does_not_bubble]
scrollend: Event,
securitypolicyviolation: SecurityPolicyViolationEvent,
scroll end: Event,
security policy violation: SecurityPolicyViolationEvent,
#[does_not_bubble]
seeked: Event,
#[does_not_bubble]
seeking: Event,
select: Event,
#[does_not_bubble]
selectionchange: Event,
selectstart: Event,
slotchange: Event,
selection change: Event,
select start: Event,
slot change: Event,
#[does_not_bubble]
stalled: Event,
submit: SubmitEvent,
#[does_not_bubble]
suspend: Event,
#[does_not_bubble]
timeupdate: Event,
time update: Event,
#[does_not_bubble]
toggle: Event,
touchcancel: TouchEvent,
touchend: TouchEvent,
touchmove: TouchEvent,
touchstart: TouchEvent,
transitioncancel: TransitionEvent,
transitionend: TransitionEvent,
transitionrun: TransitionEvent,
transitionstart: TransitionEvent,
touch cancel: TouchEvent,
touch end: TouchEvent,
touch move: TouchEvent,
touch start: TouchEvent,
transition cancel: TransitionEvent,
transition end: TransitionEvent,
transition run: TransitionEvent,
transition start: TransitionEvent,
#[does_not_bubble]
volumechange: Event,
volume change: Event,
#[does_not_bubble]
waiting: Event,
webkitanimationend: Event,
webkitanimationiteration: Event,
webkitanimationstart: Event,
webkittransitionend: Event,
webkit animation end: Event,
webkit animation iteration: Event,
webkit animation start: Event,
webkit transition end: Event,
wheel: WheelEvent,
// =========================================================
// WindowEventMap
// =========================================================
DOMContentLoaded: Event,
D O M Content Loaded: Event, // Hack for correct casing
#[does_not_bubble]
devicemotion: DeviceMotionEvent,
device motion: DeviceMotionEvent,
#[does_not_bubble]
deviceorientation: DeviceOrientationEvent,
device orientation: DeviceOrientationEvent,
#[does_not_bubble]
orientationchange: Event,
orientation change: Event,
// =========================================================
// DocumentAndElementEventHandlersEventMap
@@ -357,13 +579,13 @@ generate_event_types! {
// =========================================================
// DocumentEventMap
// =========================================================
fullscreenchange: Event,
fullscreenerror: Event,
pointerlockchange: Event,
pointerlockerror: Event,
fullscreen change: Event,
fullscreen error: Event,
pointer lock change: Event,
pointer lock error: Event,
#[does_not_bubble]
readystatechange: Event,
visibilitychange: Event,
ready state change: Event,
visibility change: Event,
}
// Export `web_sys` event types
@@ -371,7 +593,7 @@ pub use web_sys::{
AnimationEvent, BeforeUnloadEvent, CompositionEvent, CustomEvent,
DeviceMotionEvent, DeviceOrientationEvent, DragEvent, ErrorEvent, Event,
FocusEvent, GamepadEvent, HashChangeEvent, InputEvent, KeyboardEvent,
MouseEvent, PageTransitionEvent, PointerEvent, PopStateEvent,
MessageEvent, MouseEvent, PageTransitionEvent, PointerEvent, PopStateEvent,
ProgressEvent, PromiseRejectionEvent, SecurityPolicyViolationEvent,
StorageEvent, SubmitEvent, TouchEvent, TransitionEvent, UiEvent,
WheelEvent,

View File

@@ -28,9 +28,9 @@ use cfg_if::cfg_if;
pub use components::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub use events::add_event_helper;
pub use events::typed as ev;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use events::{add_event_listener, add_event_listener_undelegated};
pub use events::{typed as ev, typed::EventHandler};
pub use html::HtmlElement;
use html::{AnyElement, ElementDescriptor};
pub use hydration::{HydrationCtx, HydrationKey};

View File

@@ -1,3 +1,7 @@
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
use std::{borrow::Cow, rc::Rc};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::UnwrapThrowExt;
@@ -263,6 +267,41 @@ macro_rules! attr_type {
};
}
macro_rules! attr_signal_type {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoAttribute for $signal_type
where
T: IntoAttribute + Clone,
{
fn into_attribute(self) -> Attribute {
let modified_fn = Rc::new(move || self.get().into_attribute());
Attribute::Fn(modified_fn)
}
impl_into_attr_boxed! {}
}
};
}
macro_rules! attr_signal_type_optional {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoAttribute for $signal_type
where
T: Clone,
Option<T>: IntoAttribute,
{
fn into_attribute(self) -> Attribute {
let modified_fn = Rc::new(move || self.get().into_attribute());
Attribute::Fn(modified_fn)
}
impl_into_attr_boxed! {}
}
};
}
attr_type!(&String);
attr_type!(usize);
attr_type!(u8);
@@ -280,6 +319,13 @@ attr_type!(f32);
attr_type!(f64);
attr_type!(char);
attr_signal_type!(ReadSignal<T>);
attr_signal_type!(RwSignal<T>);
attr_signal_type!(Memo<T>);
attr_signal_type!(Signal<T>);
attr_signal_type!(MaybeSignal<T>);
attr_signal_type_optional!(MaybeProp<T>);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]

View File

@@ -1,3 +1,8 @@
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
/// Represents the different possible values a single class on an element could have,
/// allowing you to do fine-grained updates to single items
/// in [`Element.classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList).
@@ -113,3 +118,36 @@ pub(crate) fn class_expression(
}
}
}
macro_rules! class_signal_type {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl IntoClass for $signal_type {
#[inline(always)]
fn into_class(self) -> Class {
let modified_fn = Box::new(move || self.get());
Class::Fn(modified_fn)
}
}
};
}
macro_rules! class_signal_type_optional {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl IntoClass for $signal_type {
#[inline(always)]
fn into_class(self) -> Class {
let modified_fn = Box::new(move || self.get().unwrap_or(false));
Class::Fn(modified_fn)
}
}
};
}
class_signal_type!(ReadSignal<bool>);
class_signal_type!(RwSignal<bool>);
class_signal_type!(Memo<bool>);
class_signal_type!(Signal<bool>);
class_signal_type!(MaybeSignal<bool>);
class_signal_type_optional!(MaybeProp<bool>);

View File

@@ -1,3 +1,7 @@
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
use wasm_bindgen::JsValue;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::UnwrapThrowExt;
@@ -52,6 +56,37 @@ macro_rules! prop_type {
};
}
macro_rules! prop_signal_type {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoProperty for $signal_type
where
T: Into<JsValue> + Clone,
{
fn into_property(self) -> Property {
let modified_fn = Box::new(move || self.get().into());
Property::Fn(modified_fn)
}
}
};
}
macro_rules! prop_signal_type_optional {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoProperty for $signal_type
where
T: Clone,
Option<T>: Into<JsValue>,
{
fn into_property(self) -> Property {
let modified_fn = Box::new(move || self.get().into());
Property::Fn(modified_fn)
}
}
};
}
prop_type!(JsValue);
prop_type!(String);
prop_type!(&String);
@@ -72,6 +107,13 @@ prop_type!(f32);
prop_type!(f64);
prop_type!(bool);
prop_signal_type!(ReadSignal<T>);
prop_signal_type!(RwSignal<T>);
prop_signal_type!(Memo<T>);
prop_signal_type!(Signal<T>);
prop_signal_type!(MaybeSignal<T>);
prop_signal_type_optional!(MaybeProp<T>);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::borrow::Cow;

View File

@@ -1,3 +1,7 @@
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
use std::{borrow::Cow, rc::Rc};
/// todo docs
@@ -183,6 +187,37 @@ macro_rules! style_type {
};
}
macro_rules! style_signal_type {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoStyle for $signal_type
where
T: IntoStyle + Clone,
{
fn into_style(self) -> Style {
let modified_fn = Rc::new(move || self.get().into_style());
Style::Fn(modified_fn)
}
}
};
}
macro_rules! style_signal_type_optional {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoStyle for $signal_type
where
T: Clone,
Option<T>: IntoStyle,
{
fn into_style(self) -> Style {
let modified_fn = Rc::new(move || self.get().into_style());
Style::Fn(modified_fn)
}
}
};
}
style_type!(&String);
style_type!(usize);
style_type!(u8);
@@ -199,3 +234,10 @@ style_type!(i128);
style_type!(f32);
style_type!(f64);
style_type!(char);
style_signal_type!(ReadSignal<T>);
style_signal_type!(RwSignal<T>);
style_signal_type!(Memo<T>);
style_signal_type!(Signal<T>);
style_signal_type!(MaybeSignal<T>);
style_signal_type_optional!(MaybeProp<T>);

View File

@@ -23,10 +23,17 @@ pub async fn render_to_string_async(
view: impl FnOnce() -> View + 'static,
) -> String {
let mut buf = String::new();
let mut stream = Box::pin(render_to_stream_in_order(view));
let (stream, runtime) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
view,
|| "".into(),
|| {},
);
let mut stream = Box::pin(stream);
while let Some(chunk) = stream.next().await {
buf.push_str(&chunk);
}
runtime.dispose();
buf
}

View File

@@ -36,12 +36,12 @@ pub fn server_impl(
};
// default to PascalCase version of function name if no struct name given
if args.struct_name.is_none() {
let upper_cammel_case_name = Converter::new()
let upper_camel_case_name = Converter::new()
.from_case(Case::Snake)
.to_case(Case::UpperCamel)
.convert(sig.ident.to_string());
args.struct_name =
Some(Ident::new(&upper_cammel_case_name, sig.ident.span()));
Some(Ident::new(&upper_camel_case_name, sig.ident.span()));
}
// default to "/api" if no prefix given
if args.prefix.is_none() {
@@ -76,7 +76,7 @@ impl ToTokens for ServerFnArgs {
self.struct_name.as_ref().map(|s| quote::quote! { #s, });
let prefix = self.prefix.as_ref().map(|p| quote::quote! { #p, });
let encoding = self.encoding.as_ref().map(|e| quote::quote! { #e, });
let fn_path = self.fn_path.as_ref().map(|f| quote::quote! { #f, });
let fn_path = self.fn_path.as_ref().map(|f| quote::quote! { #f });
tokens.extend(quote::quote! {
#struct_name
#prefix

View File

@@ -1,6 +1,8 @@
#[cfg(debug_assertions)]
use super::ident_from_tag_name;
use super::{
client_builder::{fragment_to_tokens, TagType},
event_from_attribute_node, ident_from_tag_name,
event_from_attribute_node,
};
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{format_ident, quote};

View File

@@ -1,4 +1,4 @@
use crate::{with_runtime, Runtime};
use crate::{node::NodeId, with_runtime, Disposer, Runtime, SignalDispose};
use cfg_if::cfg_if;
use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
@@ -57,21 +57,23 @@ use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
)]
#[track_caller]
#[inline(always)]
pub fn create_effect<T>(f: impl Fn(Option<T>) -> T + 'static)
pub fn create_effect<T>(f: impl Fn(Option<T>) -> T + 'static) -> Effect
where
T: 'static,
{
cfg_if! {
if #[cfg(not(feature = "ssr"))] {
let runtime = Runtime::current();
let e = runtime.create_effect(f);
let id = runtime.create_effect(f);
//crate::macros::debug_warn!("creating effect {e:?}");
_ = with_runtime( |runtime| {
runtime.update_if_necessary(e);
runtime.update_if_necessary(id);
});
Effect { id }
} else {
// clear warnings
_ = f;
Effect::default()
}
}
}
@@ -114,16 +116,19 @@ where
)]
#[track_caller]
#[inline(always)]
pub fn create_isomorphic_effect<T>(f: impl Fn(Option<T>) -> T + 'static)
pub fn create_isomorphic_effect<T>(
f: impl Fn(Option<T>) -> T + 'static,
) -> Effect
where
T: 'static,
{
let runtime = Runtime::current();
let e = runtime.create_effect(f);
let id = runtime.create_effect(f);
//crate::macros::debug_warn!("creating effect {e:?}");
_ = with_runtime(|runtime| {
runtime.update_if_necessary(e);
runtime.update_if_necessary(id);
});
Effect { id }
}
#[doc(hidden)]
@@ -145,7 +150,25 @@ where
create_effect(f);
}
pub(crate) struct Effect<T, F>
/// A handle to an effect, can be used to explicitly dispose of the effect.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Effect {
pub(crate) id: NodeId,
}
impl From<Effect> for Disposer {
fn from(effect: Effect) -> Self {
Disposer(effect.id)
}
}
impl SignalDispose for Effect {
fn dispose(self) {
drop(Disposer::from(self));
}
}
pub(crate) struct EffectState<T, F>
where
T: 'static,
F: Fn(Option<T>) -> T,
@@ -160,7 +183,7 @@ pub(crate) trait AnyComputation {
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool;
}
impl<T, F> AnyComputation for Effect<T, F>
impl<T, F> AnyComputation for EffectState<T, F>
where
T: 'static,
F: Fn(Option<T>) -> T,

View File

@@ -9,6 +9,8 @@ use std::collections::{HashMap, HashSet, VecDeque};
/// Hydration data and other context that is shared between the server
/// and the client.
pub struct SharedContext {
/// Resources that initially needed to resolve from the server.
pub server_resources: HashSet<ResourceId>,
/// Resources that have not yet resolved.
pub pending_resources: HashSet<ResourceId>,
/// Resources that have already resolved.
@@ -201,24 +203,27 @@ impl Default for SharedContext {
let pending_resources: HashSet<ResourceId> = pending_resources
.map_err(|_| ())
.and_then(|pr| serde_wasm_bindgen::from_value(pr).map_err(|_| ()))
.unwrap_or_default();
.unwrap();
let resolved_resources = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOLVED_RESOURCES"),
)
.unwrap_or(wasm_bindgen::JsValue::NULL);
.unwrap(); // unwrap_or(wasm_bindgen::JsValue::NULL);
let resolved_resources =
serde_wasm_bindgen::from_value(resolved_resources).unwrap_or_default();
serde_wasm_bindgen::from_value(resolved_resources).unwrap();
Self {
server_resources: pending_resources.clone(),
pending_resources,
resolved_resources,
pending_fragments: Default::default(),
}
} else {
Self {
server_resources: Default::default(),
pending_resources: Default::default(),
resolved_resources: Default::default(),
pending_fragments: Default::default(),

View File

@@ -206,7 +206,9 @@ fn forward_ref_to<T, O, F: FnOnce(&T) -> O>(
}
}
impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
impl<T: Clone> SignalGetUntracked for Memo<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -257,7 +259,9 @@ impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
}
}
impl<T> SignalWithUntracked<T> for Memo<T> {
impl<T> SignalWithUntracked for Memo<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -324,7 +328,9 @@ impl<T> SignalWithUntracked<T> for Memo<T> {
/// # runtime.dispose();
/// #
/// ```
impl<T: Clone> SignalGet<T> for Memo<T> {
impl<T: Clone> SignalGet for Memo<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -364,7 +370,9 @@ impl<T: Clone> SignalGet<T> for Memo<T> {
}
}
impl<T> SignalWith<T> for Memo<T> {
impl<T> SignalWith for Memo<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(

View File

@@ -1,10 +1,12 @@
#[cfg(debug_assertions)]
use crate::SpecialNonReactiveZone;
use crate::{
create_effect, create_isomorphic_effect, create_memo, create_signal,
queue_microtask, runtime::with_runtime, serialization::Serializable,
signal_prelude::format_signal_warning, spawn::spawn_local, use_context,
GlobalSuspenseContext, Memo, ReadSignal, ScopeProperty, SignalDispose,
SignalGet, SignalGetUntracked, SignalSet, SignalUpdate, SignalWith,
SpecialNonReactiveZone, SuspenseContext, WriteSignal,
GlobalSuspenseContext, Memo, ReadSignal, ScopeProperty, Signal,
SignalDispose, SignalGet, SignalGetUntracked, SignalSet, SignalUpdate,
SignalWith, SuspenseContext, WriteSignal,
};
use std::{
any::Any,
@@ -503,16 +505,52 @@ where
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn loading(&self) -> ReadSignal<bool> {
with_runtime(|runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.loading
})
pub fn loading(&self) -> Signal<bool> {
#[allow(unused_variables)]
let (loading, is_from_server) = with_runtime(|runtime| {
let loading = runtime
.resource(self.id, |resource: &ResourceState<S, T>| {
resource.loading
});
#[cfg(feature = "hydrate")]
let is_from_server = runtime
.shared_context
.borrow()
.server_resources
.contains(&self.id);
#[cfg(not(feature = "hydrate"))]
let is_from_server = false;
(loading, is_from_server)
})
.expect(
"tried to call Resource::loading() in a runtime that has already \
been disposed.",
)
);
#[cfg(feature = "hydrate")]
{
// if the loading signal is read outside Suspense
// in hydrate mode, there will be a mismatch on first render
// unless we delay a tick
let (initial, set_initial) = create_signal(true);
queue_microtask(move || set_initial.set(false));
Signal::derive(move || {
if is_from_server
&& initial.get()
&& use_context::<SuspenseContext>().is_none()
{
true
} else {
loading.get()
}
})
}
#[cfg(not(feature = "hydrate"))]
{
loading.into()
}
}
/// Re-runs the async function with the current source data.
@@ -592,7 +630,9 @@ where
}
}
impl<S, T> SignalUpdate<Option<T>> for Resource<S, T> {
impl<S, T> SignalUpdate for Resource<S, T> {
type Value = Option<T>;
#[cfg_attr(
debug_assertions,
instrument(
@@ -648,11 +688,13 @@ impl<S, T> SignalUpdate<Option<T>> for Resource<S, T> {
}
}
impl<S, T> SignalWith<Option<T>> for Resource<S, T>
impl<S, T> SignalWith for Resource<S, T>
where
S: Clone,
T: Clone,
{
type Value = Option<T>;
#[cfg_attr(
debug_assertions,
instrument(
@@ -714,11 +756,13 @@ where
}
}
impl<S, T> SignalGet<Option<T>> for Resource<S, T>
impl<S, T> SignalGet for Resource<S, T>
where
S: Clone,
T: Clone,
{
type Value = Option<T>;
#[cfg_attr(
debug_assertions,
instrument(
@@ -751,6 +795,7 @@ where
)
)]
#[inline(always)]
#[track_caller]
fn try_get(&self) -> Option<Option<T>> {
let location = std::panic::Location::caller();
with_runtime(|runtime| {
@@ -762,7 +807,9 @@ where
}
}
impl<S, T> SignalSet<T> for Resource<S, T> {
impl<S, T> SignalSet for Resource<S, T> {
type Value = T;
#[cfg_attr(
debug_assertions,
instrument(
@@ -1248,3 +1295,29 @@ where
}
}
}
#[cfg(feature = "nightly")]
impl<S: Clone, T: Clone> FnOnce<()> for Resource<S, T> {
type Output = Option<T>;
#[inline(always)]
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
}
}
#[cfg(feature = "nightly")]
impl<S: Clone, T: Clone> FnMut<()> for Resource<S, T> {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
}
}
#[cfg(feature = "nightly")]
impl<S: Clone, T: Clone> Fn<()> for Resource<S, T> {
#[inline(always)]
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
}
}

View File

@@ -1,12 +1,13 @@
#[cfg(debug_assertions)]
use crate::SpecialNonReactiveZone;
use crate::{
hydration::SharedContext,
node::{
Disposer, NodeId, ReactiveNode, ReactiveNodeState, ReactiveNodeType,
},
AnyComputation, AnyResource, Effect, Memo, MemoState, ReadSignal,
ResourceId, ResourceState, RwSignal, SerializableResource,
SpecialNonReactiveZone, StoredValueId, Trigger, UnserializableResource,
WriteSignal,
AnyComputation, AnyResource, EffectState, Memo, MemoState, ReadSignal,
ResourceId, ResourceState, RwSignal, SerializableResource, StoredValueId,
Trigger, UnserializableResource, WriteSignal,
};
use cfg_if::cfg_if;
use core::hash::BuildHasherDefault;
@@ -771,7 +772,7 @@ impl RuntimeId {
pub(crate) fn untrack<T>(
self,
f: impl FnOnce() -> T,
diagnostics: bool,
#[allow(unused)] diagnostics: bool,
) -> T {
with_runtime(|runtime| {
let untracked_result;
@@ -938,7 +939,7 @@ impl RuntimeId {
{
self.create_concrete_effect(
Rc::new(RefCell::new(None::<T>)),
Rc::new(Effect {
Rc::new(EffectState {
f,
ty: PhantomData,
#[cfg(any(debug_assertions, feature = "ssr"))]
@@ -1002,7 +1003,7 @@ impl RuntimeId {
let id = self.create_concrete_effect(
Rc::new(RefCell::new(None::<()>)),
Rc::new(Effect {
Rc::new(EffectState {
f: effect_fn,
ty: PhantomData,
#[cfg(any(debug_assertions, feature = "ssr"))]

View File

@@ -105,35 +105,41 @@ pub mod prelude {
/// This trait allows getting an owned value of the signals
/// inner type.
pub trait SignalGet<T> {
pub trait SignalGet {
/// The value held by the signal.
type Value;
/// Clones and returns the current value of the signal, and subscribes
/// the running effect to this signal.
///
/// # Panics
/// Panics if you try to access a signal that is owned by a reactive node that has been disposed.
#[track_caller]
fn get(&self) -> T;
fn get(&self) -> Self::Value;
/// Clones and returns the signal value, returning [`Some`] if the signal
/// is still alive, and [`None`] otherwise.
fn try_get(&self) -> Option<T>;
fn try_get(&self) -> Option<Self::Value>;
}
/// This trait allows obtaining an immutable reference to the signal's
/// inner type.
pub trait SignalWith<T> {
pub trait SignalWith {
/// The value held by the signal.
type Value;
/// Applies a function to the current value of the signal, and subscribes
/// the running effect to this signal.
///
/// # Panics
/// Panics if you try to access a signal that is owned by a reactive node that has been disposed.
#[track_caller]
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O;
fn with<O>(&self, f: impl FnOnce(&Self::Value) -> O) -> O;
/// Applies a function to the current value of the signal, and subscribes
/// the running effect to this signal. Returns [`Some`] if the signal is
/// valid and the function ran, otherwise returns [`None`].
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O>;
fn try_with<O>(&self, f: impl FnOnce(&Self::Value) -> O) -> Option<O>;
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
fn track(&self) {
@@ -142,31 +148,37 @@ pub trait SignalWith<T> {
}
/// This trait allows setting the value of a signal.
pub trait SignalSet<T> {
pub trait SignalSet {
/// The value held by the signal.
type Value;
/// Sets the signals value and notifies subscribers.
///
/// **Note:** `set()` does not auto-memoize, i.e., it will notify subscribers
/// even if the value has not actually changed.
#[track_caller]
fn set(&self, new_value: T);
fn set(&self, new_value: Self::Value);
/// Sets the signals value and notifies subscribers. Returns [`None`]
/// if the signal is still valid, [`Some(T)`] otherwise.
///
/// **Note:** `set()` does not auto-memoize, i.e., it will notify subscribers
/// even if the value has not actually changed.
fn try_set(&self, new_value: T) -> Option<T>;
fn try_set(&self, new_value: Self::Value) -> Option<Self::Value>;
}
/// This trait allows updating the inner value of a signal.
pub trait SignalUpdate<T> {
pub trait SignalUpdate {
/// The value held by the signal.
type Value;
/// Applies a function to the current value to mutate it in place
/// and notifies subscribers that the signal has changed.
///
/// **Note:** `update()` does not auto-memoize, i.e., it will notify subscribers
/// even if the value has not actually changed.
#[track_caller]
fn update(&self, f: impl FnOnce(&mut T));
fn update(&self, f: impl FnOnce(&mut Self::Value));
/// Applies a function to the current value to mutate it in place
/// and notifies subscribers that the signal has changed. Returns
@@ -174,45 +186,55 @@ pub trait SignalUpdate<T> {
///
/// **Note:** `update()` does not auto-memoize, i.e., it will notify subscribers
/// even if the value has not actually changed.
fn try_update<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O>;
fn try_update<O>(&self, f: impl FnOnce(&mut Self::Value) -> O)
-> Option<O>;
}
/// Trait implemented for all signal types which you can `get` a value
/// from, such as [`ReadSignal`],
/// [`Memo`](crate::Memo), etc., which allows getting the inner value without
/// subscribing to the current scope.
pub trait SignalGetUntracked<T> {
pub trait SignalGetUntracked {
/// The value held by the signal.
type Value;
/// Gets the signal's value without creating a dependency on the
/// current scope.
///
/// # Panics
/// Panics if you try to access a signal that is owned by a reactive node that has been disposed.
#[track_caller]
fn get_untracked(&self) -> T;
fn get_untracked(&self) -> Self::Value;
/// Gets the signal's value without creating a dependency on the
/// current scope. Returns [`Some(T)`] if the signal is still
/// valid, [`None`] otherwise.
fn try_get_untracked(&self) -> Option<T>;
fn try_get_untracked(&self) -> Option<Self::Value>;
}
/// This trait allows getting a reference to the signals inner value
/// without creating a dependency on the signal.
pub trait SignalWithUntracked<T> {
pub trait SignalWithUntracked {
/// The value held by the signal.
type Value;
/// Runs the provided closure with a reference to the current
/// value without creating a dependency on the current scope.
///
/// # Panics
/// Panics if you try to access a signal that is owned by a reactive node that has been disposed.
#[track_caller]
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O;
fn with_untracked<O>(&self, f: impl FnOnce(&Self::Value) -> O) -> O;
/// Runs the provided closure with a reference to the current
/// value without creating a dependency on the current scope.
/// Returns [`Some(O)`] if the signal is still valid, [`None`]
/// otherwise.
#[track_caller]
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O>;
fn try_with_untracked<O>(
&self,
f: impl FnOnce(&Self::Value) -> O,
) -> Option<O>;
}
/// Trait implemented for all signal types which you can `set` the inner
@@ -419,7 +441,9 @@ where
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl<T: Clone> SignalGetUntracked<T> for ReadSignal<T> {
impl<T: Clone> SignalGetUntracked for ReadSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -470,7 +494,9 @@ impl<T: Clone> SignalGetUntracked<T> for ReadSignal<T> {
}
}
impl<T> SignalWithUntracked<T> for ReadSignal<T> {
impl<T> SignalWithUntracked for ReadSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -533,7 +559,9 @@ impl<T> SignalWithUntracked<T> for ReadSignal<T> {
/// assert_eq!(first_char(), 'B');
/// # runtime.dispose();
/// ```
impl<T> SignalWith<T> for ReadSignal<T> {
impl<T> SignalWith for ReadSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -600,7 +628,9 @@ impl<T> SignalWith<T> for ReadSignal<T> {
/// // assert_eq!(count.get(), 0);
/// # runtime.dispose();
/// ```
impl<T: Clone> SignalGet<T> for ReadSignal<T> {
impl<T: Clone> SignalGet for ReadSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -928,7 +958,9 @@ impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
/// assert_eq!(count.get(), 1);
/// # runtime.dispose();
/// ```
impl<T> SignalUpdate<T> for WriteSignal<T> {
impl<T> SignalUpdate for WriteSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -1000,7 +1032,9 @@ impl<T> SignalUpdate<T> for WriteSignal<T> {
/// assert_eq!(count.get(), 1);
/// # runtime.dispose();
/// ```
impl<T> SignalSet<T> for WriteSignal<T> {
impl<T> SignalSet for WriteSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -1217,7 +1251,9 @@ impl<T> From<T> for RwSignal<T> {
}
}
impl<T: Clone> SignalGetUntracked<T> for RwSignal<T> {
impl<T: Clone> SignalGetUntracked for RwSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -1279,7 +1315,9 @@ impl<T: Clone> SignalGetUntracked<T> for RwSignal<T> {
}
}
impl<T> SignalWithUntracked<T> for RwSignal<T> {
impl<T> SignalWithUntracked for RwSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -1456,7 +1494,9 @@ impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
/// # runtime.dispose();
/// #
/// ```
impl<T> SignalWith<T> for RwSignal<T> {
impl<T> SignalWith for RwSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -1524,7 +1564,9 @@ impl<T> SignalWith<T> for RwSignal<T> {
/// # runtime.dispose();
/// #
/// ```
impl<T: Clone> SignalGet<T> for RwSignal<T> {
impl<T: Clone> SignalGet for RwSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -1604,7 +1646,9 @@ impl<T: Clone> SignalGet<T> for RwSignal<T> {
/// assert_eq!(count.get(), 1);
/// # runtime.dispose();
/// ```
impl<T> SignalUpdate<T> for RwSignal<T> {
impl<T> SignalUpdate for RwSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -1671,7 +1715,9 @@ impl<T> SignalUpdate<T> for RwSignal<T> {
/// assert_eq!(count.get(), 1);
/// # runtime.dispose();
/// ```
impl<T> SignalSet<T> for RwSignal<T> {
impl<T> SignalSet for RwSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(

View File

@@ -28,12 +28,12 @@ where
/// function call, `with()`, and `get()` APIs as other signals.
///
/// ## Core Trait Implementations
/// - [`.get()`](#impl-SignalGet<T>-for-Signal<T>) (or calling the signal as a function) clones the current
/// - [`.get()`](#impl-SignalGet-for-Signal<T>) (or calling the signal as a function) clones the current
/// value of the signal. If you call it within an effect, it will cause that effect
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-Signal<T>) clones the value of the signal
/// without reactively tracking it.
/// - [`.with()`](#impl-SignalWith<T>-for-Signal<T>) allows you to reactively access the signals value without
/// - [`.with()`](#impl-SignalWith-for-Signal<T>) allows you to reactively access the signals value without
/// cloning by applying a callback function.
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-Signal<T>) allows you to access the signals
/// value without reactively tracking it.
@@ -97,7 +97,9 @@ impl<T> PartialEq for Signal<T> {
/// Please note that using `Signal::with_untracked` still clones the inner value,
/// so there's no benefit to using it as opposed to calling
/// `Signal::get_untracked`.
impl<T: Clone> SignalGetUntracked<T> for Signal<T> {
impl<T: Clone> SignalGetUntracked for Signal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -141,7 +143,9 @@ impl<T: Clone> SignalGetUntracked<T> for Signal<T> {
}
}
impl<T> SignalWithUntracked<T> for Signal<T> {
impl<T> SignalWithUntracked for Signal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -218,7 +222,9 @@ impl<T> SignalWithUntracked<T> for Signal<T> {
/// assert_eq!(memoized_lower.get(), "alice");
/// # runtime.dispose();
/// ```
impl<T> SignalWith<T> for Signal<T> {
impl<T> SignalWith for Signal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -280,7 +286,9 @@ impl<T> SignalWith<T> for Signal<T> {
/// assert_eq!(above_3(&memoized_double_count.into()), true);
/// # runtime.dispose();
/// ```
impl<T: Clone> SignalGet<T> for Signal<T> {
impl<T: Clone> SignalGet for Signal<T> {
type Value = T;
fn get(&self) -> T {
match self.inner {
SignalTypes::ReadSignal(r) => r.get(),
@@ -475,12 +483,12 @@ impl<T> Eq for SignalTypes<T> where T: PartialEq {}
/// of the same type. This is especially useful for component properties.
///
/// ## Core Trait Implementations
/// - [`.get()`](#impl-SignalGet<T>-for-MaybeSignal<T>) (or calling the signal as a function) clones the current
/// - [`.get()`](#impl-SignalGet-for-MaybeSignal<T>) (or calling the signal as a function) clones the current
/// value of the signal. If you call it within an effect, it will cause that effect
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-MaybeSignal<T>) clones the value of the signal
/// without reactively tracking it.
/// - [`.with()`](#impl-SignalWith<T>-for-MaybeSignal<T>) allows you to reactively access the signals value without
/// - [`.with()`](#impl-SignalWith-for-MaybeSignal<T>) allows you to reactively access the signals value without
/// cloning by applying a callback function.
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-MaybeSignal<T>) allows you to access the signals
/// value without reactively tracking it.
@@ -557,7 +565,9 @@ impl<T: Default> Default for MaybeSignal<T> {
/// assert_eq!(above_3(&static_value.into()), true);
/// # runtime.dispose();
/// ```
impl<T: Clone> SignalGet<T> for MaybeSignal<T> {
impl<T: Clone> SignalGet for MaybeSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -624,7 +634,9 @@ impl<T: Clone> SignalGet<T> for MaybeSignal<T> {
/// assert_eq!(static_value.get(), "Bob");
/// # runtime.dispose();
/// ```
impl<T> SignalWith<T> for MaybeSignal<T> {
impl<T> SignalWith for MaybeSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -658,7 +670,9 @@ impl<T> SignalWith<T> for MaybeSignal<T> {
}
}
impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
impl<T> SignalWithUntracked for MaybeSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -692,7 +706,9 @@ impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
}
}
impl<T: Clone> SignalGetUntracked<T> for MaybeSignal<T> {
impl<T: Clone> SignalGetUntracked for MaybeSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -831,12 +847,12 @@ impl From<&str> for MaybeSignal<String> {
/// This creates an extremely flexible type for component libraries, etc.
///
/// ## Core Trait Implementations
/// - [`.get()`](#impl-SignalGet<T>-for-MaybeProp<T>) (or calling the signal as a function) clones the current
/// - [`.get()`](#impl-SignalGet-for-MaybeProp<T>) (or calling the signal as a function) clones the current
/// value of the signal. If you call it within an effect, it will cause that effect
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-MaybeProp<T>) clones the value of the signal
/// without reactively tracking it.
/// - [`.with()`](#impl-SignalWith<T>-for-MaybeProp<T>) allows you to reactively access the signals value without
/// - [`.with()`](#impl-SignalWith-for-MaybeProp<T>) allows you to reactively access the signals value without
/// cloning by applying a callback function.
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-MaybeProp<T>) allows you to access the signals
/// value without reactively tracking it.
@@ -902,7 +918,9 @@ impl<T> Default for MaybeProp<T> {
/// assert_eq!(above_3(&memoized_double_count.into()), true);
/// # runtime.dispose();
/// ```
impl<T: Clone> SignalGet<Option<T>> for MaybeProp<T> {
impl<T: Clone> SignalGet for MaybeProp<T> {
type Value = Option<T>;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
@@ -1042,7 +1060,9 @@ impl<T> MaybeProp<T> {
}
}
impl<T: Clone> SignalGetUntracked<Option<T>> for MaybeProp<T> {
impl<T: Clone> SignalGetUntracked for MaybeProp<T> {
type Value = Option<T>;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(

View File

@@ -77,7 +77,9 @@ impl<T: Default + 'static> Default for SignalSetter<T> {
impl<T> Copy for SignalSetter<T> {}
impl<T> SignalSet<T> for SignalSetter<T> {
impl<T> SignalSet for SignalSetter<T> {
type Value = T;
fn set(&self, new_value: T) {
match self.inner {
SignalSetterTypes::Default => {}

View File

@@ -77,7 +77,7 @@ impl SuspenseContext {
if pending_resources.get() == 0 {
_ = tx.borrow_mut().try_send(());
}
})
});
});
async move {
rx.next().await;

View File

@@ -97,7 +97,9 @@ pub fn create_trigger() -> Trigger {
Runtime::current().create_trigger()
}
impl SignalGet<()> for Trigger {
impl SignalGet for Trigger {
type Value = ();
#[cfg_attr(
debug_assertions,
instrument(
@@ -134,7 +136,9 @@ impl SignalGet<()> for Trigger {
}
}
impl SignalUpdate<()> for Trigger {
impl SignalUpdate for Trigger {
type Value = ();
#[cfg_attr(
debug_assertions,
instrument(
@@ -181,7 +185,9 @@ impl SignalUpdate<()> for Trigger {
}
}
impl SignalSet<()> for Trigger {
impl SignalSet for Trigger {
type Value = ();
#[cfg_attr(
debug_assertions,
instrument(

View File

@@ -190,6 +190,7 @@ pub fn use_navigate() -> impl Fn(&str, NavigateOptions) {
let to = to.to_string();
if cfg!(any(feature = "csr", feature = "hydrate")) {
request_animation_frame(move || {
#[allow(unused_variables)]
if let Err(e) = router.navigate_from_route(&to, &options) {
leptos::debug_warn!("use_navigate error: {e:?}");
}