mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-29 07:12:33 -05:00
Compare commits
18 Commits
server-fn-
...
1457
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f067dcde7 | ||
|
|
ad6eb58fe1 | ||
|
|
3f3ab1c3c8 | ||
|
|
9adae32847 | ||
|
|
b8098e7992 | ||
|
|
bef4d0dd3b | ||
|
|
a789100e22 | ||
|
|
abeca70625 | ||
|
|
cc293b1170 | ||
|
|
8ab62c17c6 | ||
|
|
cf14e857ca | ||
|
|
c322ef38fd | ||
|
|
c9cc493063 | ||
|
|
fb48f7f117 | ||
|
|
c344e54cf6 | ||
|
|
7306ecccbc | ||
|
|
b98174db7a | ||
|
|
e48f66694d |
1
.github/workflows/check-examples.yml
vendored
1
.github/workflows/check-examples.yml
vendored
@@ -26,3 +26,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "check"
|
||||
toolchain: nightly
|
||||
|
||||
47
.github/workflows/check-stable.yml
vendored
47
.github/workflows/check-stable.yml
vendored
@@ -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
|
||||
|
||||
1
.github/workflows/ci-changed-examples.yml
vendored
1
.github/workflows/ci-changed-examples.yml
vendored
@@ -29,3 +29,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -41,3 +41,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
15
.github/workflows/run-cargo-make-task.yml
vendored
15
.github/workflows/run-cargo-make-task.yml
vendored
@@ -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
|
||||
|
||||
|
||||
1
.github/workflows/verify-all-examples.yml
vendored
1
.github/workflows/verify-all-examples.yml
vendored
@@ -23,3 +23,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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" },
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
106
examples/counters_stable/src/lib.rs
Normal file
106
examples/counters_stable/src/lib.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
17
examples/counters_stable/tests/web/add_1k_counters.rs
Normal file
17
examples/counters_stable/tests/web/add_1k_counters.rs
Normal 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);
|
||||
}
|
||||
17
examples/counters_stable/tests/web/add_counter.rs
Normal file
17
examples/counters_stable/tests/web/add_counter.rs
Normal 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);
|
||||
}
|
||||
19
examples/counters_stable/tests/web/clear_counters.rs
Normal file
19
examples/counters_stable/tests/web/clear_counters.rs
Normal 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);
|
||||
}
|
||||
18
examples/counters_stable/tests/web/decrement_counter.rs
Normal file
18
examples/counters_stable/tests/web/decrement_counter.rs
Normal 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);
|
||||
}
|
||||
112
examples/counters_stable/tests/web/fixtures/counters_page.rs
Normal file
112
examples/counters_stable/tests/web/fixtures/counters_page.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
1
examples/counters_stable/tests/web/fixtures/mod.rs
Normal file
1
examples/counters_stable/tests/web/fixtures/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod counters_page;
|
||||
18
examples/counters_stable/tests/web/increment_counter.rs
Normal file
18
examples/counters_stable/tests/web/increment_counter.rs
Normal 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);
|
||||
}
|
||||
16
examples/counters_stable/tests/web/main.rs
Normal file
16
examples/counters_stable/tests/web/main.rs
Normal 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);
|
||||
18
examples/counters_stable/tests/web/remove_counter.rs
Normal file
18
examples/counters_stable/tests/web/remove_counter.rs
Normal 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);
|
||||
}
|
||||
22
examples/counters_stable/tests/web/view_counters.rs
Normal file
22
examples/counters_stable/tests/web/view_counters.rs
Normal 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)");
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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>);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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 signal’s 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 signal’s 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(
|
||||
|
||||
@@ -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 signal’s value without
|
||||
/// - [`.with()`](#impl-SignalWith-for-Signal<T>) allows you to reactively access the signal’s value without
|
||||
/// cloning by applying a callback function.
|
||||
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-Signal<T>) allows you to access the signal’s
|
||||
/// 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 signal’s value without
|
||||
/// - [`.with()`](#impl-SignalWith-for-MaybeSignal<T>) allows you to reactively access the signal’s value without
|
||||
/// cloning by applying a callback function.
|
||||
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-MaybeSignal<T>) allows you to access the signal’s
|
||||
/// 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 signal’s value without
|
||||
/// - [`.with()`](#impl-SignalWith-for-MaybeProp<T>) allows you to reactively access the signal’s value without
|
||||
/// cloning by applying a callback function.
|
||||
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-MaybeProp<T>) allows you to access the signal’s
|
||||
/// 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(
|
||||
|
||||
@@ -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 => {}
|
||||
|
||||
@@ -77,7 +77,7 @@ impl SuspenseContext {
|
||||
if pending_resources.get() == 0 {
|
||||
_ = tx.borrow_mut().try_send(());
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
async move {
|
||||
rx.next().await;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user