Merge branch 'main' into msgpack-encoding

This commit is contained in:
Greg Johnston 2022-11-25 14:35:52 -05:00 committed by GitHub
commit 93f68e022f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 1104 additions and 672 deletions

View file

@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0"
leptos = "0.0.18"

View file

@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0"
leptos = "0.0.18"

View file

@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0"
leptos = "0.0.18"

View file

@ -1,7 +1,7 @@
use leptos::*;
fn main() {
run_scope(|cx| {
run_scope(create_runtime(), |cx| {
// signal
let (count, set_count) = create_signal(cx, 1);

View file

@ -27,7 +27,7 @@ But thats _exactly_ how reactive programming works.
```rust
use leptos::*;
run_scope(|cx| {
run_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = move || a() + b();
@ -46,7 +46,7 @@ Hopefully, this makes some intuitive sense. After all, `c` is a closure. Calling
```rust
use leptos::*;
run_scope(|cx| {
run_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = move || a() + b();

View file

@ -7,6 +7,8 @@ fn main() {
mount_to_body(|cx| view! { cx, <Counters/> })
}
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
@ -28,12 +30,14 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
};
let add_many_counters = move |_| {
let mut new_counters = vec![];
for next_id in 0..1000 {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(cx, 0);
new_counters.push((next_id, signal));
}
set_counters.update(|counters| counters.extend(new_counters.iter()));
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
@ -46,7 +50,7 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
"Add Counter"
</button>
<button on:click=add_many_counters>
"Add 1000 Counters"
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"

View file

@ -1,6 +1,8 @@
use leptos::*;
use leptos::{For, ForProps};
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
@ -22,12 +24,14 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
};
let add_many_counters = move |_| {
let mut new_counters = vec![];
for next_id in 0..1000 {
let next_id = next_counter_id();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(cx, 0);
new_counters.push((next_id, signal));
}
set_counters.update(move |counters| counters.extend(new_counters.iter()));
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
@ -40,7 +44,7 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
"Add Counter"
</button>
<button on:click=add_many_counters>
"Add 1000 Counters"
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"

View file

@ -1,5 +1,5 @@
use leptos::*;
use web_sys::Event;
use web_sys::MouseEvent;
// This highlights four different ways that child components can communicate
// with their parent:
@ -71,7 +71,7 @@ pub fn ButtonA(cx: Scope, setter: WriteSignal<bool>) -> Element {
#[component]
pub fn ButtonB<F>(cx: Scope, on_click: F) -> Element
where
F: Fn(Event) + 'static,
F: Fn(MouseEvent) + 'static,
{
view! {
cx,
@ -82,11 +82,11 @@ where
</button>
}
// just a note: in an ordinary function ButtonB could take on_click: impl Fn(Event) + 'static
// just a note: in an ordinary function ButtonB could take on_click: impl Fn(MouseEvent) + 'static
// and save you from typing out the generic
// the component macro actually expands to define a
//
// struct ButtonBProps<F> where F: Fn(Event) + 'static {
// struct ButtonBProps<F> where F: Fn(MouseEvent) + 'static {
// on_click: F
// }
//

View file

@ -136,10 +136,10 @@ pub fn TodoMVC(cx: Scope) -> Element {
});
// Callback to add a todo on pressing the `Enter` key, if the field isn't empty
let add_todo = move |ev: web_sys::Event| {
let add_todo = move |ev: web_sys::KeyboardEvent| {
let target = event_target::<HtmlInputElement>(&ev);
ev.stop_propagation();
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
let key_code = ev.key_code();
if key_code == ENTER_KEY {
let title = event_target_value(&ev);
let title = title.trim();
@ -311,7 +311,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
prop:value={move || todo.title.get()}
on:focusout=move |ev| save(&event_target_value(&ev))
on:keyup={move |ev| {
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
let key_code = ev.key_code();
if key_code == ENTER_KEY {
save(&event_target_value(&ev));
} else if key_code == ESCAPE_KEY {

View file

@ -2,6 +2,10 @@
name = "leptos_actix"
version = "0.0.1"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
description = "Actix integrations for the Leptos web framework."
[dependencies]
actix-web = "4"

View file

@ -50,8 +50,8 @@ pub fn handle_server_fns() -> Route {
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
let body: &[u8] = &body;
// TODO this leaks a runtime once per invocation
let (cx, disposer) = raw_scope_and_disposer();
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
// provide HttpRequest as context in server scope
provide_context(cx, req.clone());
@ -60,6 +60,7 @@ pub fn handle_server_fns() -> Route {
Ok(serialized) => {
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
let mut res: HttpResponseBuilder;
if accept_header.is_some() {

View file

@ -2,6 +2,10 @@
name = "leptos_axum"
version = "0.0.1"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
description = "Axum integrations for the Leptos web framework."
[dependencies]
axum = "0.5"

View file

@ -55,8 +55,8 @@ pub async fn handle_server_fns(
let body: &[u8] = &body;
let res = if let Some(server_fn) = server_fn_by_path(path.as_str()) {
// TODO this leaks a runtime once per invocation
let (cx, disposer) = raw_scope_and_disposer();
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
// provide request as context in server scope
provide_context(cx, Arc::new(req));
@ -65,6 +65,7 @@ pub async fn handle_server_fns(
Ok(serialized) => {
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
// if this is Accept: application/json then send a serialized JSON response
let accept_header =

View file

@ -1,6 +1,6 @@
[package]
name = "leptos"
version = "0.0.17"
version = "0.0.18"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@ -9,11 +9,11 @@ description = "Leptos is a full-stack, isomorphic Rust web framework leveraging
readme = "../README.md"
[dependencies]
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.17" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.17" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.17" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.17" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.17" }
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.18" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.18" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.18" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.18" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.18" }
[features]
default = ["csr", "serde"]

View file

@ -3,9 +3,9 @@
fn simple_ssr_test() {
use leptos_dom::*;
use leptos_macro::view;
use leptos_reactive::{create_scope, create_signal};
use leptos_reactive::{create_runtime, create_scope, create_signal};
_ = create_scope(|cx| {
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 0);
let rendered = view! {
cx,
@ -30,7 +30,7 @@ fn ssr_test_with_components() {
use leptos_core::Prop;
use leptos_dom::*;
use leptos_macro::*;
use leptos_reactive::{create_scope, create_signal, Scope};
use leptos_reactive::{create_runtime, create_scope, create_signal, Scope};
#[component]
fn Counter(cx: Scope, initial_value: i32) -> Element {
@ -45,7 +45,7 @@ fn ssr_test_with_components() {
}
}
_ = create_scope(|cx| {
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
cx,
<div class="counters">
@ -66,9 +66,9 @@ fn ssr_test_with_components() {
fn test_classes() {
use leptos_dom::*;
use leptos_macro::view;
use leptos_reactive::{create_scope, create_signal};
use leptos_reactive::{create_runtime, create_scope, create_signal};
_ = create_scope(|cx| {
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 5);
let rendered = view! {
cx,

View file

@ -1,6 +1,6 @@
[package]
name = "leptos_core"
version = "0.0.17"
version = "0.0.18"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@ -8,9 +8,9 @@ repository = "https://github.com/gbj/leptos"
description = "Core functionality for the Leptos web framework."
[dependencies]
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.17" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.17" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.17" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.18" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.18" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.18" }
log = "0.4"
[dev-dependencies]

View file

@ -15,7 +15,7 @@ where
G: Fn(Scope, &T) -> Element,
I: Fn(&T) -> K,
K: Eq + Hash,
T: Eq + 'static,
T: 'static,
{
/// Items over which the component should iterate.
pub each: E,

View file

@ -168,7 +168,7 @@ mod tests {
fn test_map_keyed() {
// we can really only run this in SSR mode, so just ignore if we're in CSR or hydrate
if !cfg!(any(feature = "csr", feature = "hydrate")) {
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (rows, set_rows) =
create_signal::<Vec<(usize, ReadSignal<i32>, WriteSignal<i32>)>>(cx, vec![]);

View file

@ -31,7 +31,7 @@ where
/// # use leptos_core::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// # if cfg!(not(any(feature = "csr", feature = "hydrate", feature = "ssr"))) {
/// async fn fetch_cats(how_many: u32) -> Result<Vec<String>, ()> { Ok(vec![]) }
///

View file

@ -1,6 +1,6 @@
[package]
name = "leptos_dom"
version = "0.0.17"
version = "0.0.18"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@ -12,7 +12,7 @@ cfg-if = "1"
futures = "0.3"
html-escape = "0.2"
js-sys = "0.3"
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.17" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.18" }
serde_json = "1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4.31"
@ -53,14 +53,29 @@ features = [
"Text",
"TreeWalker",
"Window",
# Events we cast to in leptos_macro -- added here so we don't force users to import them
"MouseEvent",
"DragEvent",
"FocusEvent",
"KeyboardEvent",
"ProgressEvent",
"WheelEvent",
"InputEvent",
"SubmitEvent",
"AnimationEvent",
"PointerEvent",
"TouchEvent",
"TransitionEvent"
]
[dev-dependencies]
leptos = { path = "../leptos", default-features = false, version = "0.0" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0" }
[features]
csr = ["leptos_reactive/csr", "leptos_macro/csr"]
hydrate = ["leptos_reactive/hydrate", "leptos_macro/hydrate"]
ssr = ["leptos_reactive/ssr", "leptos_macro/ssr"]
stable = ["leptos_reactive/stable", "leptos_macro/stable"]
csr = ["leptos_reactive/csr", "leptos_macro/csr", "leptos/csr"]
hydrate = ["leptos_reactive/hydrate", "leptos_macro/hydrate", "leptos/hydrate"]
ssr = ["leptos_reactive/ssr", "leptos_macro/ssr", "leptos/ssr"]
stable = ["leptos_reactive/stable", "leptos_macro/stable", "leptos/stable"]

View file

@ -140,3 +140,15 @@ macro_rules! is_dev {
cfg!(debug_assertions)
};
}
#[doc(hidden)]
pub fn __leptos_renderer_error(expected: &'static str, location: &'static str) -> web_sys::Node {
cfg_if! {
if #[cfg(debug_assertions)] {
panic!("Yikes! Something went wrong while Leptos was trying to traverse the DOM to set up the reactive system.\n\nThe renderer expected {expected:?} as {location} and couldn't get it.\n\nThis is almost certainly a bug in the framework, not your application. Please open an issue on GitHub and provide example code if possible.\n\nIn the meantime, these bugs are often related to <Component/>s or {{block}}s when they are siblings of each other. Try wrapping those in a <span> or <div> for now. Sorry for the pain!")
} else {
_ = expected;
panic!("Renderer error. You can find a more detailed error message if you compile in debug mode.")
}
}
}

View file

@ -71,11 +71,11 @@ where
F: Fn(Scope) -> T + 'static,
T: Mountable,
{
use leptos_reactive::create_scope;
use leptos_reactive::{create_runtime, create_scope};
// running "mount" intentionally leaks the memory,
// as the "mount" has no parent that can clean it up
let _ = create_scope(move |cx| {
// this is not a leak
// CSR and hydrate mode define a single, thread-local Runtime
let _ = create_scope(create_runtime(), move |cx| {
(f(cx)).mount(&parent);
});
}
@ -100,9 +100,11 @@ where
F: Fn(Scope) -> T + 'static,
T: Mountable,
{
// running "hydrate" intentionally leaks the memory,
// as the "hydrate" has no parent that can clean it up
let _ = leptos_reactive::create_scope(move |cx| {
use leptos_reactive::create_runtime;
// this is not a leak
// CSR and hydrate mode define a single, thread-local Runtime
let _ = leptos_reactive::create_scope(create_runtime(), move |cx| {
cx.start_hydration(&parent);
(f(cx));
cx.end_hydration();

View file

@ -1,5 +1,6 @@
use std::time::Duration;
use wasm_bindgen::convert::FromWasmAbi;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
use crate::{debug_warn, event_delegation, is_server};
@ -181,8 +182,12 @@ where
/// Helper function to extract `event.target.value` from an event.
///
/// This is useful in the `on:input` or `on:change` listeners for an `<input>` element.
pub fn event_target_value(event: &web_sys::Event) -> String {
pub fn event_target_value<T>(event: &T) -> String
where
T: JsCast,
{
event
.unchecked_ref::<web_sys::Event>()
.target()
.unwrap_throw()
.unchecked_into::<web_sys::HtmlInputElement>()
@ -250,30 +255,37 @@ pub fn set_interval(
}
/// Adds an event listener to the target DOM element using implicit event delegation.
pub fn add_event_listener(
pub fn add_event_listener<E>(
target: &web_sys::Element,
event_name: &'static str,
cb: impl FnMut(web_sys::Event) + 'static,
) {
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(web_sys::Event)>).into_js_value();
cb: impl FnMut(E) + 'static,
) where
E: FromWasmAbi + 'static,
{
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
let key = event_delegation::event_delegation_key(event_name);
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
event_delegation::add_event_listener(event_name);
}
#[doc(hidden)]
pub fn add_event_listener_undelegated(
pub fn add_event_listener_undelegated<E>(
target: &web_sys::Element,
event_name: &'static str,
cb: impl FnMut(web_sys::Event) + 'static,
) {
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(web_sys::Event)>).into_js_value();
cb: impl FnMut(E) + 'static,
) where
E: FromWasmAbi + 'static,
{
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
#[doc(hidden)]
#[inline(always)]
pub fn ssr_event_listener(_cb: impl FnMut(web_sys::Event) + 'static) {
pub fn ssr_event_listener<E>(_cb: impl FnMut(E) + 'static)
where
E: FromWasmAbi + 'static,
{
// this function exists only for type inference in templates for SSR
}

View file

@ -12,12 +12,30 @@ pub fn escape_attr(text: &str) -> Cow<'_, str> {
}
cfg_if! {
if #[cfg(feature = "ssr")] {
if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
use leptos_reactive::*;
use crate::Element;
use futures::{stream::FuturesUnordered, Stream, StreamExt};
/// Renders a component to a static HTML string.
///
/// ```
/// # cfg_if::cfg_if! { if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
/// let html = render_to_string(|cx| view! { cx,
/// <p>"Hello, world!"</p>
/// });
/// assert_eq!(html, r#"<p data-hk="0-0">Hello, world!</p>"#);
/// # }}
/// ```
pub fn render_to_string(view: impl FnOnce(Scope) -> Element + 'static) -> String {
let runtime = create_runtime();
let html = run_scope(runtime, move |cx| view(cx));
runtime.dispose();
html
}
/// Renders a component to a stream of HTML strings.
///
/// This renders:
@ -30,9 +48,12 @@ cfg_if! {
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
/// 3) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
pub fn render_to_stream(view: impl Fn(Scope) -> Element + 'static) -> impl Stream<Item = String> {
pub fn render_to_stream(view: impl FnOnce(Scope) -> Element + 'static) -> impl Stream<Item = String> {
// create the runtime
let runtime = create_runtime();
let ((shell, pending_resources, pending_fragments, serializers), _, disposer) =
run_scope_undisposed({
run_scope_undisposed(runtime, {
move |cx| {
// the actual app body/template code
// this does NOT contain any of the data being loaded asynchronously in resources
@ -55,6 +76,39 @@ cfg_if! {
fragments.push(async move { (fragment_id, fut.await) })
}
// resources and fragments
let resources_and_fragments = futures::stream::select(
// stream data for each Resource as it resolves
serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
format!(
r#"<script>
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
console.log("(create_resource) calling resolver");
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
}} else {{
console.log("(create_resource) saving data for resource creation");
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
}}
</script>"#,
)
}),
// stream HTML for each <Suspense/> as it resolves
fragments.map(|(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}">{html}</template>
<script>
var frag = document.querySelector(`[data-fragment-id="{fragment_id}"]`);
var tpl = document.getElementById("{fragment_id}");
console.log("replace", frag, "with", tpl.content.cloneNode(true));
frag.replaceWith(tpl.content.cloneNode(true));
</script>
"#
)
})
);
// HTML for the view function and script to store resources
futures::stream::once(async move {
format!(
@ -68,42 +122,11 @@ cfg_if! {
"#
)
})
// TODO this is wrong: it should merge the next two streams, not chain them
// you may well need to resolve some fragments before some of the resources are resolved
// stream data for each Resource as it resolves
.chain(serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
format!(
r#"<script>
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
console.log("(create_resource) calling resolver");
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
}} else {{
console.log("(create_resource) saving data for resource creation");
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
}}
</script>"#,
)
}))
// stream HTML for each <Suspense/> as it resolves
.chain(fragments.map(|(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}">{html}</template>
<script>
var frag = document.querySelector(`[data-fragment-id="{fragment_id}"]`);
var tpl = document.getElementById("{fragment_id}");
console.log("replace", frag, "with", tpl.content.cloneNode(true));
frag.replaceWith(tpl.content.cloneNode(true));
</script>
"#
)
}))
// dispose of Scope
.chain(futures::stream::once(async {
.chain(resources_and_fragments)
// dispose of Scope and Runtime
.chain(futures::stream::once(async move {
disposer.dispose();
runtime.dispose();
Default::default()
}))
}

View file

@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.0.17"
version = "0.0.18"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@ -18,9 +18,10 @@ quote = "1"
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
syn-rsx = "0.9"
uuid = { version = "1", features = ["v4"] }
leptos_dom = { path = "../leptos_dom", version = "0.0.17" }
leptos_reactive = { path = "../leptos_reactive", version = "0.0.17" }
leptos_server = { path = "../leptos_server", version = "0.0.17" }
leptos_dom = { path = "../leptos_dom", version = "0.0.18" }
leptos_reactive = { path = "../leptos_reactive", version = "0.0.18" }
leptos_server = { path = "../leptos_server", version = "0.0.18" }
lazy_static = "1.4"
[dev-dependencies]
log = "0.4"

View file

@ -42,7 +42,7 @@ mod server;
/// 1. Text content should be provided as a Rust string, i.e., double-quoted:
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// view! { cx, <p>"Heres some text"</p> };
/// # }
@ -52,7 +52,7 @@ mod server;
/// 2. Self-closing tags need an explicit `/` as in XML/XHTML
/// ```rust,compile_fail
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// // ❌ not like this
/// view! { cx, <input type="text" name="name"> }
@ -62,7 +62,7 @@ mod server;
/// ```
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// // ✅ add that slash
/// view! { cx, <input type="text" name="name" /> }
@ -76,7 +76,7 @@ mod server;
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::*; use typed_builder::TypedBuilder; use leptos_dom::wasm_bindgen::JsCast; use leptos_dom as leptos; use leptos_dom::Marker;
/// # #[derive(TypedBuilder)] struct CounterProps { initial_value: i32 }
/// # fn Counter(cx: Scope, props: CounterProps) -> Element { view! { cx, <p></p>} }
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// view! { cx, <div><Counter initial_value=3 /></div> }
/// # ;
@ -94,7 +94,7 @@ mod server;
///
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast; use leptos_dom as leptos; use leptos_dom::Marker;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (count, set_count) = create_signal(cx, 0);
///
@ -111,14 +111,15 @@ mod server;
/// # });
/// ```
///
/// 5. Event handlers can be added with `on:` attributes.
/// 5. Event handlers can be added with `on:` attributes. In most cases, the events are given the correct type
/// based on the event name.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// view! {
/// cx,
/// <button on:click=|ev: web_sys::Event| {
/// <button on:click=|ev: web_sys::MouseEvent| {
/// log::debug!("click event: {ev:#?}");
/// }>
/// "Click me"
@ -134,7 +135,7 @@ mod server;
/// and `None` deletes the property.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
///
@ -156,7 +157,7 @@ mod server;
/// 7. Classes can be toggled with `class:` attributes, which take a `bool` (or a signal that returns a `bool`).
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (count, set_count) = create_signal(cx, 2);
/// view! { cx, <div class:hidden-div={move || count() < 3}>"Now you see me, now you dont."</div> }
@ -168,7 +169,7 @@ mod server;
/// Class names can include dashes, but cannot (at the moment) include a dash-separated segment of only numbers.
/// ```rust,compile_fail
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (count, set_count) = create_signal(cx, 2);
/// // `hidden-div-25` is invalid at the moment
@ -181,7 +182,7 @@ mod server;
/// 8. You can use the `_ref` attribute to store a reference to its DOM element in a variable to use later.
/// ```rust
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view; use leptos_dom::wasm_bindgen::JsCast;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let (value, set_value) = create_signal(cx, 0);
/// let my_input: Element;
@ -203,9 +204,9 @@ mod server;
///
/// // create event handlers for our buttons
/// // note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
/// let clear = move |_ev: web_sys::Event| set_value(0);
/// let decrement = move |_ev: web_sys::Event| set_value.update(|value| *value -= 1);
/// let increment = move |_ev: web_sys::Event| set_value.update(|value| *value += 1);
/// let clear = move |_ev: web_sys::MouseEvent| set_value(0);
/// let decrement = move |_ev: web_sys::MouseEvent| set_value.update(|value| *value -= 1);
/// let increment = move |_ev: web_sys::MouseEvent| set_value.update(|value| *value += 1);
///
/// // this JSX is compiled to an HTML template string for performance
/// view! {
@ -243,6 +244,106 @@ pub fn view(tokens: TokenStream) -> TokenStream {
}
}
/// Annotates a function so that it can be used with your template as a <Component/>
///
/// Here are some things you should know.
/// 1. **The component function only runs once.** Your component function is not a “render” function
/// that re-runs whenever changes happen in the state. Its a “setup” function that runs once to
/// create the user interface, and sets up a reactive system to update it. This means its okay
/// to do relatively expensive work within the component function, as it will only happen once,
/// not on every state change.
///
/// 2. The component name should be `CamelCase` instead of `snake_case`. This is how the renderer
/// recognizes that a particular tag is a component, not an HTML element.
///
/// ```
/// # use leptos::*;
/// // ❌ not snake_case
/// #[component]
/// fn my_component(cx: Scope) -> Element { todo!() }
///
/// // ✅ CamelCase
/// #[component]
/// fn MyComponent(cx: Scope) -> Element { todo!() }
/// ```
///
/// 3. The macro generates a type `ComponentProps` for every `Component` (so, `HomePage` generates `HomePageProps`,
/// `Button` generates `ButtonProps`, etc.) When youre importing the component, you also need to **explicitly import
/// the prop type.**
///
/// ```
/// # use leptos::*;
///
/// use component::{MyComponent, MyComponentProps};
///
/// mod component {
/// use leptos::*;
///
/// #[component]
/// pub fn MyComponent(cx: Scope) -> Element { todo!() }
/// }
/// ```
///
/// 4. You can pass generic arguments, but they should be defined in a `where` clause and not inline.
///
/// ```compile_error
/// // ❌ This won't work.
/// # use leptos::*;
/// #[component]
/// fn MyComponent<T: Fn() -> Element>(cx: Scope, render_prop: T) -> Element {
/// todo!()
/// }
/// ```
///
/// ```
/// // ✅ Do this instead
/// # use leptos::*;
/// #[component]
/// fn MyComponent<T>(cx: Scope, render_prop: T) -> Element where T: Fn() -> Element {
/// todo!()
/// }
/// ```
///
/// 5. You can access the children passed into the component with the `children` property, which takes
/// an argument of the form `Box<dyn Fn() -> Vec<T>>` where `T` is the child type (usually `Element`).
///
/// ```
/// # use leptos::*;
/// #[component]
/// fn ComponentWithChildren(cx: Scope, children: Box<dyn Fn() -> Vec<Element>>) -> Element {
/// // wrap each child in a <strong> element
/// let children = children()
/// .into_iter()
/// .map(|child| view! { cx, <strong>{child}</strong> })
/// .collect::<Vec<_>>();
///
/// // wrap the whole set in a fancy wrapper
/// view! { cx,
/// <p class="fancy-wrapper">{children}</p>
/// }
/// }
///
/// #[component]
/// fn WrapSomeChildren(cx: Scope) -> Element {
/// view! { cx,
/// <ComponentWithChildren>
/// <span>"Ooh, look at us!"</span>
/// <span>"We're being projected!"</span>
/// </ComponentWithChildren>
/// }
/// }
/// ```
///
/// ```
/// # use leptos::*;
/// #[component]
/// fn MyComponent<T>(cx: Scope, render_prop: T) -> Element
/// where
/// T: Fn() -> Element,
/// {
/// todo!()
/// }
/// ```
#[proc_macro_attribute]
pub fn component(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
match syn::parse::<component::InlinePropsBody>(s) {

View file

@ -1,3 +1,4 @@
use std::collections::HashMap;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, ExprPath};
@ -20,6 +21,79 @@ const NON_BUBBLING_EVENTS: [&str; 11] = [
"loadend",
];
lazy_static::lazy_static! {
// Specialized event type
// https://github.com/yewstack/yew/blob/d422b533ea19a09cddf9b31ecd6cd5e5ce35ce3f/packages/yew/src/html/listener/events.rs
static ref EVENTS: HashMap<&'static str, &'static str> = {
let mut m = HashMap::new();
m.insert("auxclick", "MouseEvent");
m.insert("click", "MouseEvent");
m.insert("contextmenu", "MouseEvent");
m.insert("dblclick", "MouseEvent");
m.insert("drag", "DragEvent");
m.insert("dragend", "DragEvent");
m.insert("dragenter", "DragEvent");
m.insert("dragexit", "DragEvent");
m.insert("dragleave", "DragEvent");
m.insert("dragover", "DragEvent");
m.insert("dragstart", "DragEvent");
m.insert("drop", "DragEvent");
m.insert("blur", "FocusEvent");
m.insert("focus", "FocusEvent");
m.insert("focusin", "FocusEvent");
m.insert("focusout", "FocusEvent");
m.insert("keydown", "KeyboardEvent");
m.insert("keypress", "KeyboardEvent");
m.insert("keyup", "KeyboardEvent");
m.insert("loadstart", "ProgressEvent");
m.insert("progress", "ProgressEvent");
m.insert("loadend", "ProgressEvent");
m.insert("mousedown", "MouseEvent");
m.insert("mouseenter", "MouseEvent");
m.insert("mouseleave", "MouseEvent");
m.insert("mousemove", "MouseEvent");
m.insert("mouseout", "MouseEvent");
m.insert("mouseover", "MouseEvent");
m.insert("mouseup", "MouseEvent");
m.insert("wheel", "WheelEvent");
m.insert("input", "InputEvent");
m.insert("submit", "SubmitEvent");
m.insert("animationcancel", "AnimationEvent");
m.insert("animationend", "AnimationEvent");
m.insert("animationiteration", "AnimationEvent");
m.insert("animationstart", "AnimationEvent");
m.insert("gotpointercapture", "PointerEvent");
m.insert("lostpointercapture", "PointerEvent");
m.insert("pointercancel", "PointerEvent");
m.insert("pointerdown", "PointerEvent");
m.insert("pointerenter", "PointerEvent");
m.insert("pointerleave", "PointerEvent");
m.insert("pointermove", "PointerEvent");
m.insert("pointerout", "PointerEvent");
m.insert("pointerover", "PointerEvent");
m.insert("pointerup", "PointerEvent");
m.insert("touchcancel", "TouchEvent");
m.insert("touchend", "TouchEvent");
m.insert("transitioncancel", "TransitionEvent");
m.insert("transitionend", "TransitionEvent");
m.insert("transitionrun", "TransitionEvent");
m.insert("transitionstart", "TransitionEvent");
m
};
}
pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream {
let template_uid = Ident::new(
&format!("TEMPLATE_{}", Uuid::new_v4().simple()),
@ -309,14 +383,14 @@ fn element_to_tokens(
quote_spanned! {
span => let #this_el_ident = #debug_name;
//log::debug!("next_sibling ({})", #debug_name);
let #this_el_ident = #prev_sib.next_sibling().unwrap_throw();
let #this_el_ident = #prev_sib.next_sibling().unwrap_or_else(|| ::leptos::__leptos_renderer_error(#debug_name, "nextSibling"));
//log::debug!("=> got {}", #this_el_ident.node_name());
}
} else {
quote_spanned! {
span => let #this_el_ident = #debug_name;
//log::debug!("first_child ({})", #debug_name);
let #this_el_ident = #parent.first_child().unwrap_throw();
let #this_el_ident = #parent.first_child().unwrap_or_else(|| ::leptos::__leptos_renderer_error(#debug_name, "firstChild"));
//log::debug!("=> got {}", #this_el_ident.node_name());
}
};
@ -508,22 +582,26 @@ fn attr_to_tokens(
.expect("event listener attributes need a value")
.as_ref();
let name = name.replacen("on:", "", 1);
let event_type = EVENTS.get(&name.as_str()).copied().unwrap_or("Event");
let event_type = event_type.parse::<TokenStream>().expect("couldn't parse event name");
if mode != Mode::Ssr {
let name = name.replacen("on:", "", 1);
if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener_undelegated(#el_id.unchecked_ref(), #name, #handler);
span => ::leptos::add_event_listener_undelegated::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
});
} else {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener(#el_id.unchecked_ref(), #name, #handler);
span => ::leptos::add_event_listener::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
});
}
} else {
// this is here to avoid warnings about unused signals
// that are used in event listeners. I'm open to better solutions.
expressions.push(quote_spanned! {
span => let _ = ssr_event_listener(#handler);
span => let _ = ssr_event_listener::<web_sys::#event_type>(#handler);
});
}
}
@ -738,13 +816,13 @@ fn block_to_tokens(
let location = if let Some(sibling) = &prev_sib {
quote_spanned! {
span => //log::debug!("-> next sibling");
let #name = #sibling.next_sibling().unwrap_throw();
let #name = #sibling.next_sibling().unwrap_or_else(|| ::leptos::__leptos_renderer_error("{block}", "nextSibling"));
//log::debug!("\tnext sibling = {}", #name.node_name());
}
} else {
quote_spanned! {
span => //log::debug!("\\|/ first child on {}", #parent.node_name());
let #name = #parent.first_child().unwrap_throw();
let #name = #parent.first_child().unwrap_or_else(|| ::leptos::__leptos_renderer_error("{block}", "firstChild"));
//log::debug!("\tfirst child = {}", #name.node_name());
}
};
@ -860,6 +938,8 @@ fn component_to_tokens(
mode: Mode,
is_first_child: bool,
) -> PrevSibChange {
let component_name = ident_from_tag_name(&node.name);
let component_name = format!("<{component_name}/>");
let create_component = create_component(cx, node, mode);
let span = node.name.span();
@ -896,13 +976,13 @@ fn component_to_tokens(
let starts_at = if let Some(prev_sib) = prev_sib {
quote::quote! {{
//log::debug!("starts_at = next_sibling");
#prev_sib.next_sibling().unwrap_throw()
#prev_sib.next_sibling().unwrap_or_else(|| ::leptos::__leptos_renderer_error(#component_name, "nextSibling"))
//log::debug!("ok starts_at");
}}
} else {
quote::quote! {{
//log::debug!("starts_at first_child");
#parent.first_child().unwrap_throw()
#parent.first_child().unwrap_or_else(|| ::leptos::__leptos_renderer_error(#component_name, "firstChild"))
//log::debug!("starts_at ok");
}}
};
@ -1033,9 +1113,13 @@ fn create_component(cx: &Ident, node: &NodeElement, mode: Mode) -> TokenStream {
Some(quote_spanned! {
span => ::leptos::add_event_listener_undelegated(#component_name.unchecked_ref(), #event_name, #handler);
})
} else if let Some(event_type) = EVENTS.get(event_name).map(|&e| e.parse::<TokenStream>().unwrap_or_default()) {
Some(quote_spanned! {
span => ::leptos::add_event_listener::<#event_type>(#component_name.unchecked_ref(), #event_name, #handler);
})
} else {
Some(quote_spanned! {
span => ::leptos::add_event_listener(#component_name.unchecked_ref(), #event_name, #handler)
span => ::leptos::add_event_listener::<web_sys::Event>(#component_name.unchecked_ref(), #event_name, #handler)
})
}
}

View file

@ -1,6 +1,6 @@
[package]
name = "leptos_reactive"
version = "0.0.17"
version = "0.0.18"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View file

@ -3,7 +3,7 @@ use std::{
collections::HashMap,
};
use crate::Scope;
use crate::{runtime::with_runtime, Scope};
/// Provides a context value of type `T` to the current reactive [Scope](crate::Scope)
/// and all of its descendants. This can be consumed using [use_context](crate::use_context).
@ -14,7 +14,7 @@ use crate::Scope;
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
///
/// // Note: this example doesnt use Leptoss DOM model or component structure,
/// // so it ends up being a little silly.
@ -48,9 +48,11 @@ where
T: Clone + 'static,
{
let id = value.type_id();
let mut contexts = cx.runtime.scope_contexts.borrow_mut();
let context = contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
context.insert(id, Box::new(value) as Box<dyn Any>);
with_runtime(cx.runtime, |runtime| {
let mut contexts = runtime.scope_contexts.borrow_mut();
let context = contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
context.insert(id, Box::new(value) as Box<dyn Any>);
});
}
/// Extracts a context value of type `T` from the reactive system by traversing
@ -64,7 +66,7 @@ where
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
///
/// // Note: this example doesnt use Leptoss DOM model or component structure,
/// // so it ends up being a little silly.
@ -98,25 +100,26 @@ where
T: Clone + 'static,
{
let id = TypeId::of::<T>();
let local_value = {
let contexts = cx.runtime.scope_contexts.borrow();
let context = contexts.get(cx.id);
context
.and_then(|context| context.get(&id).and_then(|val| val.downcast_ref::<T>()))
.cloned()
};
match local_value {
Some(val) => Some(val),
None => cx
.runtime
.scope_parents
.borrow()
.get(cx.id)
.and_then(|parent| {
use_context::<T>(Scope {
runtime: cx.runtime,
id: *parent,
})
}),
}
with_runtime(cx.runtime, |runtime| {
let local_value = {
let contexts = runtime.scope_contexts.borrow();
let context = contexts.get(cx.id);
context
.and_then(|context| context.get(&id).and_then(|val| val.downcast_ref::<T>()))
.cloned()
};
match local_value {
Some(val) => Some(val),
None => runtime
.scope_parents
.borrow()
.get(cx.id)
.and_then(|parent| {
use_context::<T>(Scope {
runtime: cx.runtime,
id: *parent,
})
}),
}
})
}

View file

@ -1,3 +1,4 @@
use crate::runtime::{with_runtime, RuntimeId};
use crate::{debug_warn, Runtime, Scope, ScopeProperty};
use cfg_if::cfg_if;
use std::cell::RefCell;
@ -22,7 +23,7 @@ use std::fmt::Debug;
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
///
@ -66,7 +67,7 @@ where
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
///
@ -118,7 +119,7 @@ where
}
pub(crate) trait AnyEffect {
fn run(&self, id: EffectId, runtime: &Runtime);
fn run(&self, id: EffectId, runtime: RuntimeId);
}
impl<T, F> AnyEffect for Effect<T, F>
@ -126,35 +127,39 @@ where
T: 'static,
F: Fn(Option<T>) -> T,
{
fn run(&self, id: EffectId, runtime: &Runtime) {
// clear previous dependencies
id.cleanup(runtime);
fn run(&self, id: EffectId, runtime: RuntimeId) {
with_runtime(runtime, |runtime| {
// clear previous dependencies
id.cleanup(runtime);
// set this as the current observer
let prev_observer = runtime.observer.take();
runtime.observer.set(Some(id));
// set this as the current observer
let prev_observer = runtime.observer.take();
runtime.observer.set(Some(id));
// run the effect
let value = self.value.take();
let new_value = (self.f)(value);
*self.value.borrow_mut() = Some(new_value);
// run the effect
let value = self.value.take();
let new_value = (self.f)(value);
*self.value.borrow_mut() = Some(new_value);
// restore the previous observer
runtime.observer.set(prev_observer);
// restore the previous observer
runtime.observer.set(prev_observer);
})
}
}
impl EffectId {
pub(crate) fn run<T>(&self, runtime: &Runtime) {
let effect = {
let effects = runtime.effects.borrow();
effects.get(*self).cloned()
};
if let Some(effect) = effect {
effect.run(*self, runtime);
} else {
debug_warn!("[Effect] Trying to run an Effect that has been disposed. This is probably either a logic error in a component that creates and disposes of scopes, or a Resource resolving after its scope has been dropped without having been cleaned up.")
}
pub(crate) fn run<T>(&self, runtime_id: RuntimeId) {
with_runtime(runtime_id, |runtime| {
let effect = {
let effects = runtime.effects.borrow();
effects.get(*self).cloned()
};
if let Some(effect) = effect {
effect.run(*self, runtime_id);
} else {
debug_warn!("[Effect] Trying to run an Effect that has been disposed. This is probably either a logic error in a component that creates and disposes of scopes, or a Resource resolving after its scope has been dropped without having been cleaned up.")
}
})
}
pub(crate) fn cleanup(&self, runtime: &Runtime) {

View file

@ -37,7 +37,7 @@
//! // creates a new reactive Scope
//! // this is omitted from most of the examples in the docs
//! // you usually won't need to call it yourself
//! create_scope(|cx| {
//! create_scope(create_runtime(), |cx| {
//! // a signal: returns a (getter, setter) pair
//! let (count, set_count) = create_signal(cx, 0);
//!
@ -85,6 +85,7 @@ pub use effect::*;
pub use memo::*;
pub use resource::*;
use runtime::*;
pub use runtime::{create_runtime, RuntimeId};
pub use scope::*;
pub use selector::*;
pub use serialization::*;

View file

@ -20,7 +20,7 @@ use std::fmt::Debug;
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (value, set_value) = create_signal(cx, 0);
///
/// // 🆗 we could create a derived signal with a simple function
@ -80,7 +80,7 @@ where
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (value, set_value) = create_signal(cx, 0);
///
/// // 🆗 we could create a derived signal with a simple function
@ -155,7 +155,7 @@ where
/// the running effect to the memo.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
/// let double_count = create_memo(cx, move |_| count() * 2);
///
@ -178,7 +178,7 @@ where
/// the running effect to this memo.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
/// let name_upper = create_memo(cx, move |_| name().to_uppercase());
///

View file

@ -11,8 +11,10 @@ use std::{
use crate::{
create_effect, create_isomorphic_effect, create_memo, create_signal, queue_microtask,
runtime::Runtime, serialization::Serializable, spawn::spawn_local, use_context, Memo,
ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal,
runtime::{with_runtime, RuntimeId},
serialization::Serializable,
spawn::spawn_local,
use_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal,
};
/// Creates [Resource](crate::Resource), which is a signal that reflects the
@ -31,7 +33,7 @@ use crate::{
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// // any old async function; maybe this is calling a REST API or something
/// async fn fetch_cat_picture_urls(how_many: i32) -> Vec<String> {
/// // pretend we're fetching cat pics
@ -121,7 +123,9 @@ where
suspense_contexts: Default::default(),
});
let id = cx.runtime.create_serializable_resource(Rc::clone(&r));
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_serializable_resource(Rc::clone(&r))
});
create_isomorphic_effect(cx, {
let r = Rc::clone(&r);
@ -153,7 +157,7 @@ where
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// #[derive(Debug, Clone)] // doesn't implement Serialize, Deserialize
/// struct ComplicatedUnserializableStruct {
/// // something here that can't be serialized
@ -223,7 +227,9 @@ where
suspense_contexts: Default::default(),
});
let id = cx.runtime.create_unserializable_resource(Rc::clone(&r));
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_unserializable_resource(Rc::clone(&r))
});
create_effect(cx, {
let r = Rc::clone(&r);
@ -259,61 +265,63 @@ where
{
use wasm_bindgen::{JsCast, UnwrapThrowExt};
if let Some(ref mut context) = *cx.runtime.shared_context.borrow_mut() {
if let Some(data) = context.resolved_resources.remove(&id) {
// The server already sent us the serialized resource value, so
// deserialize & set it now
context.pending_resources.remove(&id); // no longer pending
r.resolved.set(true);
with_runtime(cx.runtime, |runtime| {
if let Some(ref mut context) = *runtime.shared_context.borrow_mut() {
if let Some(data) = context.resolved_resources.remove(&id) {
// The server already sent us the serialized resource value, so
// deserialize & set it now
context.pending_resources.remove(&id); // no longer pending
r.resolved.set(true);
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
r.set_value.update(|n| *n = Some(res));
r.set_loading.update(|n| *n = false);
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
r.set_value.update(|n| *n = Some(res));
r.set_loading.update(|n| *n = false);
// for reactivity
r.source.subscribe();
} else if context.pending_resources.remove(&id) {
// We're still waiting for the resource, add a "resolver" closure so
// that it will be set as soon as the server sends the serialized
// value
r.set_loading.update(|n| *n = true);
// for reactivity
r.source.subscribe();
} else if context.pending_resources.remove(&id) {
// We're still waiting for the resource, add a "resolver" closure so
// that it will be set as soon as the server sends the serialized
// value
r.set_loading.update(|n| *n = true);
let resolve = {
let resolved = r.resolved.clone();
let set_value = r.set_value;
let set_loading = r.set_loading;
move |res: String| {
let res =
T::from_json(&res).expect_throw("could not deserialize Resource JSON");
resolved.set(true);
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
}
};
let resolve =
wasm_bindgen::closure::Closure::wrap(Box::new(resolve) as Box<dyn Fn(String)>);
let resource_resolvers = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOURCE_RESOLVERS"),
)
.expect_throw("no __LEPTOS_RESOURCE_RESOLVERS found in the JS global scope");
let id = serde_json::to_string(&id).expect_throw("could not serialize Resource ID");
_ = js_sys::Reflect::set(
&resource_resolvers,
&wasm_bindgen::JsValue::from_str(&id),
resolve.as_ref().unchecked_ref(),
);
let resolve = {
let resolved = r.resolved.clone();
let set_value = r.set_value;
let set_loading = r.set_loading;
move |res: String| {
let res =
T::from_json(&res).expect_throw("could not deserialize Resource JSON");
resolved.set(true);
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
}
};
let resolve =
wasm_bindgen::closure::Closure::wrap(Box::new(resolve) as Box<dyn Fn(String)>);
let resource_resolvers = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOURCE_RESOLVERS"),
)
.expect_throw("no __LEPTOS_RESOURCE_RESOLVERS found in the JS global scope");
let id = serde_json::to_string(&id).expect_throw("could not serialize Resource ID");
_ = js_sys::Reflect::set(
&resource_resolvers,
&wasm_bindgen::JsValue::from_str(&id),
resolve.as_ref().unchecked_ref(),
);
// for reactivity
r.source.subscribe()
// for reactivity
r.source.subscribe()
} else {
// Server didn't mark the resource as pending, so load it on the
// client
r.load(false);
}
} else {
// Server didn't mark the resource as pending, so load it on the
// client
r.load(false);
r.load(false)
}
} else {
r.load(false)
}
})
}
impl<S, T> Resource<S, T>
@ -331,8 +339,9 @@ where
where
T: Clone,
{
self.runtime
.resource(self.id, |resource: &ResourceState<S, T>| resource.read())
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.read())
})
}
/// Applies a function to the current value of the resource, and subscribes
@ -343,20 +352,23 @@ where
/// If you want to get the value by cloning it, you can use
/// [Resource::read].
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
self.runtime
.resource(self.id, |resource: &ResourceState<S, T>| resource.with(f))
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.with(f))
})
}
/// Returns a signal that indicates whether the resource is currently loading.
pub fn loading(&self) -> ReadSignal<bool> {
self.runtime
.resource(self.id, |resource: &ResourceState<S, T>| resource.loading)
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.loading)
})
}
/// Re-runs the async function with the current source data.
pub fn refetch(&self) {
self.runtime
.resource(self.id, |resource: &ResourceState<S, T>| resource.refetch())
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.refetch())
});
}
/// Returns a [std::future::Future] that will resolve when the resource has loaded,
@ -366,11 +378,12 @@ where
where
T: Serializable,
{
self.runtime
.resource(self.id, |resource: &ResourceState<S, T>| {
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.to_serialization_resolver(self.id)
})
.await
})
.await
}
}
@ -390,7 +403,7 @@ where
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// // any old async function; maybe this is calling a REST API or something
/// async fn fetch_cat_picture_urls(how_many: i32) -> Vec<String> {
/// // pretend we're fetching cat pics
@ -423,7 +436,7 @@ where
S: Debug + 'static,
T: Debug + 'static,
{
runtime: &'static Runtime,
runtime: RuntimeId,
pub(crate) id: ResourceId,
pub(crate) source_ty: PhantomData<S>,
pub(crate) out_ty: PhantomData<T>,

View file

@ -3,6 +3,7 @@ use crate::{
EffectId, Memo, ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
ScopeProperty, SignalId, WriteSignal,
};
use cfg_if::cfg_if;
use futures::stream::FuturesUnordered;
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
use std::{
@ -18,6 +19,185 @@ use std::{
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
thread_local! {
pub(crate) static RUNTIME: Runtime = Runtime::new();
}
} else {
thread_local! {
pub(crate) static RUNTIMES: RefCell<SlotMap<RuntimeId, Runtime>> = Default::default();
}
}
}
/// Get the selected runtime from the thread-local set of runtimes. On the server,
/// this will return the correct runtime. In the browser, there should only be one runtime.
pub(crate) fn with_runtime<T>(id: RuntimeId, f: impl FnOnce(&Runtime) -> T) -> T {
// in the browser, everything should exist under one runtime
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
_ = id;
RUNTIME.with(|runtime| f(runtime))
} else {
RUNTIMES.with(|runtimes| {
let runtimes = runtimes.borrow();
let runtime = runtimes
.get(id)
.expect("Tried to access a Runtime that no longer exists.");
f(runtime)
})
}
}
}
#[doc(hidden)]
#[must_use = "Runtime will leak memory if Runtime::dispose() is never called."]
/// Creates a new reactive [Runtime]. This should almost always be handled by the framework.
pub fn create_runtime() -> RuntimeId {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
Default::default()
} else {
RUNTIMES.with(|runtimes| runtimes.borrow_mut().insert(Runtime::new()))
}
}
}
slotmap::new_key_type! {
/// Unique ID assigned to a [Runtime](crate::Runtime).
pub struct RuntimeId;
}
impl RuntimeId {
/// Removes the runtime, disposing all its child [Scope](crate::Scope)s.
pub fn dispose(self) {
cfg_if! {
if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
let runtime = RUNTIMES.with(move |runtimes| runtimes.borrow_mut().remove(self));
if let Some(runtime) = runtime {
for (scope_id, _) in runtime.scopes.borrow().iter() {
let scope = Scope {
runtime: self,
id: scope_id,
};
scope.dispose();
}
}
}
}
}
pub(crate) fn raw_scope_and_disposer(self) -> (Scope, ScopeDisposer) {
with_runtime(self, |runtime| {
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
let scope = Scope { runtime: self, id };
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(scope, disposer)
})
}
pub(crate) fn run_scope_undisposed<T>(
self,
f: impl FnOnce(Scope) -> T,
parent: Option<Scope>,
) -> (T, ScopeId, ScopeDisposer) {
with_runtime(self, |runtime| {
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
if let Some(parent) = parent {
runtime.scope_parents.borrow_mut().insert(id, parent.id);
}
let scope = Scope { runtime: self, id };
let val = f(scope);
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(val, id, disposer)
})
}
pub(crate) fn run_scope<T>(self, f: impl FnOnce(Scope) -> T, parent: Option<Scope>) -> T {
let (ret, _, disposer) = self.run_scope_undisposed(f, parent);
disposer.dispose();
ret
}
pub(crate) fn create_signal<T>(self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
where
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
});
(
ReadSignal {
runtime: self,
id,
ty: PhantomData,
},
WriteSignal {
runtime: self,
id,
ty: PhantomData,
},
)
}
pub(crate) fn create_rw_signal<T>(self, value: T) -> RwSignal<T>
where
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
});
RwSignal {
runtime: self,
id,
ty: PhantomData,
}
}
pub(crate) fn create_effect<T>(self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
where
T: Any + 'static,
{
with_runtime(self, |runtime| {
let effect = Effect {
f,
value: RefCell::new(None),
};
let id = { runtime.effects.borrow_mut().insert(Rc::new(effect)) };
id.run::<T>(self);
id
})
}
pub(crate) fn create_memo<T>(self, f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + Any + 'static,
{
let (read, write) = self.create_signal(None);
self.create_effect(move |_| {
let (new, changed) = read.with_no_subscription(|p| {
let new = f(p.as_ref());
let changed = Some(&new) != p.as_ref();
(new, changed)
});
if changed {
write.update(|n| *n = Some(new));
}
});
Memo(read)
}
}
#[derive(Default)]
pub(crate) struct Runtime {
pub shared_context: RefCell<Option<SharedContext>>,
@ -57,105 +237,6 @@ impl Runtime {
Self::default()
}
pub fn raw_scope_and_disposer(&'static self) -> (Scope, ScopeDisposer) {
let id = { self.scopes.borrow_mut().insert(Default::default()) };
let scope = Scope { runtime: self, id };
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(scope, disposer)
}
pub fn run_scope_undisposed<T>(
&'static self,
f: impl FnOnce(Scope) -> T,
parent: Option<Scope>,
) -> (T, ScopeId, ScopeDisposer) {
let id = { self.scopes.borrow_mut().insert(Default::default()) };
if let Some(parent) = parent {
self.scope_parents.borrow_mut().insert(id, parent.id);
}
let scope = Scope { runtime: self, id };
let val = f(scope);
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(val, id, disposer)
}
pub fn run_scope<T>(&'static self, f: impl FnOnce(Scope) -> T, parent: Option<Scope>) -> T {
let (ret, _, disposer) = self.run_scope_undisposed(f, parent);
disposer.dispose();
ret
}
pub(crate) fn create_signal<T>(&'static self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
where
T: Any + 'static,
{
let id = self
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)));
(
ReadSignal {
runtime: self,
id,
ty: PhantomData,
},
WriteSignal {
runtime: self,
id,
ty: PhantomData,
},
)
}
pub(crate) fn create_rw_signal<T>(&'static self, value: T) -> RwSignal<T>
where
T: Any + 'static,
{
let id = self
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)));
RwSignal {
runtime: self,
id,
ty: PhantomData,
}
}
pub(crate) fn create_effect<T>(&'static self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
where
T: Any + 'static,
{
let effect = Effect {
f,
value: RefCell::new(None),
};
let id = { self.effects.borrow_mut().insert(Rc::new(effect)) };
id.run::<T>(self);
id
}
pub(crate) fn create_memo<T>(&'static self, f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + Any + 'static,
{
let (read, write) = self.create_signal(None);
self.create_effect(move |_| {
let (new, changed) = read.with_no_subscription(|p| {
let new = f(p.as_ref());
let changed = Some(&new) != p.as_ref();
(new, changed)
});
if changed {
write.update(|n| *n = Some(new));
}
});
Memo(read)
}
pub(crate) fn create_unserializable_resource<S, T>(
&self,
state: Rc<ResourceState<S, T>>,

View file

@ -1,52 +1,58 @@
use cfg_if::cfg_if;
use crate::{hydration::SharedContext, EffectId, ResourceId, Runtime, SignalId};
use crate::runtime::{with_runtime, RuntimeId};
use crate::{hydration::SharedContext, EffectId, ResourceId, SignalId};
use crate::{PinnedFuture, SuspenseContext};
use futures::stream::FuturesUnordered;
use std::collections::HashMap;
use std::fmt::Debug;
use std::{future::Future, pin::Pin};
#[doc(hidden)]
#[must_use = "Scope will leak memory if the disposer function is never called"]
/// Creates a new reactive system and root reactive scope and runs the function within it.
///
/// This should usually only be used once, at the root of an application, because its reactive
/// values will not have access to values created under another `create_scope`.
pub fn create_scope(f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
// TODO leak
let runtime = Box::leak(Box::new(Runtime::new()));
///
/// You usually don't need to call this manually.
pub fn create_scope(runtime: RuntimeId, f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
runtime.run_scope_undisposed(f, None).2
}
#[doc(hidden)]
#[must_use = "Scope will leak memory if the disposer function is never called"]
/// Creates a new reactive system and root reactive scope, and returns them.
///
/// This should usually only be used once, at the root of an application, because its reactive
/// values will not have access to values created under another `create_scope`.
pub fn raw_scope_and_disposer() -> (Scope, ScopeDisposer) {
// TODO leak
let runtime = Box::leak(Box::new(Runtime::new()));
///
/// You usually don't need to call this manually.
pub fn raw_scope_and_disposer(runtime: RuntimeId) -> (Scope, ScopeDisposer) {
runtime.raw_scope_and_disposer()
}
#[doc(hidden)]
/// Creates a temporary scope, runs the given function, disposes of the scope,
/// and returns the value returned from the function. This is very useful for short-lived
/// applications like SSR, where actual reactivity is not required beyond the end
/// of the synchronous operation.
pub fn run_scope<T>(f: impl FnOnce(Scope) -> T + 'static) -> T {
// TODO this leaks the runtime — should unsafely upgrade the lifetime, and then drop it after the scope is run
let runtime = Box::leak(Box::new(Runtime::new()));
///
/// You usually don't need to call this manually.
pub fn run_scope<T>(runtime: RuntimeId, f: impl FnOnce(Scope) -> T + 'static) -> T {
runtime.run_scope(f, None)
}
#[doc(hidden)]
#[must_use = "Scope will leak memory if the disposer function is never called"]
/// Creates a temporary scope and run the given function without disposing of the scope.
/// If you do not dispose of the scope on your own, memory will leak.
///
/// You usually don't need to call this manually.
pub fn run_scope_undisposed<T>(
runtime: RuntimeId,
f: impl FnOnce(Scope) -> T + 'static,
) -> (T, ScopeId, ScopeDisposer) {
// TODO this leaks the runtime — should unsafely upgrade the lifetime, and then drop it after the scope is run
let runtime = Box::leak(Box::new(Runtime::new()));
runtime.run_scope_undisposed(f, None)
}
@ -67,7 +73,7 @@ pub fn run_scope_undisposed<T>(
/// is [Copy] and `'static` this does not add much overhead or lifetime complexity.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Scope {
pub(crate) runtime: &'static Runtime,
pub(crate) runtime: RuntimeId,
pub(crate) id: ScopeId,
}
@ -77,7 +83,7 @@ impl Scope {
self.id
}
/// Creates a child scope and runs the given function within it.
/// Creates a child scope and runs the given function within it, returning a handle to dispose of it.
///
/// The child scope has its own lifetime and disposer, but will be disposed when the parent is
/// disposed, if it has not been already.
@ -86,23 +92,39 @@ impl Scope {
/// dispose of them when they are no longer needed (e.g., a list item has been destroyed or the user
/// has navigated away from the route.)
pub fn child_scope(self, f: impl FnOnce(Scope)) -> ScopeDisposer {
let (_, child_id, disposer) = self.runtime.run_scope_undisposed(f, Some(self));
let mut children = self.runtime.scope_children.borrow_mut();
children
.entry(self.id)
.expect("trying to add a child to a Scope that has already been disposed")
.or_default()
.push(child_id);
let (_, disposer) = self.run_child_scope(f);
disposer
}
/// Creates a child scope and runs the given function within it, returning the function's return
/// type and a handle to dispose of it.
///
/// The child scope has its own lifetime and disposer, but will be disposed when the parent is
/// disposed, if it has not been already.
///
/// This is useful for applications like a list or a router, which may want to create child scopes and
/// dispose of them when they are no longer needed (e.g., a list item has been destroyed or the user
/// has navigated away from the route.)
pub fn run_child_scope<T>(self, f: impl FnOnce(Scope) -> T) -> (T, ScopeDisposer) {
let (res, child_id, disposer) = self.runtime.run_scope_undisposed(f, Some(self));
with_runtime(self.runtime, |runtime| {
let mut children = runtime.scope_children.borrow_mut();
children
.entry(self.id)
.expect("trying to add a child to a Scope that has already been disposed")
.or_default()
.push(child_id);
});
(res, disposer)
}
/// Suspends reactive tracking while running the given function.
///
/// This can be used to isolate parts of the reactive graph from one another.
///
/// ```
/// # use leptos_reactive::*;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
/// let c = create_memo(cx, move |_| {
@ -122,10 +144,12 @@ impl Scope {
/// # });
/// ```
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
let prev_observer = self.runtime.observer.take();
let untracked_result = f();
self.runtime.observer.set(prev_observer);
untracked_result
with_runtime(self.runtime, |runtime| {
let prev_observer = runtime.observer.take();
let untracked_result = f();
runtime.observer.set(prev_observer);
untracked_result
})
}
}
@ -133,71 +157,75 @@ impl Scope {
impl Scope {
pub(crate) fn dispose(self) {
// dispose of all child scopes
let children = {
let mut children = self.runtime.scope_children.borrow_mut();
children.remove(self.id)
};
with_runtime(self.runtime, |runtime| {
// dispose of all child scopes
let children = {
let mut children = runtime.scope_children.borrow_mut();
children.remove(self.id)
};
if let Some(children) = children {
for id in children {
Scope {
runtime: self.runtime,
id,
if let Some(children) = children {
for id in children {
Scope {
runtime: self.runtime,
id,
}
.dispose();
}
.dispose();
}
}
// run cleanups
if let Some(cleanups) = self.runtime.scope_cleanups.borrow_mut().remove(self.id) {
for cleanup in cleanups {
cleanup();
// run cleanups
if let Some(cleanups) = runtime.scope_cleanups.borrow_mut().remove(self.id) {
for cleanup in cleanups {
cleanup();
}
}
}
// remove everything we own and run cleanups
let owned = {
let owned = self.runtime.scopes.borrow_mut().remove(self.id);
owned.map(|owned| owned.take())
};
if let Some(owned) = owned {
for property in owned {
match property {
ScopeProperty::Signal(id) => {
// remove the signal
self.runtime.signals.borrow_mut().remove(id);
let subs = self.runtime.signal_subscribers.borrow_mut().remove(id);
// remove everything we own and run cleanups
let owned = {
let owned = runtime.scopes.borrow_mut().remove(self.id);
owned.map(|owned| owned.take())
};
if let Some(owned) = owned {
for property in owned {
match property {
ScopeProperty::Signal(id) => {
// remove the signal
runtime.signals.borrow_mut().remove(id);
let subs = runtime.signal_subscribers.borrow_mut().remove(id);
// each of the subs needs to remove the signal from its dependencies
// so that it doesn't try to read the (now disposed) signal
if let Some(subs) = subs {
let source_map = self.runtime.effect_sources.borrow();
for effect in subs.borrow().iter() {
if let Some(effect_sources) = source_map.get(*effect) {
effect_sources.borrow_mut().remove(&id);
// each of the subs needs to remove the signal from its dependencies
// so that it doesn't try to read the (now disposed) signal
if let Some(subs) = subs {
let source_map = runtime.effect_sources.borrow();
for effect in subs.borrow().iter() {
if let Some(effect_sources) = source_map.get(*effect) {
effect_sources.borrow_mut().remove(&id);
}
}
}
}
}
ScopeProperty::Effect(id) => {
self.runtime.effects.borrow_mut().remove(id);
self.runtime.effect_sources.borrow_mut().remove(id);
}
ScopeProperty::Resource(id) => {
self.runtime.resources.borrow_mut().remove(id);
ScopeProperty::Effect(id) => {
runtime.effects.borrow_mut().remove(id);
runtime.effect_sources.borrow_mut().remove(id);
}
ScopeProperty::Resource(id) => {
runtime.resources.borrow_mut().remove(id);
}
}
}
}
}
})
}
pub(crate) fn with_scope_property(&self, f: impl FnOnce(&mut Vec<ScopeProperty>)) {
let scopes = self.runtime.scopes.borrow();
let scope = scopes
.get(self.id)
.expect("tried to add property to a scope that has been disposed");
f(&mut scope.borrow_mut());
with_runtime(self.runtime, |runtime| {
let scopes = runtime.scopes.borrow();
let scope = scopes
.get(self.id)
.expect("tried to add property to a scope that has been disposed");
f(&mut scope.borrow_mut());
})
}
}
@ -206,12 +234,14 @@ impl Scope {
/// It runs after child scopes have been disposed, but before signals, effects, and resources
/// are invalidated.
pub fn on_cleanup(cx: Scope, cleanup_fn: impl FnOnce() + 'static) {
let mut cleanups = cx.runtime.scope_cleanups.borrow_mut();
let cleanups = cleanups
.entry(cx.id)
.expect("trying to clean up a Scope that has already been disposed")
.or_insert_with(Default::default);
cleanups.push(Box::new(cleanup_fn));
with_runtime(cx.runtime, |runtime| {
let mut cleanups = runtime.scope_cleanups.borrow_mut();
let cleanups = cleanups
.entry(cx.id)
.expect("trying to clean up a Scope that has already been disposed")
.or_insert_with(Default::default);
cleanups.push(Box::new(cleanup_fn));
})
}
slotmap::new_key_type! {
@ -253,17 +283,23 @@ impl Scope {
if #[cfg(any(feature = "hydrate", doc))] {
/// `hydrate` only: Whether we're currently hydrating the page.
pub fn is_hydrating(&self) -> bool {
self.runtime.shared_context.borrow().is_some()
with_runtime(self.runtime, |runtime| {
runtime.shared_context.borrow().is_some()
})
}
/// `hydrate` only: Begins the hydration process.
pub fn start_hydration(&self, element: &web_sys::Element) {
self.runtime.start_hydration(element);
with_runtime(self.runtime, |runtime| {
runtime.start_hydration(element);
})
}
/// `hydrate` only: Ends the hydration process.
pub fn end_hydration(&self) {
self.runtime.end_hydration();
with_runtime(self.runtime, |runtime| {
runtime.end_hydration();
})
}
/// `hydrate` only: Gets the next element in the hydration queue, either from the
@ -283,27 +319,29 @@ impl Scope {
t
};
if let Some(ref mut shared_context) = &mut *self.runtime.shared_context.borrow_mut() {
if shared_context.context.is_some() {
let key = shared_context.next_hydration_key();
let node = shared_context.registry.remove(&key);
with_runtime(self.runtime, |runtime| {
if let Some(ref mut shared_context) = &mut *runtime.shared_context.borrow_mut() {
if shared_context.context.is_some() {
let key = shared_context.next_hydration_key();
let node = shared_context.registry.remove(&key);
//log::debug!("(hy) searching for {key}");
//log::debug!("(hy) searching for {key}");
if let Some(node) = node {
//log::debug!("(hy) found {key}");
shared_context.completed.push(node.clone());
node
if let Some(node) = node {
//log::debug!("(hy) found {key}");
shared_context.completed.push(node.clone());
node
} else {
//log::debug!("(hy) did NOT find {key}");
cloned_template(template)
}
} else {
//log::debug!("(hy) did NOT find {key}");
cloned_template(template)
}
} else {
cloned_template(template)
}
} else {
cloned_template(template)
}
})
}
}
}
@ -317,102 +355,108 @@ impl Scope {
let mut current = Vec::new();
let mut start = start.clone();
if self
.runtime
.shared_context
.borrow()
.as_ref()
.map(|sc| sc.context.as_ref())
.is_some()
{
while let Some(curr) = end {
start = curr.clone();
if curr.node_type() == 8 {
// COMMENT
let v = curr.node_value();
if v == Some("#".to_string()) {
count += 1;
} else if v == Some("/".to_string()) {
count -= 1;
if count == 0 {
current.push(curr.clone());
return (curr, current);
with_runtime(self.runtime, |runtime| {
if runtime
.shared_context
.borrow()
.as_ref()
.map(|sc| sc.context.as_ref())
.is_some()
{
while let Some(curr) = end {
start = curr.clone();
if curr.node_type() == 8 {
// COMMENT
let v = curr.node_value();
if v == Some("#".to_string()) {
count += 1;
} else if v == Some("/".to_string()) {
count -= 1;
if count == 0 {
current.push(curr.clone());
return (curr, current);
}
}
}
current.push(curr.clone());
end = curr.next_sibling();
}
current.push(curr.clone());
end = curr.next_sibling();
}
}
(start, current)
(start, current)
})
}
/// On either the server side or the browser side, generates the next key in the hydration process.
pub fn next_hydration_key(&self) -> String {
let mut sc = self.runtime.shared_context.borrow_mut();
if let Some(ref mut sc) = *sc {
sc.next_hydration_key()
} else {
let mut new_sc = SharedContext::default();
let id = new_sc.next_hydration_key();
*sc = Some(new_sc);
id
}
with_runtime(self.runtime, |runtime| {
let mut sc = runtime.shared_context.borrow_mut();
if let Some(ref mut sc) = *sc {
sc.next_hydration_key()
} else {
let mut new_sc = SharedContext::default();
let id = new_sc.next_hydration_key();
*sc = Some(new_sc);
id
}
})
}
/// Runs the given function with the next hydration context.
pub fn with_next_context<T>(&self, f: impl FnOnce() -> T) -> T {
if self
.runtime
.shared_context
.borrow()
.as_ref()
.and_then(|sc| sc.context.as_ref())
.is_some()
{
let c = {
if let Some(ref mut sc) = *self.runtime.shared_context.borrow_mut() {
if let Some(ref mut context) = sc.context {
let next = context.next_hydration_context();
Some(std::mem::replace(context, next))
with_runtime(self.runtime, |runtime| {
if runtime
.shared_context
.borrow()
.as_ref()
.and_then(|sc| sc.context.as_ref())
.is_some()
{
let c = {
if let Some(ref mut sc) = *runtime.shared_context.borrow_mut() {
if let Some(ref mut context) = sc.context {
let next = context.next_hydration_context();
Some(std::mem::replace(context, next))
} else {
None
}
} else {
None
}
} else {
None
};
let res = self.untrack(f);
if let Some(ref mut sc) = *runtime.shared_context.borrow_mut() {
sc.context = c;
}
};
let res = self.untrack(f);
if let Some(ref mut sc) = *self.runtime.shared_context.borrow_mut() {
sc.context = c;
res
} else {
self.untrack(f)
}
res
} else {
self.untrack(f)
}
})
}
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
pub fn all_resources(&self) -> Vec<ResourceId> {
self.runtime.all_resources()
with_runtime(self.runtime, |runtime| runtime.all_resources())
}
/// The current key for an HTML fragment created by server-rendering a `<Suspense/>` component.
pub fn current_fragment_key(&self) -> String {
self.runtime
.shared_context
.borrow()
.as_ref()
.map(|context| context.current_fragment_key())
.unwrap_or_else(|| String::from("0f"))
with_runtime(self.runtime, |runtime| {
runtime
.shared_context
.borrow()
.as_ref()
.map(|context| context.current_fragment_key())
.unwrap_or_else(|| String::from("0f"))
})
}
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
pub fn serialization_resolvers(&self) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
self.runtime.serialization_resolvers()
with_runtime(self.runtime, |runtime| runtime.serialization_resolvers())
}
/// Registers the given [SuspenseContext](crate::SuspenseContext) with the current scope,
@ -426,33 +470,37 @@ impl Scope {
use crate::create_isomorphic_effect;
use futures::StreamExt;
if let Some(ref mut shared_context) = *self.runtime.shared_context.borrow_mut() {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
with_runtime(self.runtime, |runtime| {
if let Some(ref mut shared_context) = *runtime.shared_context.borrow_mut() {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
create_isomorphic_effect(*self, move |_| {
let pending = context.pending_resources.try_with(|n| *n).unwrap_or(0);
if pending == 0 {
_ = tx.unbounded_send(());
}
});
create_isomorphic_effect(*self, move |_| {
let pending = context.pending_resources.try_with(|n| *n).unwrap_or(0);
if pending == 0 {
_ = tx.unbounded_send(());
}
});
shared_context.pending_fragments.insert(
key.to_string(),
Box::pin(async move {
rx.next().await;
resolver()
}),
);
}
shared_context.pending_fragments.insert(
key.to_string(),
Box::pin(async move {
rx.next().await;
resolver()
}),
);
}
})
}
/// The set of all HTML fragments current pending, by their keys (see [Self::current_fragment_key]).
pub fn pending_fragments(&self) -> HashMap<String, Pin<Box<dyn Future<Output = String>>>> {
if let Some(ref mut shared_context) = *self.runtime.shared_context.borrow_mut() {
std::mem::take(&mut shared_context.pending_fragments)
} else {
HashMap::new()
}
with_runtime(self.runtime, |runtime| {
if let Some(ref mut shared_context) = *runtime.shared_context.borrow_mut() {
std::mem::take(&mut shared_context.pending_fragments)
} else {
HashMap::new()
}
})
}
}

View file

@ -11,10 +11,10 @@ use crate::{create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSig
/// because it reduces them from `O(n)` to `O(1)`.
///
/// ```
/// # use leptos_reactive::{create_isomorphic_effect, create_scope, create_selector, create_signal};
/// # use leptos_reactive::*;
/// # use std::rc::Rc;
/// # use std::cell::RefCell;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let is_selected = create_selector(cx, a);
/// let total_notifications = Rc::new(RefCell::new(0));

View file

@ -1,6 +1,7 @@
use crate::{
debug_warn, spawn_local, Runtime, Scope, ScopeProperty, UntrackedGettableSignal,
UntrackedSettableSignal,
debug_warn,
runtime::{with_runtime, RuntimeId},
spawn_local, Runtime, Scope, ScopeProperty, UntrackedGettableSignal, UntrackedSettableSignal,
};
use futures::Stream;
use std::{fmt::Debug, marker::PhantomData};
@ -18,7 +19,7 @@ use thiserror::Error;
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
///
/// // ✅ calling the getter clones and returns the value
@ -85,7 +86,7 @@ pub fn create_signal_from_stream<T>(
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
///
/// // ✅ calling the getter clones and returns the value
@ -116,7 +117,7 @@ pub struct ReadSignal<T>
where
T: 'static,
{
pub(crate) runtime: &'static Runtime,
pub(crate) runtime: RuntimeId,
pub(crate) id: SignalId,
pub(crate) ty: PhantomData<T>,
}
@ -142,7 +143,7 @@ where
/// the running effect to this signal.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
///
/// // ❌ unnecessarily clones the string
@ -166,7 +167,7 @@ where
#[cfg(feature = "hydrate")]
pub(crate) fn subscribe(&self) {
self.id.subscribe(self.runtime);
with_runtime(self.runtime, |runtime| self.id.subscribe(runtime))
}
/// Clones and returns the current value of the signal, and subscribes
@ -176,7 +177,7 @@ where
/// (`value.get()` is equivalent to `value.with(T::clone)`.)
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
///
/// // calling the getter clones and returns the value
@ -193,7 +194,7 @@ where
/// Applies the function to the current Signal, if it exists, and subscribes
/// the running effect.
pub(crate) fn try_with<U>(&self, f: impl FnOnce(&T) -> U) -> Result<U, SignalError> {
self.id.try_with(self.runtime, f)
with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f))
}
/// Generates a [Stream] that emits the new value of the signal whenever it changes.
@ -273,7 +274,7 @@ where
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
///
/// // ✅ calling the setter sets the value
@ -294,7 +295,7 @@ pub struct WriteSignal<T>
where
T: 'static,
{
pub(crate) runtime: &'static Runtime,
pub(crate) runtime: RuntimeId,
pub(crate) id: SignalId,
pub(crate) ty: PhantomData<T>,
}
@ -324,7 +325,7 @@ where
/// even if the value has not actually changed.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
///
/// // notifies subscribers
@ -347,7 +348,7 @@ where
/// even if the value has not actually changed.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 0);
///
/// // notifies subscribers
@ -414,7 +415,7 @@ where
/// or as a function argument.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let count = create_rw_signal(cx, 0);
///
/// // ✅ set the value
@ -441,7 +442,7 @@ pub fn create_rw_signal<T>(cx: Scope, value: T) -> RwSignal<T> {
/// its style, or it may be easier to pass around in a context or as a function argument.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let count = create_rw_signal(cx, 0);
///
/// // ✅ set the value
@ -462,7 +463,7 @@ pub struct RwSignal<T>
where
T: 'static,
{
pub(crate) runtime: &'static Runtime,
pub(crate) runtime: RuntimeId,
pub(crate) id: SignalId,
pub(crate) ty: PhantomData<T>,
}
@ -496,11 +497,11 @@ impl<T> UntrackedGettableSignal<T> for RwSignal<T> {
impl<T> UntrackedSettableSignal<T> for RwSignal<T> {
fn set_untracked(&self, new_value: T) {
self.id
.update_with_no_effect(self.runtime, |v| *v = new_value);
.update_with_no_effect(self.runtime, |v| *v = new_value)
}
fn update_untracked(&self, f: impl FnOnce(&mut T)) {
self.id.update_with_no_effect(self.runtime, f);
self.id.update_with_no_effect(self.runtime, f)
}
}
@ -512,7 +513,7 @@ where
/// the running effect to this signal.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let name = create_rw_signal(cx, "Alice".to_string());
///
/// // ❌ unnecessarily clones the string
@ -535,7 +536,7 @@ where
/// the running effect to this signal.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let count = create_rw_signal(cx, 0);
///
/// assert_eq!(count.get(), 0);
@ -556,7 +557,7 @@ where
/// and notifies subscribers that the signal has changed.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let count = create_rw_signal(cx, 0);
///
/// // notifies subscribers
@ -579,7 +580,7 @@ where
/// even if the value has not actually changed.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let count = create_rw_signal(cx, 0);
///
/// assert_eq!(count(), 0);
@ -597,7 +598,7 @@ where
/// to the signal and cause other parts of the DOM to update.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let count = create_rw_signal(cx, 0);
/// let read_count = count.read_only();
/// assert_eq!(count(), 0);
@ -726,78 +727,88 @@ impl SignalId {
self.try_with_no_subscription(runtime, f)
}
pub(crate) fn with_no_subscription<T, U>(&self, runtime: &Runtime, f: impl FnOnce(&T) -> U) -> U
pub(crate) fn with_no_subscription<T, U>(
&self,
runtime: RuntimeId,
f: impl FnOnce(&T) -> U,
) -> U
where
T: 'static,
{
self.try_with_no_subscription(runtime, f).unwrap()
with_runtime(runtime, |runtime| {
self.try_with_no_subscription(runtime, f).unwrap()
})
}
pub(crate) fn with<T, U>(&self, runtime: &Runtime, f: impl FnOnce(&T) -> U) -> U
pub(crate) fn with<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&T) -> U) -> U
where
T: 'static,
{
self.try_with(runtime, f).unwrap()
with_runtime(runtime, |runtime| self.try_with(runtime, f).unwrap())
}
fn update_value<T>(&self, runtime: &Runtime, f: impl FnOnce(&mut T)) -> bool
fn update_value<T>(&self, runtime: RuntimeId, f: impl FnOnce(&mut T)) -> bool
where
T: 'static,
{
let value = {
let signals = runtime.signals.borrow();
signals.get(*self).cloned()
};
if let Some(value) = value {
let mut value = value.borrow_mut();
if let Some(value) = value.downcast_mut::<T>() {
f(value);
true
with_runtime(runtime, |runtime| {
let value = {
let signals = runtime.signals.borrow();
signals.get(*self).cloned()
};
if let Some(value) = value {
let mut value = value.borrow_mut();
if let Some(value) = value.downcast_mut::<T>() {
f(value);
true
} else {
debug_warn!(
"[Signal::update] failed when downcasting to Signal<{}>",
std::any::type_name::<T>()
);
false
}
} else {
debug_warn!(
"[Signal::update] failed when downcasting to Signal<{}>",
"[Signal::update] Youre trying to update a Signal<{}> that has already been disposed of. This is probably either a logic error in a component that creates and disposes of scopes, or a Resource resolving after its scope has been dropped without having been cleaned up.",
std::any::type_name::<T>()
);
false
}
} else {
debug_warn!(
"[Signal::update] Youre trying to update a Signal<{}> that has already been disposed of. This is probably either a logic error in a component that creates and disposes of scopes, or a Resource resolving after its scope has been dropped without having been cleaned up.",
std::any::type_name::<T>()
);
false
}
})
}
pub(crate) fn update<T>(&self, runtime: &Runtime, f: impl FnOnce(&mut T))
pub(crate) fn update<T>(&self, runtime_id: RuntimeId, f: impl FnOnce(&mut T))
where
T: 'static,
{
// update the value
let updated = self.update_value(runtime, f);
with_runtime(runtime_id, |runtime| {
// update the value
let updated = self.update_value(runtime_id, f);
// notify subscribers
if updated {
let subs = {
let subs = runtime.signal_subscribers.borrow();
let subs = subs.get(*self);
subs.map(|subs| subs.borrow().clone())
};
if let Some(subs) = subs {
for sub in subs {
let effect = {
let effects = runtime.effects.borrow();
effects.get(sub).cloned()
};
if let Some(effect) = effect {
effect.run(sub, runtime);
// notify subscribers
if updated {
let subs = {
let subs = runtime.signal_subscribers.borrow();
let subs = subs.get(*self);
subs.map(|subs| subs.borrow().clone())
};
if let Some(subs) = subs {
for sub in subs {
let effect = {
let effects = runtime.effects.borrow();
effects.get(sub).cloned()
};
if let Some(effect) = effect {
effect.run(sub, runtime_id);
}
}
}
}
}
})
}
pub(crate) fn update_with_no_effect<T>(&self, runtime: &Runtime, f: impl FnOnce(&mut T))
pub(crate) fn update_with_no_effect<T>(&self, runtime: RuntimeId, f: impl FnOnce(&mut T))
where
T: 'static,
{

View file

@ -10,8 +10,8 @@ use crate::{Memo, ReadSignal, RwSignal, Scope, UntrackedGettableSignal};
/// function call, `with()`, and `get()` APIs as over signals.
///
/// ```rust
/// # use leptos_reactive::{create_scope, create_signal, create_rw_signal, create_memo, Signal};
/// # create_scope(|cx| {
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = Signal::derive(cx, move || count() * 2);
/// let memoized_double_count = create_memo(cx, move |_| count() * 2);
@ -73,8 +73,8 @@ where
/// Wraps a derived signal, i.e., any computation that accesses one or more
/// reactive signals.
/// ```rust
/// # use leptos_reactive::{create_scope, create_signal, create_rw_signal, create_memo, Signal};
/// # create_scope(|cx| {
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = Signal::derive(cx, move || count() * 2);
///
@ -95,7 +95,7 @@ where
/// the running effect to this signal.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
/// let name_upper = Signal::derive(cx, move || name.with(|n| n.to_uppercase()));
/// let memoized_lower = create_memo(cx, move |_| name.with(|n| n.to_lowercase()));
@ -134,8 +134,8 @@ where
/// If you want to get the value without cloning it, use [ReadSignal::with].
/// (Theres no difference in behavior for derived signals: they re-run in any case.)
/// ```
/// # use leptos_reactive::{create_scope, create_signal, create_rw_signal, create_memo, Signal};
/// # create_scope(|cx| {
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = Signal::derive(cx, move || count() * 2);
/// let memoized_double_count = create_memo(cx, move |_| count() * 2);
@ -258,7 +258,7 @@ where
///
/// ```rust
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = MaybeSignal::derive(cx, move || count() * 2);
/// let memoized_double_count = create_memo(cx, move |_| count() * 2);
@ -317,8 +317,8 @@ where
/// Wraps a derived signal, i.e., any computation that accesses one or more
/// reactive signals.
/// ```rust
/// # use leptos_reactive::{create_scope, create_signal, create_rw_signal, create_memo, Signal};
/// # create_scope(|cx| {
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = Signal::derive(cx, move || count() * 2);
///
@ -339,7 +339,7 @@ where
/// the running effect to this signal.
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (name, set_name) = create_signal(cx, "Alice".to_string());
/// let name_upper = MaybeSignal::derive(cx, move || name.with(|n| n.to_uppercase()));
/// let memoized_lower = create_memo(cx, move |_| name.with(|n| n.to_lowercase()));
@ -381,7 +381,7 @@ where
/// (Theres no difference in behavior for derived signals: they re-run in any case.)
/// ```
/// # use leptos_reactive::*;
/// # create_scope(|cx| {
/// # create_scope(create_runtime(), |cx| {
/// let (count, set_count) = create_signal(cx, 2);
/// let double_count = MaybeSignal::derive(cx, move || count() * 2);
/// let memoized_double_count = create_memo(cx, move |_| count() * 2);

View file

@ -1,5 +1,7 @@
#[cfg(not(feature = "stable"))]
use leptos_reactive::{create_isomorphic_effect, create_memo, create_scope, create_signal};
use leptos_reactive::{
create_isomorphic_effect, create_memo, create_runtime, create_scope, create_signal,
};
#[cfg(not(feature = "stable"))]
#[test]
@ -7,7 +9,7 @@ fn effect_runs() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
// simulate an arbitrary side effect
@ -36,7 +38,7 @@ fn effect_tracks_memo() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
let b = create_memo(cx, move |_| format!("Value is {}", a()));
@ -67,7 +69,7 @@ fn untrack_mutes_effect() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
// simulate an arbitrary side effect

View file

@ -1,10 +1,10 @@
#[cfg(not(feature = "stable"))]
use leptos_reactive::{create_memo, create_scope, create_signal};
use leptos_reactive::{create_memo, create_runtime, create_scope, create_signal};
#[cfg(not(feature = "stable"))]
#[test]
fn basic_memo() {
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let a = create_memo(cx, |_| 5);
assert_eq!(a(), 5);
})
@ -14,7 +14,7 @@ fn basic_memo() {
#[cfg(not(feature = "stable"))]
#[test]
fn memo_with_computed_value() {
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = create_memo(cx, move |_| a() + b());
@ -30,7 +30,7 @@ fn memo_with_computed_value() {
#[cfg(not(feature = "stable"))]
#[test]
fn nested_memos() {
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = create_memo(cx, move |_| a() + b());
@ -54,7 +54,7 @@ fn nested_memos() {
fn memo_runs_only_when_inputs_change() {
use std::{cell::Cell, rc::Rc};
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let call_count = Rc::new(Cell::new(0));
let (a, set_a) = create_signal(cx, 0);
let (b, _) = create_signal(cx, 0);

View file

@ -1,10 +1,10 @@
#[cfg(not(feature = "stable"))]
use leptos_reactive::{create_scope, create_signal};
use leptos_reactive::{create_runtime, create_scope, create_signal};
#[cfg(not(feature = "stable"))]
#[test]
fn basic_signal() {
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
assert_eq!(a(), 0);
set_a(5);
@ -16,7 +16,7 @@ fn basic_signal() {
#[cfg(not(feature = "stable"))]
#[test]
fn derived_signals() {
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = move || a() + b();

View file

@ -1,6 +1,6 @@
//#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_isomorphic_effect, create_scope, create_signal, UntrackedGettableSignal,
create_isomorphic_effect, create_runtime, create_scope, create_signal, UntrackedGettableSignal,
UntrackedSettableSignal,
};
@ -10,7 +10,7 @@ fn untracked_set_doesnt_trigger_effect() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
// simulate an arbitrary side effect
@ -42,7 +42,7 @@ fn untracked_get_doesnt_trigger_effect() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
create_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, -1);
let (a2, set_a2) = create_signal(cx, 1);

View file

@ -1,6 +1,6 @@
[package]
name = "leptos_server"
version = "0.0.17"
version = "0.0.18"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@ -8,8 +8,8 @@ repository = "https://github.com/gbj/leptos"
description = "RPC for the Leptos web framework."
[dependencies]
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.17" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.17" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.18" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.18" }
form_urlencoded = "1"
gloo-net = "0.2"
lazy_static = "1"

View file

@ -9,9 +9,9 @@ use std::{future::Future, pin::Pin, rc::Rc};
/// run an `async` function in response to something like a user clicking a button, you're in the right place.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_reactive::*;
/// # use leptos_server::create_action;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
@ -60,9 +60,9 @@ use std::{future::Future, pin::Pin, rc::Rc};
/// function, because it is stored in [Action::input] as well.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_reactive::*;
/// # use leptos_server::create_action;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// // if there's a single argument, just use that
/// let action1 = create_action(cx, |input: &String| {
/// let input = input.clone();
@ -150,9 +150,9 @@ where
/// you're in the right place.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_reactive::*;
/// # use leptos_server::create_action;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
@ -201,9 +201,9 @@ where
/// function, because it is stored in [Action::input] as well.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_reactive::*;
/// # use leptos_server::create_action;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// // if there's a single argument, just use that
/// let action1 = create_action(cx, |input: &String| {
/// let input = input.clone();
@ -246,7 +246,7 @@ where
/// Creates an [Action] that can be used to call a server function.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_reactive::*;
/// # use leptos_server::{create_server_action, ServerFnError, ServerFn};
/// # use leptos_macro::server;
///
@ -255,7 +255,7 @@ where
/// todo!()
/// }
///
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// let my_server_action = create_server_action::<MyServerFn>(cx);
/// # });
/// ```

View file

@ -40,7 +40,7 @@
//! }
//!
//! // call the function
//! # run_scope(|cx| {
//! # run_scope(create_runtime(), |cx| {
//! spawn_local(async {
//! let posts = read_posts(3, "my search".to_string()).await;
//! log::debug!("posts = {posts{:#?}");

View file

@ -14,9 +14,9 @@ use std::{future::Future, pin::Pin, rc::Rc};
/// youre in the right place.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_reactive::*;
/// # use leptos_server::create_multi_action;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
@ -41,9 +41,9 @@ use std::{future::Future, pin::Pin, rc::Rc};
/// function, because it is stored in [Submission::input] as well.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_reactive::*;
/// # use leptos_server::create_multi_action;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// // if there's a single argument, just use that
/// let action1 = create_multi_action(cx, |input: &String| {
/// let input = input.clone();
@ -186,9 +186,9 @@ where
/// you're in the right place.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_reactive::*;
/// # use leptos_server::create_multi_action;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
@ -214,9 +214,9 @@ where
/// function, because it is stored in [Submission::input] as well.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_reactive::*;
/// # use leptos_server::create_multi_action;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// // if there's a single argument, just use that
/// let action1 = create_multi_action(cx, |input: &String| {
/// let input = input.clone();
@ -256,7 +256,7 @@ where
/// Creates an [MultiAction] that can be used to call a server function.
///
/// ```rust
/// # use leptos_reactive::run_scope;
/// # use leptos_reactive::*;
/// # use leptos_server::{create_server_multi_action, ServerFnError, ServerFn};
/// # use leptos_macro::server;
///
@ -265,7 +265,7 @@ where
/// todo!()
/// }
///
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// let my_server_multi_action = create_server_multi_action::<MyServerFn>(cx);
/// # });
/// ```

View file

@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.0.1"
version = "0.0.2"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View file

@ -92,7 +92,7 @@ impl MetaContext {
/// use leptos_meta::*;
///
/// # #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
/// run_scope(|cx| {
/// run_scope(create_runtime(), |cx| {
/// provide_context(cx, MetaContext::new());
///
/// let app = view! { cx,

View file

@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.0.3"
version = "0.0.4"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View file

@ -64,7 +64,7 @@ where
let action_version = version;
let action = use_resolved_path(cx, move || action.to_href()());
let on_submit = move |ev: web_sys::Event| {
let on_submit = move |ev: web_sys::SubmitEvent| {
if ev.default_prevented() {
return;
}
@ -262,7 +262,7 @@ where
""
}.to_string();
let on_submit = move |ev: web_sys::Event| {
let on_submit = move |ev: web_sys::SubmitEvent| {
if ev.default_prevented() {
return;
}

View file

@ -119,7 +119,7 @@ impl History for BrowserIntegration {
/// ```
/// # use leptos_router::*;
/// # use leptos::*;
/// # run_scope(|cx| {
/// # run_scope(create_runtime(), |cx| {
/// let integration = ServerIntegration { path: "insert/current/path/here".to_string() };
/// provide_context(cx, RouterIntegrationContext::new(integration));
/// # });