diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index 6ee613345..d84580497 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -18,6 +18,8 @@ leptos_server = { workspace = true } leptos_config = { workspace = true } leptos-spin-macro = { version = "0.2", optional = true } tracing = "0.1" +reactive_graph = { workspace = true } +tachys = { workspace = true, features = ["reactive_graph"] } typed-builder = "0.18" typed-builder-macro = "0.18" serde = { version = "1", optional = true } @@ -66,6 +68,7 @@ nightly = [ "leptos_macro/nightly", "leptos_reactive/nightly", "leptos_server/nightly", + "tachys/nightly", ] serde = ["leptos_reactive/serde"] serde-lite = ["leptos_reactive/serde-lite"] diff --git a/leptos/src/children.rs b/leptos/src/children.rs index 404a0e59d..f459b913a 100644 --- a/leptos/src/children.rs +++ b/leptos/src/children.rs @@ -1,20 +1,26 @@ -use leptos_dom::Fragment; -use std::rc::Rc; +use std::sync::Arc; +use tachys::{ + renderer::dom::Dom, + view::{ + any_view::{AnyView, IntoAny}, + RenderHtml, + }, +}; /// The most common type for the `children` property on components, /// which can only be called once. -pub type Children = Box Fragment>; +pub type Children = Box AnyView>; /// A type for the `children` property on components that can be called /// more than once. -pub type ChildrenFn = Rc Fragment>; +pub type ChildrenFn = Arc AnyView>; /// A type for the `children` property on components that can be called /// more than once, but may mutate the children. -pub type ChildrenFnMut = Box Fragment>; +pub type ChildrenFnMut = Box AnyView>; -// This is to still support components that accept `Box Fragment>` as a children. -type BoxedChildrenFn = Box Fragment>; +// This is to still support components that accept `Box AnyView>` as a children. +type BoxedChildrenFn = Box AnyView>; /// This trait can be used when constructing a component that takes children without needing /// to know exactly what children type the component expects. This is used internally by the @@ -93,42 +99,74 @@ pub trait ToChildren { fn to_children(f: F) -> Self; } -impl ToChildren for Children +impl ToChildren for Children where - F: FnOnce() -> Fragment + 'static, + F: FnOnce() -> C + 'static, + C: RenderHtml + 'static, { #[inline] fn to_children(f: F) -> Self { - Box::new(f) + Box::new(move || f().into_any()) } } -impl ToChildren for ChildrenFn +impl ToChildren for ChildrenFn where - F: Fn() -> Fragment + 'static, + F: Fn() -> C + 'static, + C: RenderHtml + 'static, { #[inline] fn to_children(f: F) -> Self { - Rc::new(f) + Arc::new(move || f().into_any()) } } -impl ToChildren for ChildrenFnMut +impl ToChildren for ChildrenFnMut where - F: FnMut() -> Fragment + 'static, + F: Fn() -> C + 'static, + C: RenderHtml + 'static, { #[inline] fn to_children(f: F) -> Self { - Box::new(f) + Box::new(move || f().into_any()) } } -impl ToChildren for BoxedChildrenFn +impl ToChildren for BoxedChildrenFn where - F: Fn() -> Fragment + 'static, + F: Fn() -> C + 'static, + C: RenderHtml + 'static, { #[inline] fn to_children(f: F) -> Self { - Box::new(f) + Box::new(move || f().into_any()) + } +} + +/// New-type wrapper for the a function that returns a view with `From` and `Default` traits implemented +/// to enable optional props in for example `` and ``. +#[derive(Clone)] +pub struct ViewFn(Arc AnyView>); + +impl Default for ViewFn { + fn default() -> Self { + Self(Arc::new(|| ().into_any())) + } +} + +impl From for ViewFn +where + F: Fn() -> C + 'static, + C: RenderHtml + 'static, +{ + fn from(value: F) -> Self { + Self(Arc::new(move || value().into_any())) + } +} + +impl ViewFn { + /// Execute the wrapped function + pub fn run(&self) -> AnyView { + (self.0)() } } diff --git a/leptos/src/component.rs b/leptos/src/component.rs new file mode 100644 index 000000000..a2e194d76 --- /dev/null +++ b/leptos/src/component.rs @@ -0,0 +1,83 @@ +//! Utility traits and functions that allow building components, +//! as either functions of their props or functions with no arguments, +//! without knowing the name of the props struct. + +pub trait Component

{} + +pub trait Props { + type Builder; + + fn builder() -> Self::Builder; +} + +#[doc(hidden)] +pub trait PropsOrNoPropsBuilder { + type Builder; + + fn builder_or_not() -> Self::Builder; +} + +#[doc(hidden)] +#[derive(Copy, Clone, Debug, Default)] +pub struct EmptyPropsBuilder {} + +impl EmptyPropsBuilder { + pub fn build(self) {} +} + +impl PropsOrNoPropsBuilder for P { + type Builder =

::Builder; + + fn builder_or_not() -> Self::Builder { + Self::builder() + } +} + +impl PropsOrNoPropsBuilder for EmptyPropsBuilder { + type Builder = EmptyPropsBuilder; + + fn builder_or_not() -> Self::Builder { + EmptyPropsBuilder {} + } +} + +impl Component for F where F: FnOnce() -> R {} + +impl Component

for F +where + F: FnOnce(P) -> R, + P: Props, +{ +} + +pub fn component_props_builder( + _f: &impl Component

, +) ->

::Builder { +

::builder_or_not() +} + +pub fn component_view(f: impl ComponentConstructor, props: P) -> T { + f.construct(props) +} +pub trait ComponentConstructor { + fn construct(self, props: P) -> T; +} + +impl ComponentConstructor<(), T> for Func +where + Func: FnOnce() -> T, +{ + fn construct(self, (): ()) -> T { + (self)() + } +} + +impl ComponentConstructor for Func +where + Func: FnOnce(P) -> T, + P: PropsOrNoPropsBuilder, +{ + fn construct(self, props: P) -> T { + (self)(props) + } +} diff --git a/leptos/src/for_loop.rs b/leptos/src/for_loop.rs index c60071e3f..d6a481aeb 100644 --- a/leptos/src/for_loop.rs +++ b/leptos/src/for_loop.rs @@ -1,6 +1,9 @@ -use leptos_dom::IntoView; use leptos_macro::component; -use std::hash::Hash; +use std::{hash::Hash, marker::PhantomData}; +use tachys::{ + renderer::Renderer, + view::{keyed::keyed, RenderHtml}, +}; /// Iterates over children and displays them, keyed by the `key` function given. /// @@ -38,64 +41,60 @@ use std::hash::Hash; /// } /// } /// ``` -#[cfg_attr( - any(debug_assertions, feature = "ssr"), - tracing::instrument(level = "trace", skip_all) -)] +#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))] #[component(transparent)] -pub fn For( +pub fn For( /// Items over which the component should iterate. each: IF, /// A key function that will be applied to each item. key: KF, /// A function that takes the item, and returns the view that will be displayed for each item. - /// - /// ## Syntax - /// This can be passed directly in the `view` children of the `` by using the - /// `let:` syntax to specify the name for the data variable passed in the argument. - /// - /// ```rust - /// # use leptos::*; - /// # if false { - /// let (data, set_data) = create_signal(vec![0, 1, 2]); - /// view! { - /// - ///

{data}

- /// - /// } - /// # ; - /// # } - /// ``` - /// is the same as - /// ```rust - /// # use leptos::*; - /// # if false { - /// let (data, set_data) = create_signal(vec![0, 1, 2]); - /// view! { - /// {data}

} - /// /> - /// } - /// # ; - /// # } - /// ``` children: EF, -) -> impl IntoView + #[prop(optional)] _rndr: PhantomData, +) -> impl RenderHtml where IF: Fn() -> I + 'static, I: IntoIterator, - EF: Fn(T) -> N + 'static, - N: IntoView + 'static, - KF: Fn(&T) -> K + 'static, + EF: Fn(T) -> N + Clone + 'static, + N: RenderHtml + 'static, + KF: Fn(&T) -> K + Clone + 'static, K: Eq + Hash + 'static, T: 'static, + Rndr: Renderer + 'static, + Rndr::Node: Clone, + Rndr::Element: Clone, { - leptos_dom::Each::new(each, key, children).into_view() + move || keyed(each(), key.clone(), children.clone()) +} + +#[cfg(test)] +mod tests { + use crate::For; + use leptos_macro::view; + use reactive_graph::{signal::RwSignal, signal_traits::SignalGet}; + use tachys::{ + html::element::HtmlElement, prelude::ElementChild, + renderer::mock_dom::MockDom, view::Render, + }; + + #[test] + fn creates_list() { + let values = RwSignal::new(vec![1, 2, 3, 4, 5]); + let list: HtmlElement<_, _, _, MockDom> = view! { +
    + +
  1. {i}
  2. +
    +
+ }; + let list = list.build(); + assert_eq!( + list.el.to_debug_html(), + "
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
" + ); + } } diff --git a/leptos/src/hydration_scripts/hydration_script.js b/leptos/src/hydration_scripts/hydration_script.js new file mode 100644 index 000000000..4775a89f7 --- /dev/null +++ b/leptos/src/hydration_scripts/hydration_script.js @@ -0,0 +1,8 @@ +(function (pkg_path, output_name, wasm_output_name) { + import(`/${pkg_path}/${output_name}.js`) + .then(mod => { + mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => { + mod.hydrate(); + }); + }) +}) \ No newline at end of file diff --git a/leptos/src/hydration_scripts/island_script.js b/leptos/src/hydration_scripts/island_script.js new file mode 100644 index 000000000..59242bcb4 --- /dev/null +++ b/leptos/src/hydration_scripts/island_script.js @@ -0,0 +1,26 @@ +(function (pkg_path, output_name, wasm_output_name) { + function idle(c) { + if ("requestIdleCallback" in window) { + window.requestIdleCallback(c); + } else { + c(); + } + } + idle(() => { + import(`/${pkg_path}/${output_name}.js`) + .then(mod => { + mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => { + mod.hydrate(); + for (let e of document.querySelectorAll("leptos-island")) { + const l = e.dataset.component; + const islandFn = mod["_island_" + l]; + if (islandFn) { + islandFn(e); + } else { + console.warn(`Could not find WASM function for the island ${l}.`); + } + } + }); + }) + }); +}) diff --git a/leptos/src/hydration_scripts/mod.rs b/leptos/src/hydration_scripts/mod.rs new file mode 100644 index 000000000..40aec1e51 --- /dev/null +++ b/leptos/src/hydration_scripts/mod.rs @@ -0,0 +1,58 @@ +#![allow(clippy::needless_lifetimes)] + +use crate::prelude::*; +use leptos_config::LeptosOptions; +use leptos_macro::{component, view}; +use tachys::view::RenderHtml; + +#[component] +pub fn AutoReload<'a>( + #[prop(optional)] disable_watch: bool, + #[prop(optional)] nonce: Option<&'a str>, + options: LeptosOptions, +) -> impl RenderHtml + 'a { + (!disable_watch && std::env::var("LEPTOS_WATCH").is_ok()).then(|| { + let reload_port = match options.reload_external_port { + Some(val) => val, + None => options.reload_port, + }; + let protocol = match options.reload_ws_protocol { + leptos_config::ReloadWSProtocol::WS => "'ws://'", + leptos_config::ReloadWSProtocol::WSS => "'wss://'", + }; + + let script = include_str!("reload_script.js"); + view! { + + } + }) +} + +#[component] +pub fn HydrationScripts( + options: LeptosOptions, + #[prop(optional)] islands: bool, +) -> impl RenderHtml { + let pkg_path = &options.site_pkg_dir; + let output_name = &options.output_name; + let mut wasm_output_name = output_name.clone(); + if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() { + wasm_output_name.push_str("_bg"); + } + let nonce = None::; // use_nonce(); // TODO + let script = if islands { + include_str!("./island_script.js") + } else { + include_str!("./hydration_script.js") + }; + + view! { + + + + } +} diff --git a/leptos/src/hydration_scripts/reload_script.js b/leptos/src/hydration_scripts/reload_script.js new file mode 100644 index 000000000..b8c34e8c8 --- /dev/null +++ b/leptos/src/hydration_scripts/reload_script.js @@ -0,0 +1,23 @@ +(function (reload_port, protocol) { +let host = window.location.hostname; +let ws = new WebSocket(`${protocol}${host}:${reload_port}/live_reload`); +ws.onmessage = (ev) => { + let msg = JSON.parse(ev.data); + if (msg.all) window.location.reload(); + if (msg.css) { + let found = false; + document.querySelectorAll("link").forEach((link) => { + if (link.getAttribute('href').includes(msg.css)) { + let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds(); + link.setAttribute('href', newHref); + found = true; + } + }); + if (!found) console.warn(`CSS hot-reload: Could not find a element`); + }; + if(msg.view) { + patch(msg.view); + } +}; +ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.'); +}) diff --git a/leptos/src/into_view.rs b/leptos/src/into_view.rs new file mode 100644 index 000000000..4b278262f --- /dev/null +++ b/leptos/src/into_view.rs @@ -0,0 +1,120 @@ +use tachys::{ + hydration::Cursor, + renderer::dom::Dom, + ssr::StreamBuilder, + view::{Mountable, Position, PositionState, Render, RenderHtml}, +}; + +pub struct View(T); + +pub trait IntoView: Sized { + fn into_view(self) -> View; +} + +impl + RenderHtml> IntoView for T { + fn into_view(self) -> View { + View(self) + } +} + +impl> Render for View { + type State = T::State; + + fn build(self) -> Self::State { + self.0.build() + } + + fn rebuild(self, state: &mut Self::State) { + self.0.rebuild(state) + } +} + +impl> RenderHtml for View { + const MIN_LENGTH: usize = >::MIN_LENGTH; + + fn to_html_with_buf(self, buf: &mut String, position: &mut Position) { + self.0.to_html_with_buf(buf, position); + } + + fn to_html_async_with_buf( + self, + buf: &mut StreamBuilder, + position: &mut Position, + ) where + Self: Sized, + { + self.0.to_html_async_with_buf::(buf, position) + } + + fn hydrate( + self, + cursor: &Cursor, + position: &PositionState, + ) -> Self::State { + self.0.hydrate::(cursor, position) + } +} + +/*pub trait IntoView { + const MIN_HTML_LENGTH: usize; + + type State: Mountable; + + fn build(self) -> Self::State; + + fn rebuild(self, state: &mut Self::State); + + fn to_html_with_buf(self, buf: &mut String, position: &mut Position); + + fn to_html_async_with_buf( + self, + buf: &mut StreamBuilder, + position: &mut Position, + ); + + fn hydrate( + self, + cursor: &Cursor, + position: &PositionState, + ) -> Self::State; +} + +impl> IntoView for T {} + +impl Render for T { + type State = ::State; + + fn build(self) -> Self::State { + IntoView::build(self) + } + + fn rebuild(self, state: &mut Self::State) { + IntoView::rebuild(self, state); + } +} + +impl RenderHtml for T { + const MIN_LENGTH: usize = T::MIN_HTML_LENGTH; + + fn to_html_with_buf(self, buf: &mut String, position: &mut Position) { + IntoView::to_html_with_buf(self, buf, position); + } + + fn to_html_async_with_buf( + self, + buf: &mut StreamBuilder, + position: &mut Position, + ) where + Self: Sized, + { + IntoView::to_html_async_with_buf::(self, buf, position); + } + + fn hydrate( + self, + cursor: &Cursor, + position: &PositionState, + ) -> Self::State { + IntoView::hydrate::(self, cursor, position) + } +}*/ diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index 968309a9e..e169cf508 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -1,4 +1,4 @@ -#![deny(missing_docs)] +//#![deny(missing_docs)] // TODO restore #![forbid(unsafe_code)] //! # About Leptos //! @@ -139,7 +139,34 @@ //! # } //! ``` -mod additional_attributes; +extern crate self as leptos; + +pub mod prelude { + pub use reactive_graph::prelude::*; + pub use tachys::prelude::*; +} + +pub mod children; +pub mod component; +mod for_loop; +mod hydration_scripts; +mod show; +pub use for_loop::*; +pub use hydration_scripts::*; +pub use leptos_macro::*; +pub use reactive_graph::{ + self, + signal::{arc_signal, create_signal, signal}, +}; +pub use show::*; +#[doc(hidden)] +pub use typed_builder; +#[doc(hidden)] +pub use typed_builder_macro; +mod into_view; +pub use into_view::IntoView; + +/*mod additional_attributes; pub use additional_attributes::*; mod await_; pub use await_::*; @@ -209,10 +236,9 @@ pub use serde; #[cfg(feature = "experimental-islands")] pub use serde_json; pub use show::*; -pub use suspense_component::*; -mod suspense_component; -mod transition; - +//pub use suspense_component::*; +//mod suspense_component; +//mod transition; #[cfg(any(debug_assertions, feature = "ssr"))] #[doc(hidden)] pub use tracing; @@ -373,4 +399,4 @@ where fn construct(self, props: P) -> View { (self)(props).into_view() } -} +}*/ diff --git a/leptos/src/show.rs b/leptos/src/show.rs index 33a4090ce..27a61f403 100644 --- a/leptos/src/show.rs +++ b/leptos/src/show.rs @@ -1,35 +1,11 @@ -use leptos::{component, ChildrenFn, ViewFn}; -use leptos_dom::IntoView; -use leptos_reactive::signal_prelude::*; +use crate::children::{ChildrenFn, ViewFn}; +use leptos_macro::component; +use reactive_graph::{computed::ArcMemo, traits::Get}; +use tachys::{ + renderer::dom::Dom, + view::{either::Either, RenderHtml}, +}; -/// A component that will show its children when the `when` condition is `true`, -/// and show the fallback when it is `false`, without rerendering every time -/// the condition changes. -/// -/// The fallback prop is optional and defaults to rendering nothing. -/// -/// ```rust -/// # use leptos_reactive::*; -/// # use leptos_macro::*; -/// # use leptos_dom::*; use leptos::*; -/// # let runtime = create_runtime(); -/// let (value, set_value) = create_signal(0); -/// -/// view! { -/// -/// "Small number!" -/// -/// } -/// # ; -/// # runtime.dispose(); -/// ``` -#[cfg_attr( - any(debug_assertions, feature = "ssr"), - tracing::instrument(level = "trace", skip_all) -)] #[component] pub fn Show( /// The children will be shown whenever the condition in the `when` closure returns `true`. @@ -39,14 +15,14 @@ pub fn Show( /// A closure that returns what gets rendered if the when statement is false. By default this is the empty view. #[prop(optional, into)] fallback: ViewFn, -) -> impl IntoView +) -> impl RenderHtml where - W: Fn() -> bool + 'static, + W: Fn() -> bool + Send + Sync + 'static, { - let memoized_when = create_memo(move |_| when()); + let memoized_when = ArcMemo::new(move |_| when()); move || match memoized_when.get() { - true => children().into_view(), - false => fallback.run(), + true => Either::Left(children()), + false => Either::Right(fallback.run()), } }