diff --git a/Cargo.lock b/Cargo.lock index 2af34ca4a5..ec125a52a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1057,7 +1057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -2884,6 +2884,7 @@ dependencies = [ "hydration_context", "indexmap", "or_poisoned", + "paste", "pin-project-lite", "rustc-hash 2.1.1", "rustc_version", @@ -2895,6 +2896,7 @@ dependencies = [ "tokio", "tokio-test", "tracing", + "typed-builder", "web-sys", ] @@ -3144,7 +3146,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -3813,7 +3815,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -4271,18 +4273,18 @@ dependencies = [ [[package]] name = "typed-builder" -version = "0.21.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef81aec2ca29576f9f6ae8755108640d0a86dd3161b2e8bca6cfa554e98f77d" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" dependencies = [ "typed-builder-macro", ] [[package]] name = "typed-builder-macro" -version = "0.21.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecb9ecf7799210407c14a8cfdfe0173365780968dc57973ed082211958e0b18" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" dependencies = [ "proc-macro2", "quote", @@ -4620,7 +4622,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a86392695b..44a20ddacf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,8 @@ itertools = { default-features = false, version = "0.14.0" } convert_case = { default-features = false, version = "0.8.0" } serde_json = { default-features = false, version = "1.0.143" } trybuild = { default-features = false, version = "1.0.110" } -typed-builder = { default-features = false, version = "0.21.2" } +typed-builder = { default-features = false, version = "0.22.0" } +typed-builder-macro = { default-features = false, version = "0.22.0" } thiserror = { default-features = false, version = "2.0.16" } wasm-bindgen = { default-features = false, version = "0.2.100" } indexmap = { default-features = false, version = "2.11.0" } @@ -123,7 +124,6 @@ serial_test = { default-features = false, version = "3.2.0" } erased = { default-features = false, version = "0.1.2" } glib = { default-features = false, version = "0.20.12" } async-trait = { default-features = false, version = "0.1.89" } -typed-builder-macro = { default-features = false, version = "0.21.0" } linear-map = { default-features = false, version = "1.2.0" } anyhow = { default-features = false, version = "1.0.100" } walkdir = { default-features = false, version = "2.5.0" } diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index 274a6cf34a..d187647dd3 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -203,7 +203,7 @@ pub mod prelude { pub mod form; /// A standard way to wrap functions and closures to pass them to components. -pub mod callback; +pub use reactive_graph::callback; /// Types that can be passed as the `children` prop of a component. pub mod children; diff --git a/leptos_macro/src/component.rs b/leptos_macro/src/component.rs index 52e5f6149f..dbf9dc020b 100644 --- a/leptos_macro/src/component.rs +++ b/leptos_macro/src/component.rs @@ -1016,25 +1016,27 @@ struct PropOpt { name: Option, } -struct TypedBuilderOpts { +struct TypedBuilderOpts<'a> { default: bool, default_with_value: Option, strip_option: bool, into: bool, + ty: &'a Type, } -impl TypedBuilderOpts { - fn from_opts(opts: &PropOpt, is_ty_option: bool) -> Self { +impl<'a> TypedBuilderOpts<'a> { + fn from_opts(opts: &PropOpt, ty: &'a Type) -> Self { Self { default: opts.optional || opts.optional_no_strip || opts.attrs, default_with_value: opts.default.clone(), - strip_option: opts.strip_option || opts.optional && is_ty_option, + strip_option: opts.strip_option || opts.optional && is_option(ty), into: opts.into, + ty, } } } -impl TypedBuilderOpts { +impl TypedBuilderOpts<'_> { fn to_serde_tokens(&self) -> TokenStream { let default = if let Some(v) = &self.default_with_value { let v = v.to_token_stream().to_string(); @@ -1053,7 +1055,7 @@ impl TypedBuilderOpts { } } -impl ToTokens for TypedBuilderOpts { +impl ToTokens for TypedBuilderOpts<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { let default = if let Some(v) = &self.default_with_value { let v = v.to_token_stream().to_string(); @@ -1064,14 +1066,29 @@ impl ToTokens for TypedBuilderOpts { quote! {} }; - let strip_option = if self.strip_option { + // If self.strip_option && self.into, then the strip_option will be represented as part of the transform closure. + let strip_option = if self.strip_option && !self.into { quote! { strip_option, } } else { quote! {} }; let into = if self.into { - quote! { into, } + if !self.strip_option { + let ty = &self.ty; + quote! { + fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> #ty { + value.into_reactive_value() + }, + } + } else { + let ty = unwrap_option(self.ty); + quote! { + fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> Option<#ty> { + Some(value.into_reactive_value()) + }, + } + } } else { quote! {} }; @@ -1107,8 +1124,7 @@ fn prop_builder_fields( ty, } = prop; - let builder_attrs = - TypedBuilderOpts::from_opts(prop_opts, is_option(ty)); + let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, ty); let builder_docs = prop_to_doc(prop, PropDocStyle::Inline); @@ -1153,8 +1169,7 @@ fn prop_serializer_fields(vis: &Visibility, props: &[Prop]) -> TokenStream { ty, } = prop; - let builder_attrs = - TypedBuilderOpts::from_opts(prop_opts, is_option(ty)); + let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, ty); let serde_attrs = builder_attrs.to_serde_tokens(); let PatIdent { ident, by_ref, .. } = &name; diff --git a/leptos_macro/src/slot.rs b/leptos_macro/src/slot.rs index 229afa05cc..c4593818fe 100644 --- a/leptos_macro/src/slot.rs +++ b/leptos_macro/src/slot.rs @@ -159,25 +159,27 @@ struct PropOpt { pub attrs: bool, } -struct TypedBuilderOpts { +struct TypedBuilderOpts<'a> { default: bool, default_with_value: Option, strip_option: bool, into: bool, + ty: &'a Type, } -impl TypedBuilderOpts { - pub fn from_opts(opts: &PropOpt, is_ty_option: bool) -> Self { +impl<'a> TypedBuilderOpts<'a> { + pub fn from_opts(opts: &PropOpt, ty: &'a Type) -> Self { Self { default: opts.optional || opts.optional_no_strip || opts.attrs, default_with_value: opts.default.clone(), - strip_option: opts.strip_option || opts.optional && is_ty_option, + strip_option: opts.strip_option || opts.optional && is_option(ty), into: opts.into, + ty, } } } -impl ToTokens for TypedBuilderOpts { +impl ToTokens for TypedBuilderOpts<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { let default = if let Some(v) = &self.default_with_value { let v = v.to_token_stream().to_string(); @@ -188,14 +190,29 @@ impl ToTokens for TypedBuilderOpts { quote! {} }; - let strip_option = if self.strip_option { + // If self.strip_option && self.into, then the strip_option will be represented as part of the transform closure. + let strip_option = if self.strip_option && !self.into { quote! { strip_option, } } else { quote! {} }; let into = if self.into { - quote! { into, } + if !self.strip_option { + let ty = &self.ty; + quote! { + fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> #ty { + value.into_reactive_value() + }, + } + } else { + let ty = unwrap_option(self.ty); + quote! { + fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> Option<#ty> { + Some(value.into_reactive_value()) + }, + } + } } else { quote! {} }; @@ -227,8 +244,7 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream { ty, } = prop; - let builder_attrs = - TypedBuilderOpts::from_opts(prop_opts, is_option(ty)); + let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, ty); let builder_docs = prop_to_doc(prop, PropDocStyle::Inline); diff --git a/leptos_macro/src/view/component_builder.rs b/leptos_macro/src/view/component_builder.rs index dda88f5ae0..2f61988b3f 100644 --- a/leptos_macro/src/view/component_builder.rs +++ b/leptos_macro/src/view/component_builder.rs @@ -90,7 +90,7 @@ pub(crate) fn component_to_tokens( if optional { optional_props.push(quote! { - props.#name = { #value }.map(Into::into); + props.#name = { #value }.map(::leptos::prelude::IntoReactiveValue::into_reactive_value); }) } else { required_props.push(quote! { diff --git a/leptos_macro/tests/component.rs b/leptos_macro/tests/component.rs index 0c6b62d994..c1980a33ed 100644 --- a/leptos_macro/tests/component.rs +++ b/leptos_macro/tests/component.rs @@ -120,6 +120,122 @@ fn returns_static_lifetime() { } } +#[component] +pub fn IntoReactiveValueTestComponentSignal( + #[prop(into)] arg1: Signal, + #[prop(into)] arg2: Signal, + #[prop(into)] arg3: Signal, + #[prop(into)] arg4: Signal, + #[prop(into)] arg5: Signal, + #[prop(into)] arg6: Signal, + #[prop(into)] arg7: Signal>, + #[prop(into)] arg8: ArcSignal, + #[prop(into)] arg9: ArcSignal, + #[prop(into)] arg10: ArcSignal, + #[prop(into)] arg11: ArcSignal, + #[prop(into)] arg12: ArcSignal, + #[prop(into)] arg13: ArcSignal, + #[prop(into)] arg14: ArcSignal>, + // Optionals: + #[prop(into, optional)] arg15: Option>, + #[prop(into, optional)] arg16_purposely_omitted: Option>, + #[prop(into, optional)] arg17: Option>, + #[prop(into, strip_option)] arg18: Option>, +) -> impl IntoView { + move || { + view! { +
+

{arg1.get()}

+

{arg2.get()}

+

{arg3.get()}

+

{arg4.get()}

+

{arg5.get()}

+

{arg6.get()}

+

{arg7.get()}

+

{arg8.get()}

+

{arg9.get()}

+

{arg10.get()}

+

{arg11.get()}

+

{arg12.get()}

+

{arg13.get()}

+

{arg14.get()}

+

{arg15.get()}

+

{arg16_purposely_omitted.get()}

+

{arg17.get()}

+

{arg18.get()}

+
+ } + } +} + +#[component] +pub fn IntoReactiveValueTestComponentCallback( + #[prop(into)] arg1: Callback<(), String>, + #[prop(into)] arg2: Callback, + #[prop(into)] arg3: Callback<(usize,), String>, + #[prop(into)] arg4: Callback<(usize, String), String>, + #[prop(into)] arg5: UnsyncCallback<(), String>, + #[prop(into)] arg6: UnsyncCallback, + #[prop(into)] arg7: UnsyncCallback<(usize,), String>, + #[prop(into)] arg8: UnsyncCallback<(usize, String), String>, +) -> impl IntoView { + move || { + view! { +
+

{arg1.run(())}

+

{arg2.run(1)}

+

{arg3.run((2,))}

+

{arg4.run((3, "three".into()))}

+

{arg5.run(())}

+

{arg6.run(1)}

+

{arg7.run((2,))}

+

{arg8.run((3, "three".into()))}

+
+ } + } +} + +#[test] +fn test_into_reactive_value_signal() { + let _ = view! { + + }; +} + +#[test] +fn test_into_reactive_value_callback() { + let _ = view! { + + }; +} + // an attempt to catch unhygienic macros regression mod macro_hygiene { // To ensure no relative module path to leptos inside macros. @@ -152,12 +268,7 @@ mod macro_hygiene { #[component] fn Component() -> impl IntoView { - view! { -
- {().into_any()} - {()} -
- } + view! {
{().into_any()} {()}
} } } } diff --git a/reactive_graph/Cargo.toml b/reactive_graph/Cargo.toml index a073966e5c..3d70a757e5 100644 --- a/reactive_graph/Cargo.toml +++ b/reactive_graph/Cargo.toml @@ -29,6 +29,7 @@ send_wrapper = { features = [ ], workspace = true, default-features = true } subsecond = { workspace = true, default-features = true, optional = true } indexmap = { workspace = true, default-features = true } +paste = { workspace = true, default-features = true } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] web-sys = { version = "0.3.77", features = ["console"] } @@ -40,6 +41,7 @@ tokio = { features = [ ], workspace = true, default-features = true } tokio-test = { workspace = true, default-features = true } any_spawner = { workspace = true, features = ["futures-executor", "tokio"] } +typed-builder.workspace = true [build-dependencies] rustc_version = { workspace = true, default-features = true } diff --git a/leptos/src/callback.rs b/reactive_graph/src/callback.rs similarity index 67% rename from leptos/src/callback.rs rename to reactive_graph/src/callback.rs index 4daf12da9c..bfe7a61388 100644 --- a/leptos/src/callback.rs +++ b/reactive_graph/src/callback.rs @@ -2,48 +2,19 @@ //! for component properties, because they can be used to define optional callback functions, //! which generic props don’t support. //! -//! # Usage -//! Callbacks can be created manually from any function or closure, but the easiest way -//! to create them is to use `#[prop(into)]]` when defining a component. -//! ``` -//! use leptos::prelude::*; -//! -//! #[component] -//! fn MyComponent( -//! #[prop(into)] render_number: Callback<(i32,), String>, -//! ) -> impl IntoView { -//! view! { -//!
-//! {render_number.run((1,))} -//! // callbacks can be called multiple times -//! {render_number.run((42,))} -//!
-//! } -//! } -//! // you can pass a closure directly as `render_number` -//! fn test() -> impl IntoView { -//! view! { -//! -//! } -//! } -//! ``` -//! -//! *Notes*: -//! - The `render_number` prop can receive any type that implements `Fn(i32) -> String`. -//! - Callbacks are most useful when you want optional generic props. -//! - All callbacks implement the [`Callable`](leptos::callback::Callable) trait, and can be invoked with `my_callback.run(input)`. -//! - The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals. +//! The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals. //! //! # Types //! This modules implements 2 callback types: -//! - [`Callback`](leptos::callback::Callback) -//! - [`UnsyncCallback`](leptos::callback::UnsyncCallback) +//! - [`Callback`](reactive_graph::callback::Callback) +//! - [`UnsyncCallback`](reactive_graph::callback::UnsyncCallback) //! //! Use `SyncCallback` if the function is not `Sync` and `Send`. -use reactive_graph::{ +use crate::{ owner::{LocalStorage, StoredValue}, traits::{Dispose, WithValue}, + IntoReactiveValue, }; use std::{fmt, rc::Rc, sync::Arc}; @@ -60,7 +31,16 @@ pub trait Callable { fn run(&self, input: In) -> Out; } -/// A callback type that is not required to be `Send + Sync`. +/// A callback type that is not required to be [`Send`] or [`Sync`]. +/// +/// # Example +/// ``` +/// # use reactive_graph::prelude::*; use reactive_graph::callback::*; let owner = reactive_graph::owner::Owner::new(); owner.set(); +/// let _: UnsyncCallback<()> = UnsyncCallback::new(|_| {}); +/// let _: UnsyncCallback<(i32, i32)> = (|_x: i32, _y: i32| {}).into(); +/// let cb: UnsyncCallback = UnsyncCallback::new(|x: i32| x.to_string()); +/// assert_eq!(cb.run(42), "42".to_string()); +/// ``` pub struct UnsyncCallback( StoredValue Out>, LocalStorage>, ); @@ -148,28 +128,15 @@ impl_unsync_callable_from_fn!( P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12 ); -/// Callbacks define a standard way to store functions and closures. +/// A callback type that is [`Send`] + [`Sync`]. /// /// # Example /// ``` -/// # use leptos::prelude::*; -/// # use leptos::callback::{Callable, Callback}; -/// #[component] -/// fn MyComponent( -/// #[prop(into)] render_number: Callback<(i32,), String>, -/// ) -> impl IntoView { -/// view! { -///
-/// {render_number.run((42,))} -///
-/// } -/// } -/// -/// fn test() -> impl IntoView { -/// view! { -/// -/// } -/// } +/// # use reactive_graph::prelude::*; use reactive_graph::callback::*; let owner = reactive_graph::owner::Owner::new(); owner.set(); +/// let _: Callback<()> = Callback::new(|_| {}); +/// let _: Callback<(i32, i32)> = (|_x: i32, _y: i32| {}).into(); +/// let cb: Callback = Callback::new(|x: i32| x.to_string()); +/// assert_eq!(cb.run(42), "42".to_string()); /// ``` pub struct Callback( StoredValue Out + Send + Sync>>, @@ -241,6 +208,7 @@ impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12); impl Callback { /// Creates a new callback from the given function. + #[track_caller] pub fn new(fun: F) -> Self where F: Fn(In) -> Out + Send + Sync + 'static, @@ -262,22 +230,94 @@ impl Callback { } } +#[doc(hidden)] +pub struct __IntoReactiveValueMarkerCallbackSingleParam; + +#[doc(hidden)] +pub struct __IntoReactiveValueMarkerCallbackStrOutputToString; + +impl + IntoReactiveValue< + Callback, + __IntoReactiveValueMarkerCallbackSingleParam, + > for F +where + F: Fn(I) -> O + Send + Sync + 'static, +{ + #[track_caller] + fn into_reactive_value(self) -> Callback { + Callback::new(self) + } +} + +impl + IntoReactiveValue< + UnsyncCallback, + __IntoReactiveValueMarkerCallbackSingleParam, + > for F +where + F: Fn(I) -> O + 'static, +{ + #[track_caller] + fn into_reactive_value(self) -> UnsyncCallback { + UnsyncCallback::new(self) + } +} + +impl + IntoReactiveValue< + Callback, + __IntoReactiveValueMarkerCallbackStrOutputToString, + > for F +where + F: Fn(I) -> &'static str + Send + Sync + 'static, +{ + #[track_caller] + fn into_reactive_value(self) -> Callback { + Callback::new(move |i| self(i).to_string()) + } +} + +impl + IntoReactiveValue< + UnsyncCallback, + __IntoReactiveValueMarkerCallbackStrOutputToString, + > for F +where + F: Fn(I) -> &'static str + 'static, +{ + #[track_caller] + fn into_reactive_value(self) -> UnsyncCallback { + UnsyncCallback::new(move |i| self(i).to_string()) + } +} + #[cfg(test)] mod tests { use super::Callable; - use crate::callback::{Callback, UnsyncCallback}; - use reactive_graph::traits::Dispose; + use crate::{ + callback::{Callback, UnsyncCallback}, + owner::Owner, + traits::Dispose, + IntoReactiveValue, + }; struct NoClone {} #[test] fn clone_callback() { + let owner = Owner::new(); + owner.set(); + let callback = Callback::new(move |_no_clone: NoClone| NoClone {}); let _cloned = callback; } #[test] fn clone_unsync_callback() { + let owner = Owner::new(); + owner.set(); + let callback = UnsyncCallback::new(move |_no_clone: NoClone| NoClone {}); let _cloned = callback; @@ -285,20 +325,39 @@ mod tests { #[test] fn runback_from() { + let owner = Owner::new(); + owner.set(); + let _callback: Callback<(), String> = (|| "test").into(); let _callback: Callback<(i32, String), String> = (|num, s| format!("{num} {s}")).into(); + // Single params should work without needing the (foo,) tuple using IntoReactiveValue: + let _callback: Callback = + (|_usize| "test").into_reactive_value(); + let _callback: Callback = + (|_usize| "test").into_reactive_value(); } #[test] fn sync_callback_from() { + let owner = Owner::new(); + owner.set(); + let _callback: UnsyncCallback<(), String> = (|| "test").into(); let _callback: UnsyncCallback<(i32, String), String> = (|num, s| format!("{num} {s}")).into(); + // Single params should work without needing the (foo,) tuple using IntoReactiveValue: + let _callback: UnsyncCallback = + (|_usize| "test").into_reactive_value(); + let _callback: UnsyncCallback = + (|_usize| "test").into_reactive_value(); } #[test] fn sync_callback_try_run() { + let owner = Owner::new(); + owner.set(); + let callback = Callback::new(move |arg| arg); assert_eq!(callback.try_run((0,)), Some((0,))); callback.dispose(); @@ -307,6 +366,9 @@ mod tests { #[test] fn unsync_callback_try_run() { + let owner = Owner::new(); + owner.set(); + let callback = UnsyncCallback::new(move |arg| arg); assert_eq!(callback.try_run((0,)), Some((0,))); callback.dispose(); @@ -315,6 +377,9 @@ mod tests { #[test] fn callback_matches_same() { + let owner = Owner::new(); + owner.set(); + let callback1 = Callback::new(|x: i32| x * 2); let callback2 = callback1; assert!(callback1.matches(&callback2)); @@ -322,6 +387,9 @@ mod tests { #[test] fn callback_matches_different() { + let owner = Owner::new(); + owner.set(); + let callback1 = Callback::new(|x: i32| x * 2); let callback2 = Callback::new(|x: i32| x + 1); assert!(!callback1.matches(&callback2)); @@ -329,6 +397,9 @@ mod tests { #[test] fn unsync_callback_matches_same() { + let owner = Owner::new(); + owner.set(); + let callback1 = UnsyncCallback::new(|x: i32| x * 2); let callback2 = callback1; assert!(callback1.matches(&callback2)); @@ -336,6 +407,9 @@ mod tests { #[test] fn unsync_callback_matches_different() { + let owner = Owner::new(); + owner.set(); + let callback1 = UnsyncCallback::new(|x: i32| x * 2); let callback2 = UnsyncCallback::new(|x: i32| x + 1); assert!(!callback1.matches(&callback2)); diff --git a/reactive_graph/src/into_reactive_value.rs b/reactive_graph/src/into_reactive_value.rs new file mode 100644 index 0000000000..f4b0d2a652 --- /dev/null +++ b/reactive_graph/src/into_reactive_value.rs @@ -0,0 +1,64 @@ +#[doc(hidden)] +pub struct __IntoReactiveValueMarkerBaseCase; + +/// A helper trait that works like `Into` but uses a marker generic +/// to allow more `From` implementations than would be allowed with just `Into`. +pub trait IntoReactiveValue { + /// Converts `self` into a `T`. + fn into_reactive_value(self) -> T; +} + +// The base case, which allows anything which implements .into() to work: +impl IntoReactiveValue for I +where + I: Into, +{ + fn into_reactive_value(self) -> T { + self.into() + } +} + +#[cfg(test)] +mod tests { + + use crate::{ + into_reactive_value::IntoReactiveValue, + owner::{LocalStorage, Owner}, + traits::GetUntracked, + wrappers::read::Signal, + }; + use typed_builder::TypedBuilder; + + #[test] + fn test_into_signal_compiles() { + let owner = Owner::new(); + owner.set(); + + let _: Signal = (|| 2).into_reactive_value(); + let _: Signal = 2.into_reactive_value(); + let _: Signal = (|| 2).into_reactive_value(); + let _: Signal = "str".into_reactive_value(); + let _: Signal = "str".into_reactive_value(); + + #[derive(TypedBuilder)] + struct Foo { + #[builder(setter( + fn transform(value: impl IntoReactiveValue, M>) { + value.into_reactive_value() + } + ))] + sig: Signal, + } + + assert_eq!(Foo::builder().sig(2).build().sig.get_untracked(), 2); + assert_eq!(Foo::builder().sig(|| 2).build().sig.get_untracked(), 2); + assert_eq!( + Foo::builder() + .sig(Signal::stored(2)) + .build() + .sig + .get_untracked(), + 2 + ); + } +} diff --git a/reactive_graph/src/lib.rs b/reactive_graph/src/lib.rs index fd1018961f..8361e4a435 100644 --- a/reactive_graph/src/lib.rs +++ b/reactive_graph/src/lib.rs @@ -70,6 +70,8 @@ #![cfg_attr(all(feature = "nightly", rustc_nightly), feature(unboxed_closures))] #![cfg_attr(all(feature = "nightly", rustc_nightly), feature(fn_traits))] +#![cfg_attr(all(feature = "nightly", rustc_nightly), feature(auto_traits))] +#![cfg_attr(all(feature = "nightly", rustc_nightly), feature(negative_impls))] #![deny(missing_docs)] use std::{fmt::Arguments, future::Future}; @@ -90,6 +92,12 @@ pub mod traits; pub mod transition; pub mod wrappers; +mod into_reactive_value; +pub use into_reactive_value::*; + +/// A standard way to wrap functions and closures to pass them to components. +pub mod callback; + use computed::ScopedFuture; #[cfg(all(feature = "nightly", rustc_nightly))] @@ -97,7 +105,9 @@ mod nightly; /// Reexports frequently-used traits. pub mod prelude { - pub use crate::{owner::FromLocal, traits::*}; + pub use crate::{ + into_reactive_value::IntoReactiveValue, owner::FromLocal, traits::*, + }; } // TODO remove this, it's just useful while developing diff --git a/reactive_graph/src/wrappers.rs b/reactive_graph/src/wrappers.rs index b91a27b285..b09fb8dae6 100644 --- a/reactive_graph/src/wrappers.rs +++ b/reactive_graph/src/wrappers.rs @@ -324,6 +324,22 @@ pub mod read { } } + impl From<&'static str> for ArcSignal + where + S: Storage<&'static str> + Storage, + { + #[track_caller] + fn from(value: &'static str) -> Self { + Self { + inner: SignalTypes::Stored(ArcStoredValue::new( + value.to_string(), + )), + #[cfg(any(debug_assertions, leptos_debuginfo))] + defined_at: std::panic::Location::caller(), + } + } + } + impl DefinedAt for ArcSignal where S: Storage, @@ -1049,6 +1065,13 @@ pub mod read { } } + impl From> for Signal { + #[track_caller] + fn from(value: Signal<&'static str, LocalStorage>) -> Self { + Signal::derive_local(move || value.read().to_string()) + } + } + impl From> for Signal { #[track_caller] fn from(value: Signal<&'static str>) -> Self { @@ -1077,6 +1100,15 @@ pub mod read { } } + impl From, LocalStorage>> + for Signal, LocalStorage> + { + #[track_caller] + fn from(value: Signal, LocalStorage>) -> Self { + Signal::derive_local(move || value.read().map(str::to_string)) + } + } + impl From>> for Signal, LocalStorage> { @@ -1086,6 +1118,194 @@ pub mod read { } } + #[cfg(not(all(feature = "nightly", rustc_nightly)))] + /// On nightly signal types implement `Fn()`, + /// so must use negative trait bounds to prevent these implementations for Fn() types + /// causing "multiple impl errors" with the signal types themselves. + trait NotSignalMarker {} + #[cfg(not(all(feature = "nightly", rustc_nightly)))] + impl NotSignalMarker for T {} + + #[cfg(all(feature = "nightly", rustc_nightly))] + auto trait NotSignalMarker {} + + #[cfg(all(feature = "nightly", rustc_nightly))] + impl !NotSignalMarker for Signal {} + + #[cfg(all(feature = "nightly", rustc_nightly))] + impl !NotSignalMarker for ArcSignal {} + + #[doc(hidden)] + pub struct __IntoReactiveValueMarkerSignalFromReactiveClosure; + #[doc(hidden)] + pub struct __IntoReactiveValueMarkerSignalStrOutputToString; + #[doc(hidden)] + pub struct __IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways; + + impl + crate::IntoReactiveValue< + Signal, + __IntoReactiveValueMarkerSignalFromReactiveClosure, + > for F + where + T: Send + Sync + 'static, + F: Fn() -> T + NotSignalMarker + Send + Sync + 'static, + { + fn into_reactive_value(self) -> Signal { + Signal::derive(self) + } + } + + impl + crate::IntoReactiveValue< + ArcSignal, + __IntoReactiveValueMarkerSignalFromReactiveClosure, + > for F + where + T: Send + Sync + 'static, + F: Fn() -> T + NotSignalMarker + Send + Sync + 'static, + { + fn into_reactive_value(self) -> ArcSignal { + ArcSignal::derive(self) + } + } + + impl + crate::IntoReactiveValue< + Signal, + __IntoReactiveValueMarkerSignalFromReactiveClosure, + > for F + where + T: 'static, + F: Fn() -> T + NotSignalMarker + 'static, + { + fn into_reactive_value(self) -> Signal { + Signal::derive_local(self) + } + } + + impl + crate::IntoReactiveValue< + ArcSignal, + __IntoReactiveValueMarkerSignalFromReactiveClosure, + > for F + where + T: 'static, + F: Fn() -> T + NotSignalMarker + 'static, + { + fn into_reactive_value(self) -> ArcSignal { + ArcSignal::derive_local(self) + } + } + + impl + crate::IntoReactiveValue< + Signal, + __IntoReactiveValueMarkerSignalStrOutputToString, + > for F + where + F: Fn() -> &'static str + NotSignalMarker + Send + Sync + 'static, + { + fn into_reactive_value(self) -> Signal { + Signal::derive(self).into() + } + } + + impl + crate::IntoReactiveValue< + ArcSignal, + __IntoReactiveValueMarkerSignalStrOutputToString, + > for F + where + F: Fn() -> &'static str + NotSignalMarker + Send + Sync + 'static, + { + fn into_reactive_value(self) -> ArcSignal { + ArcSignal::derive(move || self().to_string()) + } + } + + impl + crate::IntoReactiveValue< + Signal, + __IntoReactiveValueMarkerSignalStrOutputToString, + > for F + where + F: Fn() -> &'static str + NotSignalMarker + 'static, + { + fn into_reactive_value(self) -> Signal { + Signal::derive_local(self).into() + } + } + + impl + crate::IntoReactiveValue< + ArcSignal, + __IntoReactiveValueMarkerSignalStrOutputToString, + > for F + where + F: Fn() -> &'static str + NotSignalMarker + 'static, + { + fn into_reactive_value(self) -> ArcSignal { + ArcSignal::derive_local(move || self().to_string()) + } + } + + impl + crate::IntoReactiveValue< + Signal, SyncStorage>, + __IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways, + > for F + where + T: Send + Sync + 'static, + F: Fn() -> T + NotSignalMarker + Send + Sync + 'static, + { + fn into_reactive_value(self) -> Signal, SyncStorage> { + Signal::derive(move || Some(self())) + } + } + + impl + crate::IntoReactiveValue< + ArcSignal, SyncStorage>, + __IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways, + > for F + where + T: Send + Sync + 'static, + F: Fn() -> T + NotSignalMarker + Send + Sync + 'static, + { + fn into_reactive_value(self) -> ArcSignal, SyncStorage> { + ArcSignal::derive(move || Some(self())) + } + } + + impl + crate::IntoReactiveValue< + Signal, LocalStorage>, + __IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways, + > for F + where + T: 'static, + F: Fn() -> T + NotSignalMarker + 'static, + { + fn into_reactive_value(self) -> Signal, LocalStorage> { + Signal::derive_local(move || Some(self())) + } + } + + impl + crate::IntoReactiveValue< + ArcSignal, LocalStorage>, + __IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways, + > for F + where + T: 'static, + F: Fn() -> T + NotSignalMarker + 'static, + { + fn into_reactive_value(self) -> ArcSignal, LocalStorage> { + ArcSignal::derive_local(move || Some(self())) + } + } + #[allow(deprecated)] impl From> for Signal where