diff --git a/leptos_dom/src/components.rs b/leptos_dom/src/components.rs deleted file mode 100644 index 3d0423687..000000000 --- a/leptos_dom/src/components.rs +++ /dev/null @@ -1,287 +0,0 @@ -mod dyn_child; -mod each; -mod errors; -mod fragment; -mod unit; - -use crate::{ - hydration::{HydrationCtx, HydrationKey}, - Comment, IntoView, View, -}; -#[cfg(all(target_arch = "wasm32", feature = "web"))] -use crate::{mount_child, prepare_to_move, MountKind, Mountable}; -pub use dyn_child::*; -pub use each::*; -pub use errors::*; -pub use fragment::*; -use leptos_reactive::{untrack_with_diagnostics, Oco}; -#[cfg(all(target_arch = "wasm32", feature = "web"))] -use once_cell::unsync::OnceCell; -use std::fmt; -#[cfg(all(target_arch = "wasm32", feature = "web"))] -use std::rc::Rc; -pub use unit::*; -#[cfg(all(target_arch = "wasm32", feature = "web"))] -use wasm_bindgen::JsCast; - -/// The core foundational leptos components. -#[derive(Clone, PartialEq, Eq)] -pub enum CoreComponent { - /// The [Unit] component. - Unit(UnitRepr), - /// The [DynChild] component. - DynChild(DynChildRepr), - /// The [Each] component. - Each(EachRepr), -} - -impl Default for CoreComponent { - fn default() -> Self { - Self::Unit(UnitRepr::default()) - } -} - -impl fmt::Debug for CoreComponent { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Unit(u) => u.fmt(f), - Self::DynChild(dc) => dc.fmt(f), - Self::Each(e) => e.fmt(f), - } - } -} - -/// Custom leptos component. -#[derive(Clone, PartialEq, Eq)] -pub struct ComponentRepr { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - pub(crate) document_fragment: web_sys::DocumentFragment, - #[cfg(all(target_arch = "wasm32", feature = "web"))] - mounted: Rc>, - #[cfg(any(debug_assertions, feature = "ssr"))] - pub(crate) name: Oco<'static, str>, - #[cfg(debug_assertions)] - _opening: Comment, - /// The children of the component. - pub children: Vec, - closing: Comment, - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - pub(crate) id: Option, - #[cfg(debug_assertions)] - pub(crate) view_marker: Option, -} - -impl fmt::Debug for ComponentRepr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use fmt::Write; - - if self.children.is_empty() { - #[cfg(debug_assertions)] - return write!(f, "<{} />", self.name); - - #[cfg(not(debug_assertions))] - return f.write_str(""); - } else { - #[cfg(debug_assertions)] - writeln!(f, "<{}>", self.name)?; - #[cfg(not(debug_assertions))] - f.write_str("")?; - - let mut pad_adapter = pad_adapter::PadAdapter::new(f); - - for child in &self.children { - writeln!(pad_adapter, "{child:#?}")?; - } - - #[cfg(debug_assertions)] - write!(f, "", self.name)?; - #[cfg(not(debug_assertions))] - f.write_str("")?; - - Ok(()) - } - } -} - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -impl Mountable for ComponentRepr { - fn get_mountable_node(&self) -> web_sys::Node { - if self.mounted.get().is_none() { - self.mounted.set(()).unwrap(); - - self.document_fragment - .unchecked_ref::() - .to_owned() - } - // We need to prepare all children to move - else { - let opening = self.get_opening_node(); - - prepare_to_move( - &self.document_fragment, - &opening, - &self.closing.node, - ); - - self.document_fragment.clone().unchecked_into() - } - } - - fn get_opening_node(&self) -> web_sys::Node { - #[cfg(debug_assertions)] - return self._opening.node.clone(); - - #[cfg(not(debug_assertions))] - return if let Some(child) = self.children.get(0) { - child.get_opening_node() - } else { - self.closing.node.clone() - }; - } - - #[inline] - fn get_closing_node(&self) -> web_sys::Node { - self.closing.node.clone() - } -} -impl From for View { - fn from(value: ComponentRepr) -> Self { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - if !HydrationCtx::is_hydrating() { - for child in &value.children { - mount_child(MountKind::Before(&value.closing.node), child); - } - } - - View::Component(value) - } -} - -impl IntoView for ComponentRepr { - #[cfg_attr(any(debug_assertions, feature = "ssr"), instrument(level = "trace", name = "", skip_all, fields(name = %self.name)))] - fn into_view(self) -> View { - self.into() - } -} - -impl ComponentRepr { - /// Creates a new [`Component`]. - #[inline(always)] - pub fn new(name: impl Into>) -> Self { - Self::new_with_id_concrete(name.into(), HydrationCtx::id()) - } - - /// Creates a new [`Component`] with the given hydration ID. - #[inline(always)] - pub fn new_with_id( - name: impl Into>, - id: Option, - ) -> Self { - Self::new_with_id_concrete(name.into(), id) - } - - fn new_with_id_concrete( - name: Oco<'static, str>, - id: Option, - ) -> Self { - let markers = ( - Comment::new(format!(""), &id, true), - #[cfg(debug_assertions)] - Comment::new(format!("<{name}>"), &id, false), - ); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - let document_fragment = { - let fragment = crate::document().create_document_fragment(); - - // Insert the comments into the document fragment - // so they can serve as our references when inserting - // future nodes - if !HydrationCtx::is_hydrating() { - #[cfg(debug_assertions)] - fragment - .append_with_node_2(&markers.1.node, &markers.0.node) - .expect("append to not err"); - #[cfg(not(debug_assertions))] - fragment - .append_with_node_1(&markers.0.node) - .expect("append to not err"); - } - - fragment - }; - - Self { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - document_fragment, - #[cfg(all(target_arch = "wasm32", feature = "web"))] - mounted: Default::default(), - #[cfg(debug_assertions)] - _opening: markers.1, - closing: markers.0, - #[cfg(any(debug_assertions, feature = "ssr"))] - name, - children: Vec::with_capacity(1), - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - id, - #[cfg(debug_assertions)] - view_marker: None, - } - } - - #[cfg(any(debug_assertions, feature = "ssr"))] - /// Returns the name of the component. - pub fn name(&self) -> &str { - &self.name - } -} - -/// A user-defined `leptos` component. -pub struct Component -where - F: FnOnce() -> V, - V: IntoView, -{ - id: Option, - name: Oco<'static, str>, - children_fn: F, -} - -impl Component -where - F: FnOnce() -> V, - V: IntoView, -{ - /// Creates a new component. - pub fn new(name: impl Into>, f: F) -> Self { - Self { - id: HydrationCtx::id(), - name: name.into(), - children_fn: f, - } - } -} - -impl IntoView for Component -where - F: FnOnce() -> V, - V: IntoView, -{ - #[track_caller] - fn into_view(self) -> View { - let Self { - id, - name, - children_fn, - } = self; - - let mut repr = ComponentRepr::new_with_id(name, id); - - // disposed automatically when the parent scope is disposed - let child = untrack_with_diagnostics(|| children_fn().into_view()); - - repr.children.push(child); - - repr.into_view() - } -} diff --git a/leptos_dom/src/components/dyn_child.rs b/leptos_dom/src/components/dyn_child.rs deleted file mode 100644 index 61bbb73e2..000000000 --- a/leptos_dom/src/components/dyn_child.rs +++ /dev/null @@ -1,452 +0,0 @@ -use crate::{ - hydration::{HydrationCtx, HydrationKey}, - Comment, IntoView, View, -}; -use cfg_if::cfg_if; -use std::{cell::RefCell, fmt, ops::Deref, rc::Rc}; -cfg_if! { - if #[cfg(all(target_arch = "wasm32", feature = "web"))] { - use crate::{mount_child, prepare_to_move, unmount_child, MountKind, Mountable, Text}; - use leptos_reactive::create_render_effect; - use wasm_bindgen::JsCast; - } -} - -/// The internal representation of the [`DynChild`] core-component. -#[derive(Clone, PartialEq, Eq)] -pub struct DynChildRepr { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - document_fragment: web_sys::DocumentFragment, - #[cfg(debug_assertions)] - opening: Comment, - pub(crate) child: Rc>>>, - closing: Comment, - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - pub(crate) id: Option, -} - -impl fmt::Debug for DynChildRepr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use fmt::Write; - - f.write_str("\n")?; - - let mut pad_adapter = pad_adapter::PadAdapter::new(f); - - writeln!( - pad_adapter, - "{:#?}", - self.child.borrow().deref().deref().as_ref().unwrap() - )?; - - f.write_str("") - } -} - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -impl Mountable for DynChildRepr { - fn get_mountable_node(&self) -> web_sys::Node { - if self.document_fragment.child_nodes().length() != 0 { - self.document_fragment.clone().unchecked_into() - } else { - let opening = self.get_opening_node(); - - prepare_to_move( - &self.document_fragment, - &opening, - &self.closing.node, - ); - - self.document_fragment.clone().unchecked_into() - } - } - - fn get_opening_node(&self) -> web_sys::Node { - #[cfg(debug_assertions)] - return self.opening.node.clone(); - - #[cfg(not(debug_assertions))] - return self - .child - .borrow() - .as_ref() - .as_ref() - .unwrap() - .get_opening_node(); - } - - fn get_closing_node(&self) -> web_sys::Node { - self.closing.node.clone() - } -} - -impl DynChildRepr { - fn new_with_id(id: Option) -> Self { - let markers = ( - Comment::new("", &id, true), - #[cfg(debug_assertions)] - Comment::new("", &id, false), - ); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - let document_fragment = { - let fragment = crate::document().create_document_fragment(); - - // Insert the comments into the document fragment - // so they can serve as our references when inserting - // future nodes - if !HydrationCtx::is_hydrating() { - #[cfg(debug_assertions)] - fragment - .append_with_node_2(&markers.1.node, &markers.0.node) - .unwrap(); - #[cfg(not(debug_assertions))] - fragment.append_with_node_1(&markers.0.node).unwrap(); - } - - fragment - }; - - Self { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - document_fragment, - #[cfg(debug_assertions)] - opening: markers.1, - child: Default::default(), - closing: markers.0, - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - id, - } - } -} - -/// Represents any [`View`] that can change over time. -pub struct DynChild -where - CF: Fn() -> N + 'static, - N: IntoView, -{ - id: Option, - child_fn: CF, -} - -impl DynChild -where - CF: Fn() -> N + 'static, - N: IntoView, -{ - /// Creates a new dynamic child which will re-render whenever it's - /// signal dependencies change. - #[track_caller] - #[inline(always)] - pub fn new(child_fn: CF) -> Self { - Self::new_with_id(HydrationCtx::id(), child_fn) - } - - #[doc(hidden)] - #[track_caller] - #[inline(always)] - pub const fn new_with_id(id: Option, child_fn: CF) -> Self { - Self { id, child_fn } - } -} - -impl IntoView for DynChild -where - CF: Fn() -> N + 'static, - N: IntoView, -{ - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", name = "", skip_all) - )] - #[inline] - fn into_view(self) -> View { - // concrete inner function - #[inline(never)] - fn create_dyn_view( - component: DynChildRepr, - child_fn: Box View>, - ) -> DynChildRepr { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - let closing = component.closing.node.clone(); - - let child = component.child.clone(); - - #[cfg(all( - debug_assertions, - target_arch = "wasm32", - feature = "web" - ))] - let span = tracing::Span::current(); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - create_render_effect( - move |prev_run: Option>| { - #[cfg(debug_assertions)] - let _guard = span.enter(); - - let new_child = child_fn().into_view(); - - let mut child_borrow = child.borrow_mut(); - - // Is this at least the second time we are loading a child? - if let Some(prev_t) = prev_run { - let child = child_borrow.take().unwrap(); - - // We need to know if our child wasn't moved elsewhere. - // If it was, `DynChild` no longer "owns" that child, and - // is therefore no longer sound to unmount it from the DOM - // or to reuse it in the case of a text node - - // TODO check does this still detect moves correctly? - let was_child_moved = prev_t.is_none() - && child - .get_closing_node() - .next_non_view_marker_sibling() - .as_ref() - != Some(&closing); - - // If the previous child was a text node, we would like to - // make use of it again if our current child is also a text - // node - let ret = if let Some(prev_t) = prev_t { - // Here, our child is also a text node - - // nb: the match/ownership gymnastics here - // are so that, if we can reuse the text node, - // we can take ownership of new_t so we don't clone - // the contents, which in O(n) on the length of the text - if matches!(new_child, View::Text(_)) { - if !was_child_moved && child != new_child { - let mut new_t = match new_child { - View::Text(t) => t, - _ => unreachable!(), - }; - prev_t - .unchecked_ref::() - .set_data(&new_t.content); - - // replace new_t's text node with the prev node - // see discussion: https://github.com/leptos-rs/leptos/pull/1472 - new_t.node = prev_t.clone(); - - let new_child = View::Text(new_t); - **child_borrow = Some(new_child); - - Some(prev_t) - } else { - let new_t = new_child.as_text().unwrap(); - mount_child( - MountKind::Before(&closing), - &new_child, - ); - - **child_borrow = Some(new_child.clone()); - - Some(new_t.node.clone()) - } - } - // Child is not a text node, so we can remove the previous - // text node - else { - if !was_child_moved && child != new_child { - // Remove the text - closing - .previous_non_view_marker_sibling() - .unwrap() - .unchecked_into::() - .remove(); - } - - // Mount the new child, and we're done - mount_child( - MountKind::Before(&closing), - &new_child, - ); - - **child_borrow = Some(new_child); - - None - } - } - // Otherwise, the new child can still be a text node, - // but we know the previous child was not, so no special - // treatment here - else { - // Technically, I think this check shouldn't be necessary, but - // I can imagine some edge case that the child changes while - // hydration is ongoing - if !HydrationCtx::is_hydrating() { - let same_child = child == new_child; - if !was_child_moved && !same_child { - // Remove the child - let start = child.get_opening_node(); - let end = &closing; - - match child { - View::CoreComponent( - crate::CoreComponent::DynChild( - child, - ), - ) => { - let start = - child.get_opening_node(); - let end = child.closing.node; - prepare_to_move( - &child.document_fragment, - &start, - &end, - ); - } - View::Component(child) => { - let start = - child.get_opening_node(); - let end = child.closing.node; - prepare_to_move( - &child.document_fragment, - &start, - &end, - ); - } - _ => unmount_child(&start, end), - } - } - - // Mount the new child - // If it's the same child, don't re-mount - if !same_child { - mount_child( - MountKind::Before(&closing), - &new_child, - ); - } - } - - // We want to reuse text nodes, so hold onto it if - // our child is one - let t = - new_child.get_text().map(|t| t.node.clone()); - - **child_borrow = Some(new_child); - - t - }; - - ret - } - // Otherwise, we know for sure this is our first time - else { - // If it's a text node, we want to use the old text node - // as the text node for the DynChild, rather than the new - // text node being created during hydration - let new_child = if HydrationCtx::is_hydrating() - && new_child.get_text().is_some() - { - let t = closing - .previous_non_view_marker_sibling() - .unwrap() - .unchecked_into::(); - - let new_child = match new_child { - View::Text(text) => text, - _ => unreachable!(), - }; - t.set_data(&new_child.content); - View::Text(Text { - node: t.unchecked_into(), - content: new_child.content, - }) - } else { - new_child - }; - - // If we are not hydrating, we simply mount the child - if !HydrationCtx::is_hydrating() { - mount_child( - MountKind::Before(&closing), - &new_child, - ); - } - - // We want to update text nodes, rather than replace them, so - // make sure to hold onto the text node - let t = new_child.get_text().map(|t| t.node.clone()); - - **child_borrow = Some(new_child); - - t - } - }, - ); - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - let new_child = child_fn().into_view(); - - **child.borrow_mut() = Some(new_child); - } - - component - } - - // monomorphized outer function - let Self { id, child_fn } = self; - - let component = DynChildRepr::new_with_id(id); - let component = create_dyn_view( - component, - Box::new(move || child_fn().into_view()), - ); - - View::CoreComponent(crate::CoreComponent::DynChild(component)) - } -} - -cfg_if! { - if #[cfg(all(target_arch = "wasm32", feature = "web"))] { - use web_sys::Node; - - pub(crate) trait NonViewMarkerSibling { - fn next_non_view_marker_sibling(&self) -> Option; - - fn previous_non_view_marker_sibling(&self) -> Option; - } - - impl NonViewMarkerSibling for web_sys::Node { - #[cfg_attr(not(debug_assertions), inline(always))] - fn next_non_view_marker_sibling(&self) -> Option { - cfg_if! { - if #[cfg(debug_assertions)] { - self.next_sibling().and_then(|node| { - if node.text_content().unwrap_or_default().trim().starts_with("leptos-view") { - node.next_sibling() - } else { - Some(node) - } - }) - } else { - self.next_sibling() - } - } - } - - #[cfg_attr(not(debug_assertions), inline(always))] - fn previous_non_view_marker_sibling(&self) -> Option { - cfg_if! { - if #[cfg(debug_assertions)] { - self.previous_sibling().and_then(|node| { - if node.text_content().unwrap_or_default().trim().starts_with("leptos-view") { - node.previous_sibling() - } else { - Some(node) - } - }) - } else { - self.previous_sibling() - } - } - } - } - } -} diff --git a/leptos_dom/src/components/each.rs b/leptos_dom/src/components/each.rs deleted file mode 100644 index fa18c4d02..000000000 --- a/leptos_dom/src/components/each.rs +++ /dev/null @@ -1,1296 +0,0 @@ -#[cfg(not(all(target_arch = "wasm32", feature = "web")))] -use crate::hydration::HydrationKey; -use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View}; -use leptos_reactive::{as_child_of_current_owner, Disposer}; -use std::{cell::RefCell, fmt, hash::Hash, ops::Deref, rc::Rc}; -#[cfg(all(target_arch = "wasm32", feature = "web"))] -use web::*; - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -mod web { - pub(crate) use crate::{ - mount_child, prepare_to_move, MountKind, Mountable, RANGE, - }; - pub use drain_filter_polyfill::VecExt as VecDrainFilterExt; - pub use leptos_reactive::create_render_effect; - pub use std::cell::OnceCell; - pub use wasm_bindgen::JsCast; -} - -type FxIndexSet = - indexmap::IndexSet>; - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -trait VecExt { - fn get_next_closest_mounted_sibling( - &self, - start_at: usize, - or: web_sys::Node, - ) -> web_sys::Node; -} - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -impl VecExt for Vec> { - fn get_next_closest_mounted_sibling( - &self, - start_at: usize, - or: web_sys::Node, - ) -> web_sys::Node { - self[start_at..] - .iter() - .find_map(|s| s.as_ref().map(|s| s.get_opening_node())) - .unwrap_or(or) - } -} - -/// The internal representation of the [`Each`] core-component. -#[derive(Clone, PartialEq, Eq)] -pub struct EachRepr { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - document_fragment: web_sys::DocumentFragment, - #[cfg(all(target_arch = "wasm32", feature = "web"))] - mounted: Rc>, - #[cfg(debug_assertions)] - opening: Comment, - pub(crate) children: Rc>>>, - closing: Comment, - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - pub(crate) id: Option, -} - -impl fmt::Debug for EachRepr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use fmt::Write; - - f.write_str("\n")?; - - for child in self.children.borrow().deref() { - let mut pad_adapter = pad_adapter::PadAdapter::new(f); - - writeln!(pad_adapter, "{:#?}", child.as_ref().unwrap())?; - } - - f.write_str("") - } -} - -impl Default for EachRepr { - fn default() -> Self { - let id = HydrationCtx::id(); - - let markers = ( - Comment::new("", &id, true), - #[cfg(debug_assertions)] - Comment::new("", &id, false), - ); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - let document_fragment = { - let fragment = crate::document().create_document_fragment(); - - // Insert the comments into the document fragment - #[cfg(debug_assertions)] - // so they can serve as our references when inserting - // future nodes - if !HydrationCtx::is_hydrating() { - fragment - .append_with_node_2(&markers.1.node, &markers.0.node) - .expect("append to not err"); - } - - #[cfg(not(debug_assertions))] - if !HydrationCtx::is_hydrating() { - fragment.append_with_node_1(&markers.0.node).unwrap(); - } - - fragment - }; - - Self { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - document_fragment, - #[cfg(all(target_arch = "wasm32", feature = "web"))] - mounted: Default::default(), - #[cfg(debug_assertions)] - opening: markers.1, - children: Default::default(), - closing: markers.0, - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - id, - } - } -} - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -impl Mountable for EachRepr { - fn get_mountable_node(&self) -> web_sys::Node { - if self.mounted.get().is_none() { - self.mounted.set(()).unwrap(); - - self.document_fragment.clone().unchecked_into() - } else { - let opening = self.get_opening_node(); - - prepare_to_move( - &self.document_fragment, - &opening, - &self.closing.node, - ); - - self.document_fragment.clone().unchecked_into() - } - } - - fn get_opening_node(&self) -> web_sys::Node { - #[cfg(debug_assertions)] - return self.opening.node.clone(); - - #[cfg(not(debug_assertions))] - return { - let children_borrow = self.children.borrow(); - - if let Some(Some(child)) = children_borrow.get(0) { - child.get_opening_node() - } else { - self.closing.node.clone() - } - }; - } - - #[inline(always)] - fn get_closing_node(&self) -> web_sys::Node { - self.closing.node.clone() - } -} - -/// The internal representation of an [`Each`] item. -#[derive(PartialEq, Eq)] -pub(crate) struct EachItem { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - disposer: Disposer, - #[cfg(all(target_arch = "wasm32", feature = "web"))] - document_fragment: Option, - #[cfg(debug_assertions)] - opening: Option, - pub(crate) child: View, - closing: Option, - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - pub(crate) id: Option, -} - -impl fmt::Debug for EachItem { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use fmt::Write; - - f.write_str("\n")?; - - let mut pad_adapter = pad_adapter::PadAdapter::new(f); - - writeln!(pad_adapter, "{:#?}", self.child)?; - - f.write_str("") - } -} - -impl EachItem { - fn new(disposer: Disposer, child: View) -> Self { - let id = HydrationCtx::id(); - let needs_closing = !matches!(child, View::Element(_)); - - // On the client, this disposer runs when the EachItem - // drops. However, imagine you have a nested situation like - // > create a resource [0, 1, 2] - // > Suspense - // > For - // > each row - // > create a resource (say, look up post by ID) - // > Suspense - // > read the resource - // - // In this situation, if the EachItem scopes were disposed when they drop, - // the resources will actually be disposed when the parent Suspense is - // resolved and rendered, because at that point the For will have been rendered - // to an HTML string and dropped. - // - // When the child Suspense for each row goes to read from the resource, that - // resource no longer exists, because it was disposed when that row dropped. - // - // Hoisting this into an `on_cleanup` on here forgets it until the reactive owner - // is cleaned up, rather than only until the For drops. Practically speaking, in SSR - // mode this should mean that it sticks around for the life of the request, and is then - // cleaned up with the rest of the request. - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - leptos_reactive::on_cleanup(move || drop(disposer)); - - let markers = ( - if needs_closing { - Some(Comment::new("", &id, true)) - } else { - None - }, - #[cfg(debug_assertions)] - if needs_closing { - Some(Comment::new("", &id, false)) - } else { - None - }, - ); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - let document_fragment = if needs_closing { - let fragment = crate::document().create_document_fragment(); - let closing = markers.0.as_ref().unwrap(); - - // Insert the comments into the document fragment - // so they can serve as our references when inserting - // future nodes - if !HydrationCtx::is_hydrating() { - #[cfg(debug_assertions)] - fragment - .append_with_node_2( - &markers.1.as_ref().unwrap().node, - &closing.node, - ) - .unwrap(); - fragment.append_with_node_1(&closing.node).unwrap(); - } - - // if child view is Text and if we are hydrating, we do not - // need to mount it. otherwise, mount it here - if !HydrationCtx::is_hydrating() || !matches!(child, View::Text(_)) - { - mount_child(MountKind::Before(&closing.node), &child); - } - - Some(fragment) - } else { - None - }; - - Self { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - disposer, - #[cfg(all(target_arch = "wasm32", feature = "web"))] - document_fragment, - #[cfg(debug_assertions)] - opening: markers.1, - child, - closing: markers.0, - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - id, - } - } -} - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -impl Mountable for EachItem { - fn get_mountable_node(&self) -> web_sys::Node { - if let Some(fragment) = &self.document_fragment { - fragment.clone().unchecked_into() - } else { - self.child.get_mountable_node() - } - } - - #[inline(always)] - fn get_opening_node(&self) -> web_sys::Node { - return self.child.get_opening_node(); - } - - fn get_closing_node(&self) -> web_sys::Node { - if let Some(closing) = &self.closing { - closing.node.clone().unchecked_into() - } else { - self.child.get_mountable_node().clone() - } - } -} - -impl EachItem { - /// Moves all child nodes into its' `DocumentFragment` in - /// order to be reinserted somewhere else. - #[cfg(all(target_arch = "wasm32", feature = "web"))] - fn prepare_for_move(&self) { - if let Some(fragment) = &self.document_fragment { - let start = self.get_opening_node(); - let end = &self.get_closing_node(); - - let mut sibling = start; - - while sibling != *end { - let next_sibling = sibling.next_sibling().unwrap(); - - fragment.append_child(&sibling).unwrap(); - - sibling = next_sibling; - } - - fragment.append_with_node_1(end).unwrap(); - } else { - let node = self.child.get_mountable_node(); - node.unchecked_into::().remove(); - } - } -} - -/// A component for efficiently rendering an iterable. -pub struct Each -where - IF: Fn() -> I + 'static, - I: IntoIterator, - EF: Fn(T) -> N + 'static, - N: IntoView, - KF: Fn(&T) -> K + 'static, - K: Eq + Hash + 'static, - T: 'static, -{ - pub(crate) items_fn: IF, - pub(crate) each_fn: EF, - key_fn: KF, -} - -impl Each -where - IF: Fn() -> I + 'static, - I: IntoIterator, - EF: Fn(T) -> N + 'static, - N: IntoView, - KF: Fn(&T) -> K, - K: Eq + Hash + 'static, - T: 'static, -{ - /// Creates a new [`Each`] component. - #[inline(always)] - pub const fn new(items_fn: IF, key_fn: KF, each_fn: EF) -> Self { - Self { - items_fn, - each_fn, - key_fn, - } - } -} - -impl IntoView for Each -where - IF: Fn() -> I + 'static, - I: IntoIterator, - EF: Fn(T) -> N + 'static, - N: IntoView + 'static, - KF: Fn(&T) -> K + 'static, - K: Eq + Hash + 'static, - T: 'static, -{ - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", name = "", skip_all) - )] - fn into_view(self) -> crate::View { - let Self { - items_fn, - each_fn, - key_fn, - } = self; - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - let _ = key_fn; - - let component = EachRepr::default(); - - #[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))] - let opening = component.opening.node.clone().unchecked_into(); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - let (children, closing) = - (component.children.clone(), component.closing.node.clone()); - - let each_fn = as_child_of_current_owner(each_fn); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - create_render_effect( - move |prev_hash_run: Option>>| { - let mut children_borrow = children.borrow_mut(); - - #[cfg(all( - not(debug_assertions), - target_arch = "wasm32", - feature = "web" - ))] - let opening = if let Some(Some(child)) = children_borrow.get(0) - { - // correctly remove opening - let child_opening = child.get_opening_node(); - #[cfg(debug_assertions)] - { - use crate::components::dyn_child::NonViewMarkerSibling; - child_opening - .previous_non_view_marker_sibling() - .unwrap_or(child_opening) - } - #[cfg(not(debug_assertions))] - { - child_opening - } - } else { - closing.clone() - }; - - let items_iter = items_fn().into_iter(); - - let (capacity, _) = items_iter.size_hint(); - let mut hashed_items = FxIndexSet::with_capacity_and_hasher( - capacity, - Default::default(), - ); - - if let Some(HashRun(prev_hash_run)) = prev_hash_run { - if !prev_hash_run.is_empty() { - let mut items = Vec::with_capacity(capacity); - for item in items_iter { - hashed_items.insert(key_fn(&item)); - items.push(Some(item)); - } - - let cmds = diff(&prev_hash_run, &hashed_items); - - apply_diff( - #[cfg(all( - target_arch = "wasm32", - feature = "web" - ))] - &opening, - #[cfg(all( - target_arch = "wasm32", - feature = "web" - ))] - &closing, - cmds, - &mut children_borrow, - items, - &each_fn, - ); - return HashRun(hashed_items); - } - } - - // if previous run is empty - *children_borrow = Vec::with_capacity(capacity); - #[cfg(all(target_arch = "wasm32", feature = "web"))] - let fragment = crate::document().create_document_fragment(); - - for item in items_iter { - hashed_items.insert(key_fn(&item)); - let (child, disposer) = each_fn(item); - let each_item = EachItem::new(disposer, child.into_view()); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - _ = fragment - .append_child(&each_item.get_mountable_node()); - } - - children_borrow.push(Some(each_item)); - } - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - closing - .unchecked_ref::() - .before_with_node_1(&fragment) - .expect("before to not err"); - - HashRun(hashed_items) - }, - ); - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - *component.children.borrow_mut() = (items_fn)() - .into_iter() - .map(|child| { - let (item, disposer) = each_fn(child); - Some(EachItem::new(disposer, item.into_view())) - }) - .collect(); - } - - View::CoreComponent(CoreComponent::Each(component)) - } -} - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -struct HashRun(T); - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -impl fmt::Debug for HashRun { - #[inline] - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.debug_tuple("HashRun").finish() - } -} - -/// Calculates the operations needed to get from `from` to `to`. -#[allow(dead_code)] // not used in SSR but useful to have available for testing -fn diff(from: &FxIndexSet, to: &FxIndexSet) -> Diff { - if from.is_empty() && to.is_empty() { - return Diff::default(); - } else if to.is_empty() { - return Diff { - clear: true, - ..Default::default() - }; - } else if from.is_empty() { - return Diff { - added: to - .iter() - .enumerate() - .map(|(at, _)| DiffOpAdd { - at, - mode: DiffOpAddMode::Append, - }) - .collect(), - ..Default::default() - }; - } - - let mut removed = vec![]; - let mut moved = vec![]; - let mut added = vec![]; - let max_len = std::cmp::max(from.len(), to.len()); - - for index in 0..max_len { - let from_item = from.get_index(index); - let to_item = to.get_index(index); - - // if they're the same, do nothing - if from_item != to_item { - // if it's only in old, not new, remove it - if from_item.is_some() && !to.contains(from_item.unwrap()) { - let op = DiffOpRemove { at: index }; - removed.push(op); - } - // if it's only in new, not old, add it - if to_item.is_some() && !from.contains(to_item.unwrap()) { - let op = DiffOpAdd { - at: index, - mode: DiffOpAddMode::Normal, - }; - added.push(op); - } - // if it's in both old and new, it can either - // 1) be moved (and need to move in the DOM) - // 2) be moved (but not need to move in the DOM) - // * this would happen if, for example, 2 items - // have been added before it, and it has moved by 2 - if let Some(from_item) = from_item { - if let Some(to_item) = to.get_full(from_item) { - let moves_forward_by = (to_item.0 as i32) - (index as i32); - let move_in_dom = moves_forward_by - != (added.len() as i32) - (removed.len() as i32); - - let op = DiffOpMove { - from: index, - len: 1, - to: to_item.0, - move_in_dom, - }; - moved.push(op); - } - } - } - } - - moved = group_adjacent_moves(moved); - - Diff { - removed, - items_to_move: moved.iter().map(|m| m.len).sum(), - moved, - added, - clear: false, - } -} - -/// Group adjacent items that are being moved as a group. -/// For example from `[2, 3, 5, 6]` to `[1, 2, 3, 4, 5, 6]` should result -/// in a move for `2,3` and `5,6` rather than 4 individual moves. -fn group_adjacent_moves(moved: Vec) -> Vec { - let mut prev: Option = None; - let mut new_moved = Vec::with_capacity(moved.len()); - for m in moved { - match prev { - Some(mut p) => { - if (m.from == p.from + p.len) && (m.to == p.to + p.len) { - p.len += 1; - prev = Some(p); - } else { - new_moved.push(prev.take().unwrap()); - prev = Some(m); - } - } - None => prev = Some(m), - } - } - if let Some(prev) = prev { - new_moved.push(prev) - } - new_moved -} - -#[derive(Debug, Default, PartialEq, Eq)] -struct Diff { - removed: Vec, - moved: Vec, - items_to_move: usize, - added: Vec, - clear: bool, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct DiffOpMove { - /// The index this range is starting relative to `from`. - from: usize, - /// The number of elements included in this range. - len: usize, - /// The starting index this range will be moved to relative to `to`. - to: usize, - /// Marks this move to be applied to the DOM, or just to the underlying - /// storage - move_in_dom: bool, -} - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -impl Default for DiffOpMove { - fn default() -> Self { - Self { - from: 0, - to: 0, - len: 1, - move_in_dom: true, - } - } -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -struct DiffOpAdd { - at: usize, - mode: DiffOpAddMode, -} - -#[derive(Debug, PartialEq, Eq)] -struct DiffOpRemove { - at: usize, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum DiffOpAddMode { - Normal, - Append, - // Todo - _Prepend, -} - -impl Default for DiffOpAddMode { - fn default() -> Self { - Self::Normal - } -} - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -fn apply_diff( - opening: &web_sys::Node, - closing: &web_sys::Node, - diff: Diff, - children: &mut Vec>, - mut items: Vec>, - each_fn: &EF, -) where - EF: Fn(T) -> (V, Disposer), - V: IntoView, -{ - let range = RANGE.with(|range| (*range).clone()); - - // The order of cmds needs to be: - // 1. Clear - // 2. Removals - // 3. Move out - // 4. Resize - // 5. Move in - // 6. Additions - // 7. Removes holes - if diff.clear { - if opening.previous_sibling().is_none() - && closing.next_sibling().is_none() - { - let parent = closing - .parent_node() - .expect("could not get closing node") - .unchecked_into::(); - parent.set_text_content(Some("")); - - #[cfg(debug_assertions)] - parent.append_with_node_2(opening, closing).unwrap(); - - #[cfg(not(debug_assertions))] - parent.append_with_node_1(closing).unwrap(); - } else { - #[cfg(debug_assertions)] - range.set_start_after(opening).unwrap(); - #[cfg(not(debug_assertions))] - range.set_start_before(opening).unwrap(); - - range.set_end_before(closing).unwrap(); - - range.delete_contents().unwrap(); - } - - children.clear(); - - if diff.added.is_empty() { - return; - } - } - - for DiffOpRemove { at } in &diff.removed { - let item_to_remove = children[*at].take().unwrap(); - - item_to_remove.prepare_for_move(); - } - - let (move_cmds, add_cmds) = unpack_moves(&diff); - - let mut moved_children = move_cmds - .iter() - .map(|move_| { - let each_item = children[move_.from].take().unwrap(); - - if move_.move_in_dom { - each_item.prepare_for_move(); - } - - Some(each_item) - }) - .collect::>(); - - children.resize_with(children.len() + diff.added.len(), || None); - - for (i, DiffOpMove { to, .. }) in move_cmds - .iter() - .enumerate() - .filter(|(_, move_)| !move_.move_in_dom) - { - children[*to] = moved_children[i].take(); - } - - for (i, DiffOpMove { to, .. }) in move_cmds - .into_iter() - .enumerate() - .filter(|(_, move_)| move_.move_in_dom) - { - let each_item = moved_children[i].take().unwrap(); - - let sibling_node = - children.get_next_closest_mounted_sibling(to, closing.to_owned()); - - mount_child(MountKind::Before(&sibling_node), &each_item); - - children[to] = Some(each_item); - } - - for DiffOpAdd { at, mode } in add_cmds { - let (item, disposer) = each_fn(items[at].take().unwrap()); - let each_item = EachItem::new(disposer, item.into_view()); - - match mode { - DiffOpAddMode::Normal => { - let sibling_node = children - .get_next_closest_mounted_sibling(at, closing.to_owned()); - - mount_child(MountKind::Before(&sibling_node), &each_item); - } - DiffOpAddMode::Append => { - mount_child(MountKind::Before(closing), &each_item); - } - DiffOpAddMode::_Prepend => { - todo!("Prepends are not yet implemented") - } - } - - children[at] = Some(each_item); - } - - #[allow(unstable_name_collisions)] - children.drain_filter(|c| c.is_none()); -} - -/// Unpacks adds and moves into a sequence of interleaved -/// add and move commands. Move commands will always return -/// with a `len == 1`. -#[cfg(all(target_arch = "wasm32", feature = "web"))] -fn unpack_moves(diff: &Diff) -> (Vec, Vec) { - let mut moves = Vec::with_capacity(diff.items_to_move); - let mut adds = Vec::with_capacity(diff.added.len()); - - let mut removes_iter = diff.removed.iter(); - let mut adds_iter = diff.added.iter(); - let mut moves_iter = diff.moved.iter(); - - let mut removes_next = removes_iter.next(); - let mut adds_next = adds_iter.next(); - let mut moves_next = moves_iter.next().copied(); - - for i in 0..diff.items_to_move + diff.added.len() + diff.removed.len() { - if let Some(DiffOpRemove { at, .. }) = removes_next { - if i == *at { - removes_next = removes_iter.next(); - - continue; - } - } - - match (adds_next, &mut moves_next) { - (Some(add), Some(move_)) => { - if add.at == i { - adds.push(*add); - - adds_next = adds_iter.next(); - } else { - let mut single_move = *move_; - single_move.len = 1; - - moves.push(single_move); - - move_.len -= 1; - move_.from += 1; - move_.to += 1; - - if move_.len == 0 { - moves_next = moves_iter.next().copied(); - } - } - } - (Some(add), None) => { - adds.push(*add); - - adds_next = adds_iter.next(); - } - (None, Some(move_)) => { - let mut single_move = *move_; - single_move.len = 1; - - moves.push(single_move); - - move_.len -= 1; - move_.from += 1; - move_.to += 1; - - if move_.len == 0 { - moves_next = moves_iter.next().copied(); - } - } - (None, None) => break, - } - } - - (moves, adds) -} - -// #[cfg(test)] -// mod test_utils { -// use super::*; - -// pub trait IntoFxIndexSet { -// fn into_fx_index_set(self) -> FxIndexSet; -// } - -// impl IntoFxIndexSet for T -// where -// T: IntoIterator, -// K: Eq + Hash, -// { -// fn into_fx_index_set(self) -> FxIndexSet { -// self.into_iter().collect() -// } -// } -// } - -// #[cfg(test)] -// use test_utils::*; - -// #[cfg(test)] -// mod find_ranges { -// use super::*; - -// // Single range tests will be empty because of removing ranges -// // that didn't move -// #[test] -// fn single_range() { -// let ranges = find_ranges( -// [1, 2, 3, 4].iter().into_fx_index_set(), -// [1, 2, 3, 4].iter().into_fx_index_set(), -// &[1, 2, 3, 4].into_fx_index_set(), -// &[1, 2, 3, 4].into_fx_index_set(), -// ); - -// assert_eq!(ranges, vec![]); -// } - -// #[test] -// fn single_range_with_adds() { -// let ranges = find_ranges( -// [1, 2, 3, 4].iter().into_fx_index_set(), -// [1, 2, 3, 4].iter().into_fx_index_set(), -// &[1, 2, 3, 4].into_fx_index_set(), -// &[1, 2, 5, 3, 4].into_fx_index_set(), -// ); - -// assert_eq!(ranges, vec![]); -// } - -// #[test] -// fn single_range_with_removals() { -// let ranges = find_ranges( -// [1, 2, 3, 4].iter().into_fx_index_set(), -// [1, 2, 3, 4].iter().into_fx_index_set(), -// &[1, 2, 5, 3, 4].into_fx_index_set(), -// &[1, 2, 3, 4].into_fx_index_set(), -// ); - -// assert_eq!(ranges, vec![]); -// } - -// #[test] -// fn two_ranges() { -// let ranges = find_ranges( -// [1, 2, 3, 4].iter().into_fx_index_set(), -// [3, 4, 1, 2].iter().into_fx_index_set(), -// &[1, 2, 3, 4].into_fx_index_set(), -// &[3, 4, 1, 2].into_fx_index_set(), -// ); - -// assert_eq!( -// ranges, -// vec![ -// DiffOpMove { -// from: 2, -// to: 0, -// len: 2, -// move_in_dom: true, -// }, -// DiffOpMove { -// from: 0, -// to: 2, -// len: 2, -// move_in_dom: true, -// }, -// ] -// ); -// } - -// #[test] -// fn two_ranges_with_adds() { -// let ranges = find_ranges( -// [1, 2, 3, 4].iter().into_fx_index_set(), -// [3, 4, 1, 2].iter().into_fx_index_set(), -// &[1, 2, 3, 4].into_fx_index_set(), -// &[3, 4, 5, 1, 6, 2].into_fx_index_set(), -// ); - -// assert_eq!( -// ranges, -// vec![ -// DiffOpMove { -// from: 2, -// to: 0, -// len: 2, -// }, -// DiffOpMove { -// from: 0, -// to: 3, -// len: 2, -// }, -// ] -// ); -// } -// #[test] -// fn two_ranges_with_removals() { -// let ranges = find_ranges( -// [1, 2, 3, 4].iter().into_fx_index_set(), -// [3, 4, 1, 2].iter().into_fx_index_set(), -// &[1, 5, 2, 6, 3, 4].into_fx_index_set(), -// &[3, 4, 1, 2].into_fx_index_set(), -// ); - -// assert_eq!( -// ranges, -// vec![ -// DiffOpMove { -// from: 4, -// to: 0, -// len: 2, -// }, -// DiffOpMove { -// from: 0, -// to: 2, -// len: 2, -// }, -// ] -// ); -// } - -// #[test] -// fn remove_ranges_that_did_not_move() { -// // Here, 'C' doesn't change -// let ranges = find_ranges( -// ['A', 'B', 'C', 'D'].iter().into_fx_index_set(), -// ['B', 'D', 'C', 'A'].iter().into_fx_index_set(), -// &['A', 'B', 'C', 'D'].into_fx_index_set(), -// &['B', 'D', 'C', 'A'].into_fx_index_set(), -// ); - -// assert_eq!( -// ranges, -// vec![ -// DiffOpMove { -// from: 1, -// to: 0, -// len: 1, -// }, -// DiffOpMove { -// from: 3, -// to: 1, -// len: 1, -// }, -// DiffOpMove { -// from: 0, -// to: 3, -// len: 1, -// }, -// ] -// ); - -// // Now we're going to do the same as above, just with more items -// // -// // A = 1 -// // B = 2, 3 -// // C = 4, 5, 6 -// // D = 7, 8, 9, 0 - -// let ranges = find_ranges( -// //A B C D -// [1, 2, 3, 4, 5, 6, 7, 8, 9, 0].iter().into_fx_index_set(), -// //B D C A -// [2, 3, 7, 8, 9, 0, 4, 5, 6, 1].iter().into_fx_index_set(), -// //A B C D -// &[1, 2, 3, 4, 5, 6, 7, 8, 9, 0].into_fx_index_set(), -// //B D C A -// &[2, 3, 7, 8, 9, 0, 4, 5, 6, 1].into_fx_index_set(), -// ); - -// assert_eq!( -// ranges, -// vec![ -// DiffOpMove { -// from: 1, -// to: 0, -// len: 2, -// }, -// DiffOpMove { -// from: 6, -// to: 2, -// len: 4, -// }, -// DiffOpMove { -// from: 0, -// to: 9, -// len: 1, -// }, -// ] -// ); -// } -// } - -// #[cfg(test)] -// mod optimize_moves { -// use super::*; - -// #[test] -// fn swap() { -// let mut moves = vec![ -// DiffOpMove { -// from: 0, -// to: 6, -// len: 2, -// ..Default::default() -// }, -// DiffOpMove { -// from: 6, -// to: 0, -// len: 7, -// ..Default::default() -// }, -// ]; - -// optimize_moves(&mut moves); - -// assert_eq!( -// moves, -// vec![DiffOpMove { -// from: 0, -// to: 6, -// len: 2, -// ..Default::default() -// }] -// ); -// } -// } - -// #[cfg(test)] -// mod add_or_move { -// use super::*; - -// #[test] -// fn simple_range() { -// let cmds = AddOrMove::from_diff(&Diff { -// moved: vec![DiffOpMove { -// from: 0, -// to: 0, -// len: 3, -// }], -// ..Default::default() -// }); - -// assert_eq!( -// cmds, -// vec![ -// DiffOpMove { -// from: 0, -// to: 0, -// len: 1, -// }, -// DiffOpMove { -// from: 1, -// to: 1, -// len: 1, -// }, -// DiffOpMove { -// from: 2, -// to: 2, -// len: 1, -// }, -// ] -// ); -// } - -// #[test] -// fn range_with_add() { -// let cmds = AddOrMove::from_diff(&Diff { -// moved: vec![DiffOpMove { -// from: 0, -// to: 0, -// len: 3, -// move_in_dom: true, -// }], -// added: vec![DiffOpAdd { -// at: 2, -// ..Default::default() -// }], -// ..Default::default() -// }); - -// assert_eq!( -// cmds, -// vec![ -// AddOrMove::Move(DiffOpMove { -// from: 0, -// to: 0, -// len: 1, -// move_in_dom: true, -// }), -// AddOrMove::Move(DiffOpMove { -// from: 1, -// to: 1, -// len: 1, -// move_in_dom: true, -// }), -// AddOrMove::Add(DiffOpAdd { -// at: 2, -// ..Default::default() -// }), -// AddOrMove::Move(DiffOpMove { -// from: 3, -// to: 3, -// len: 1, -// move_in_dom: true, -// }), -// ] -// ); -// } -// } - -// #[cfg(test)] -// mod diff { -// use super::*; - -// #[test] -// fn only_adds() { -// let diff = -// diff(&[].into_fx_index_set(), &[1, 2, 3].into_fx_index_set()); - -// assert_eq!( -// diff, -// Diff { -// added: vec![ -// DiffOpAdd { -// at: 0, -// mode: DiffOpAddMode::Append -// }, -// DiffOpAdd { -// at: 1, -// mode: DiffOpAddMode::Append -// }, -// DiffOpAdd { -// at: 2, -// mode: DiffOpAddMode::Append -// }, -// ], -// ..Default::default() -// } -// ); -// } - -// #[test] -// fn only_removes() { -// let diff = -// diff(&[1, 2, 3].into_fx_index_set(), &[3].into_fx_index_set()); - -// assert_eq!( -// diff, -// Diff { -// removed: vec![DiffOpRemove { at: 0 }, DiffOpRemove { at: 1 }], -// ..Default::default() -// } -// ); -// } - -// #[test] -// fn adds_with_no_move() { -// let diff = -// diff(&[3].into_fx_index_set(), &[1, 2, 3].into_fx_index_set()); - -// assert_eq!( -// diff, -// Diff { -// added: vec![ -// DiffOpAdd { -// at: 0, -// ..Default::default() -// }, -// DiffOpAdd { -// at: 1, -// ..Default::default() -// }, -// ], -// ..Default::default() -// } -// ); -// } -// } diff --git a/leptos_dom/src/components/errors.rs b/leptos_dom/src/components/errors.rs deleted file mode 100644 index 0b0159da0..000000000 --- a/leptos_dom/src/components/errors.rs +++ /dev/null @@ -1,166 +0,0 @@ -use crate::{HydrationCtx, IntoView}; -use cfg_if::cfg_if; -use leptos_reactive::{signal_prelude::*, use_context}; -use server_fn::error::Error; -use std::{borrow::Cow, collections::HashMap}; - -/// A struct to hold all the possible errors that could be provided by child Views -#[derive(Debug, Clone, Default)] -#[repr(transparent)] -pub struct Errors(HashMap); - -/// A unique key for an error that occurs at a particular location in the user interface. -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -#[repr(transparent)] -pub struct ErrorKey(Cow<'static, str>); - -impl From for ErrorKey -where - T: Into>, -{ - #[inline(always)] - fn from(key: T) -> ErrorKey { - ErrorKey(key.into()) - } -} - -impl IntoIterator for Errors { - type Item = (ErrorKey, Error); - type IntoIter = IntoIter; - - #[inline(always)] - fn into_iter(self) -> Self::IntoIter { - IntoIter(self.0.into_iter()) - } -} - -/// An owning iterator over all the errors contained in the [`Errors`] struct. -#[repr(transparent)] -pub struct IntoIter(std::collections::hash_map::IntoIter); - -impl Iterator for IntoIter { - type Item = (ErrorKey, Error); - - #[inline(always)] - fn next( - &mut self, - ) -> std::option::Option<::Item> { - self.0.next() - } -} - -/// An iterator over all the errors contained in the [`Errors`] struct. -#[repr(transparent)] -pub struct Iter<'a>(std::collections::hash_map::Iter<'a, ErrorKey, Error>); - -impl<'a> Iterator for Iter<'a> { - type Item = (&'a ErrorKey, &'a Error); - - #[inline(always)] - fn next( - &mut self, - ) -> std::option::Option<::Item> { - self.0.next() - } -} - -impl IntoView for Result -where - T: IntoView + 'static, - E: Into, -{ - fn into_view(self) -> crate::View { - let id = ErrorKey( - HydrationCtx::peek() - .map(|n| n.to_string()) - .unwrap_or_default() - .into(), - ); - let errors = use_context::>(); - match self { - Ok(stuff) => { - if let Some(errors) = errors { - errors.update(|errors| { - errors.0.remove(&id); - }); - } - stuff.into_view() - } - Err(error) => { - let error = error.into(); - match errors { - Some(errors) => { - errors.update({ - #[cfg(all( - target_arch = "wasm32", - feature = "web" - ))] - let id = id.clone(); - move |errors: &mut Errors| errors.insert(id, error) - }); - - // remove the error from the list if this drops, - // i.e., if it's in a DynChild that switches from Err to Ok - // Only can run on the client, will panic on the server - cfg_if! { - if #[cfg(all(target_arch = "wasm32", feature = "web"))] { - use leptos_reactive::{on_cleanup, queue_microtask}; - on_cleanup(move || { - queue_microtask(move || { - errors.update(|errors: &mut Errors| { - errors.remove(&id); - }); - }); - }); - } - } - } - None => { - #[cfg(debug_assertions)] - warn!( - "No ErrorBoundary components found! Returning \ - errors will not be handled and will silently \ - disappear" - ); - } - } - ().into_view() - } - } - } -} - -impl Errors { - /// Returns `true` if there are no errors. - #[inline(always)] - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Add an error to Errors that will be processed by `` - pub fn insert(&mut self, key: ErrorKey, error: E) - where - E: Into, - { - self.0.insert(key, error.into()); - } - - /// Add an error with the default key for errors outside the reactive system - pub fn insert_with_default_key(&mut self, error: E) - where - E: Into, - { - self.0.insert(Default::default(), error.into()); - } - - /// Remove an error to Errors that will be processed by `` - pub fn remove(&mut self, key: &ErrorKey) -> Option { - self.0.remove(key) - } - - /// An iterator over all the errors, in arbitrary order. - #[inline(always)] - pub fn iter(&self) -> Iter<'_> { - Iter(self.0.iter()) - } -} diff --git a/leptos_dom/src/components/fragment.rs b/leptos_dom/src/components/fragment.rs deleted file mode 100644 index 2c4f80fb5..000000000 --- a/leptos_dom/src/components/fragment.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::{ - hydration::HydrationKey, ComponentRepr, HydrationCtx, IntoView, View, -}; - -/// Trait for converting any iterable into a [`Fragment`]. -pub trait IntoFragment { - /// Consumes this type, returning [`Fragment`]. - fn into_fragment(self) -> Fragment; -} - -impl IntoFragment for I -where - I: IntoIterator, - V: IntoView, -{ - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", skip_all,) - )] - fn into_fragment(self) -> Fragment { - self.into_iter().map(|v| v.into_view()).collect() - } -} - -/// Represents a group of [`views`](View). -#[must_use = "You are creating a Fragment but not using it. An unused view can \ - cause your view to be rendered as () unexpectedly, and it can \ - also cause issues with client-side hydration."] -#[derive(Debug, Clone)] -pub struct Fragment { - id: Option, - /// The nodes contained in the fragment. - pub nodes: Vec, - #[cfg(debug_assertions)] - pub(crate) view_marker: Option, -} - -impl FromIterator for Fragment { - fn from_iter>(iter: T) -> Self { - Fragment::new(iter.into_iter().collect()) - } -} - -impl From for Fragment { - fn from(view: View) -> Self { - Fragment::new(vec![view]) - } -} - -impl From for View { - fn from(value: Fragment) -> Self { - let mut frag = ComponentRepr::new_with_id("", value.id); - - #[cfg(debug_assertions)] - { - frag.view_marker = value.view_marker; - } - - frag.children = value.nodes; - - frag.into() - } -} - -impl Fragment { - /// Creates a new [`Fragment`] from a [`Vec`]. - #[inline(always)] - pub fn new(nodes: Vec) -> Self { - Self::new_with_id(HydrationCtx::id(), nodes) - } - - /// Creates a new [`Fragment`] from a function that returns [`Vec`]. - #[inline(always)] - pub fn lazy(nodes: impl FnOnce() -> Vec) -> Self { - Self::new_with_id(HydrationCtx::id(), nodes()) - } - - /// Creates a new [`Fragment`] with the given hydration ID from a [`Vec`]. - #[inline(always)] - pub const fn new_with_id( - id: Option, - nodes: Vec, - ) -> Self { - Self { - id, - nodes, - #[cfg(debug_assertions)] - view_marker: None, - } - } - - /// Gives access to the [`View`] children contained within the fragment. - #[inline(always)] - pub fn as_children(&self) -> &[View] { - &self.nodes - } - - /// Returns the fragment's hydration ID. - #[inline(always)] - pub fn id(&self) -> &Option { - &self.id - } - - #[cfg(debug_assertions)] - /// Adds an optional marker indicating the view macro source. - pub fn with_view_marker(mut self, marker: impl Into) -> Self { - self.view_marker = Some(marker.into()); - self - } -} - -impl IntoView for Fragment { - #[cfg_attr(debug_assertions, instrument(level = "trace", name = "", skip_all, fields(children = self.nodes.len())))] - fn into_view(self) -> View { - self.into() - } -} diff --git a/leptos_dom/src/components/unit.rs b/leptos_dom/src/components/unit.rs deleted file mode 100644 index 34a91573e..000000000 --- a/leptos_dom/src/components/unit.rs +++ /dev/null @@ -1,73 +0,0 @@ -use cfg_if::cfg_if; -use std::fmt; - -cfg_if! { - if #[cfg(all(target_arch = "wasm32", feature = "web"))] { - use crate::Mountable; - use wasm_bindgen::JsCast; - } else { - use crate::hydration::HydrationKey; - } -} - -use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View}; - -/// The internal representation of the [`Unit`] core-component. -#[derive(Clone, PartialEq, Eq)] -pub struct UnitRepr { - comment: Comment, - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - pub(crate) id: Option, -} - -impl fmt::Debug for UnitRepr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("<() />") - } -} - -impl Default for UnitRepr { - fn default() -> Self { - let id = HydrationCtx::id(); - - Self { - comment: Comment::new("<() />", &id, true), - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - id, - } - } -} - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -impl Mountable for UnitRepr { - #[inline(always)] - fn get_mountable_node(&self) -> web_sys::Node { - self.comment.node.clone().unchecked_into() - } - - #[inline(always)] - fn get_opening_node(&self) -> web_sys::Node { - self.comment.node.clone().unchecked_into() - } - - #[inline(always)] - fn get_closing_node(&self) -> web_sys::Node { - self.comment.node.clone().unchecked_into() - } -} - -/// The unit `()` leptos counterpart. -#[derive(Clone, Copy, Debug)] -pub struct Unit; - -impl IntoView for Unit { - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", name = "<() />", skip_all) - )] - fn into_view(self) -> crate::View { - let component = UnitRepr::default(); - - View::CoreComponent(CoreComponent::Unit(component)) - } -} diff --git a/leptos_dom/src/directive.rs b/leptos_dom/src/directive.rs deleted file mode 100644 index ed343b61a..000000000 --- a/leptos_dom/src/directive.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::{html::AnyElement, HtmlElement}; -use std::rc::Rc; - -/// Trait for a directive handler function. -/// This is used so it's possible to use functions with one or two -/// parameters as directive handlers. -/// -/// You can use directives like the following. -/// -/// ``` -/// # use leptos::{*, html::AnyElement}; -/// -/// // This doesn't take an attribute value -/// fn my_directive(el: HtmlElement) { -/// // do sth -/// } -/// -/// // This requires an attribute value -/// fn another_directive(el: HtmlElement, params: i32) { -/// // do sth -/// } -/// -/// #[component] -/// pub fn MyComponent() -> impl IntoView { -/// view! { -/// // no attribute value -///
-/// -/// // with an attribute value -///
-/// } -/// } -/// ``` -/// -/// A directive is just syntactic sugar for -/// -/// ```ignore -/// let node_ref = create_node_ref(); -/// -/// create_effect(move |_| { -/// if let Some(el) = node_ref.get() { -/// directive_func(el, possibly_some_param); -/// } -/// }); -/// ``` -/// -/// A directive can be a function with one or two parameters. -/// The first is the element the directive is added to and the optional -/// second is the parameter that is provided in the attribute. -pub trait Directive { - /// Calls the handler function - fn run(&self, el: HtmlElement, param: P); -} - -impl Directive<(HtmlElement,), ()> for F -where - F: Fn(HtmlElement), -{ - fn run(&self, el: HtmlElement, _: ()) { - self(el) - } -} - -impl Directive<(HtmlElement, P), P> for F -where - F: Fn(HtmlElement, P), -{ - fn run(&self, el: HtmlElement, param: P) { - self(el, param); - } -} - -impl Directive for Rc> { - fn run(&self, el: HtmlElement, param: P) { - (**self).run(el, param) - } -} - -impl Directive for Box> { - fn run(&self, el: HtmlElement, param: P) { - (**self).run(el, param); - } -} diff --git a/leptos_dom/src/events.rs b/leptos_dom/src/events.rs deleted file mode 100644 index ddf557d21..000000000 --- a/leptos_dom/src/events.rs +++ /dev/null @@ -1,213 +0,0 @@ -pub mod typed; - -use leptos_reactive::Oco; -use std::{cell::RefCell, collections::HashSet}; -#[cfg(all(target_arch = "wasm32", feature = "web"))] -use wasm_bindgen::{ - convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue, - UnwrapThrowExt, -}; - -thread_local! { - pub(crate) static GLOBAL_EVENTS: RefCell>> = RefCell::new(HashSet::new()); -} - -// Used in template macro -#[doc(hidden)] -#[cfg(all(target_arch = "wasm32", feature = "web"))] -#[inline(always)] -pub fn add_event_helper( - target: &web_sys::Element, - event: E, - #[allow(unused_mut)] // used for tracing in debug - mut event_handler: impl FnMut(E::EventType) + 'static, -) { - let event_name = event.name(); - let event_handler = Box::new(event_handler); - - if E::BUBBLES { - add_event_listener( - target, - event.event_delegation_key(), - event_name, - event_handler, - &None, - ); - } else { - add_event_listener_undelegated( - target, - &event_name, - event_handler, - &None, - ); - } -} - -/// Adds an event listener to the target DOM element using implicit event delegation. -#[doc(hidden)] -#[cfg(all(target_arch = "wasm32", feature = "web"))] -pub fn add_event_listener( - target: &web_sys::Element, - key: Oco<'static, str>, - event_name: Oco<'static, str>, - #[cfg(debug_assertions)] mut cb: Box, - #[cfg(not(debug_assertions))] cb: Box, - options: &Option, -) where - E: FromWasmAbi + 'static, -{ - cfg_if::cfg_if! { - if #[cfg(debug_assertions)] { - let span = ::tracing::Span::current(); - let cb = Box::new(move |e| { - let prev = leptos_reactive::SpecialNonReactiveZone::enter(); - let _guard = span.enter(); - cb(e); - leptos_reactive::SpecialNonReactiveZone::exit(prev); - }); - } - } - - let cb = Closure::wrap(cb as Box).into_js_value(); - let key = intern(&key); - debug_assert_eq!( - Ok(false), - js_sys::Reflect::has(target, &JsValue::from_str(&key)), - "Error while adding {key} event listener, a listener of type {key} \ - already present." - ); - _ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb); - add_delegated_event_listener(&key, event_name, options); -} - -#[doc(hidden)] -#[cfg(all(target_arch = "wasm32", feature = "web"))] -pub(crate) fn add_event_listener_undelegated( - target: &web_sys::Element, - event_name: &str, - #[cfg(debug_assertions)] mut cb: Box, - #[cfg(not(debug_assertions))] cb: Box, - options: &Option, -) where - E: FromWasmAbi + 'static, -{ - cfg_if::cfg_if! { - if #[cfg(debug_assertions)] { - let span = ::tracing::Span::current(); - let cb = Box::new(move |e| { - let prev = leptos_reactive::SpecialNonReactiveZone::enter(); - let _guard = span.enter(); - cb(e); - leptos_reactive::SpecialNonReactiveZone::exit(prev); - }); - } - } - - let event_name = intern(event_name); - let cb = Closure::wrap(cb as Box).into_js_value(); - if let Some(options) = options { - _ = target - .add_event_listener_with_callback_and_add_event_listener_options( - event_name, - cb.unchecked_ref(), - options, - ); - } else { - _ = target - .add_event_listener_with_callback(event_name, cb.unchecked_ref()); - } -} - -// cf eventHandler in ryansolid/dom-expressions -#[cfg(all(target_arch = "wasm32", feature = "web"))] -pub(crate) fn add_delegated_event_listener( - key: &str, - event_name: Oco<'static, str>, - options: &Option, -) { - GLOBAL_EVENTS.with(|global_events| { - let mut events = global_events.borrow_mut(); - if !events.contains(&event_name) { - // create global handler - let key = JsValue::from_str(&key); - let handler = move |ev: web_sys::Event| { - let target = ev.target(); - let node = ev.composed_path().get(0); - let mut node = if node.is_undefined() || node.is_null() { - JsValue::from(target) - } else { - node - }; - - // TODO reverse Shadow DOM retargetting - - // TODO simulate currentTarget - - while !node.is_null() { - let node_is_disabled = js_sys::Reflect::get( - &node, - &JsValue::from_str("disabled"), - ) - .unwrap_throw() - .is_truthy(); - if !node_is_disabled { - let maybe_handler = - js_sys::Reflect::get(&node, &key).unwrap_throw(); - if !maybe_handler.is_undefined() { - let f = maybe_handler - .unchecked_ref::(); - - if let Err(e) = f.call1(&node, &ev) { - wasm_bindgen::throw_val(e); - } - - if ev.cancel_bubble() { - return; - } - } - } - - // navigate up tree - if let Some(parent) = - node.unchecked_ref::().parent_node() - { - node = parent.into() - } else if let Some(root) = node.dyn_ref::() { - node = root.host().unchecked_into(); - } else { - node = JsValue::null() - } - } - }; - - cfg_if::cfg_if! { - if #[cfg(debug_assertions)] { - let span = ::tracing::Span::current(); - let handler = move |e| { - let _guard = span.enter(); - handler(e); - }; - } - } - - let handler = Box::new(handler) as Box; - let handler = Closure::wrap(handler).into_js_value(); - if let Some(options) = options { - _ = crate::window().add_event_listener_with_callback_and_add_event_listener_options( - &event_name, - handler.unchecked_ref(), - options, - ); - } else { - _ = crate::window().add_event_listener_with_callback( - &event_name, - handler.unchecked_ref(), - ); - - } - - // register that we've created handler - events.insert(event_name); - } - }) -} diff --git a/leptos_dom/src/events/typed.rs b/leptos_dom/src/events/typed.rs deleted file mode 100644 index c981afbbf..000000000 --- a/leptos_dom/src/events/typed.rs +++ /dev/null @@ -1,681 +0,0 @@ -//! Types for all DOM events. - -use leptos_reactive::Oco; -use std::marker::PhantomData; -use wasm_bindgen::convert::FromWasmAbi; - -/// A trait for converting types into [web_sys events](web_sys). -pub trait EventDescriptor: Clone { - /// The [`web_sys`] event type, such as [`web_sys::MouseEvent`]. - type EventType: FromWasmAbi; - - /// Indicates if this event bubbles. For example, `click` bubbles, - /// but `focus` does not. - /// - /// If this is true, then the event will be delegated globally, - /// otherwise, event listeners will be directly attached to the element. - const BUBBLES: bool; - - /// The name of the event, such as `click` or `mouseover`. - fn name(&self) -> Oco<'static, str>; - - /// The key used for event delegation. - fn event_delegation_key(&self) -> Oco<'static, str>; - - /// Return the options for this type. This is only used when you create a [`Custom`] event - /// handler. - #[inline(always)] - fn options(&self) -> &Option { - &None - } -} - -/// Overrides the [`EventDescriptor::BUBBLES`] value to always return -/// `false`, which forces the event to not be globally delegated. -#[derive(Clone, Debug)] -#[allow(non_camel_case_types)] -pub struct undelegated(pub Ev); - -impl EventDescriptor for undelegated { - type EventType = Ev::EventType; - - #[inline(always)] - fn name(&self) -> Oco<'static, str> { - self.0.name() - } - - #[inline(always)] - fn event_delegation_key(&self) -> Oco<'static, str> { - self.0.event_delegation_key() - } - - const BUBBLES: bool = false; -} - -/// A custom event. -#[derive(Debug)] -pub struct Custom { - name: Oco<'static, str>, - options: Option, - _event_type: PhantomData, -} - -impl Clone for Custom { - fn clone(&self) -> Self { - Self { - name: self.name.clone(), - options: self.options.clone(), - _event_type: PhantomData, - } - } -} - -impl EventDescriptor for Custom { - type EventType = E; - - fn name(&self) -> Oco<'static, str> { - self.name.clone() - } - - fn event_delegation_key(&self) -> Oco<'static, str> { - format!("$$${}", self.name).into() - } - - const BUBBLES: bool = false; - - #[inline(always)] - fn options(&self) -> &Option { - &self.options - } -} - -impl Custom { - /// Creates a custom event type that can be used within - /// [`HtmlElement::on`](crate::HtmlElement::on), for events - /// which are not covered in the [`ev`](crate::ev) module. - pub fn new(name: impl Into>) -> Self { - Self { - name: name.into(), - options: None, - _event_type: PhantomData, - } - } - - /// Modify the [`AddEventListenerOptions`] used for this event listener. - /// - /// ```rust - /// # use leptos::*; - /// # let runtime = create_runtime(); - /// # let canvas_ref: NodeRef = create_node_ref(); - /// # if false { - /// let mut non_passive_wheel = ev::Custom::::new("wheel"); - /// let options = non_passive_wheel.options_mut(); - /// options.passive(false); - /// canvas_ref.on_load(move |canvas: HtmlElement| { - /// canvas.on(non_passive_wheel, move |_event| { - /// // Handle _event - /// }); - /// }); - /// # } - /// # runtime.dispose(); - /// ``` - /// - /// [`AddEventListenerOptions`]: web_sys::AddEventListenerOptions - pub fn options_mut(&mut self) -> &mut web_sys::AddEventListenerOptions { - self.options - .get_or_insert_with(web_sys::AddEventListenerOptions::new) - } -} - -/// Type that can respond to DOM events -pub trait DOMEventResponder: Sized { - /// Adds handler to specified event - fn add( - self, - event: E, - handler: impl FnMut(E::EventType) + 'static, - ) -> Self; - /// Same as [add](DOMEventResponder::add), but with [`EventHandler`] - #[inline] - fn add_handler(self, handler: impl EventHandler) -> Self { - handler.attach(self) - } -} - -impl DOMEventResponder for crate::HtmlElement -where - T: crate::html::ElementDescriptor + 'static, -{ - #[inline(always)] - fn add( - self, - event: E, - handler: impl FnMut(E::EventType) + 'static, - ) -> Self { - self.on(event, handler) - } -} - -impl DOMEventResponder for crate::View { - #[inline(always)] - fn add( - self, - event: E, - handler: impl FnMut(E::EventType) + 'static, - ) -> Self { - self.on(event, handler) - } -} - -/// A statically typed event handler. -pub enum EventHandlerFn { - /// `keydown` event handler. - Keydown(Box), - /// `keyup` event handler. - Keyup(Box), - /// `keypress` event handler. - Keypress(Box), - - /// `click` event handler. - Click(Box), - /// `dblclick` event handler. - Dblclick(Box), - /// `mousedown` event handler. - Mousedown(Box), - /// `mouseup` event handler. - Mouseup(Box), - /// `mouseenter` event handler. - Mouseenter(Box), - /// `mouseleave` event handler. - Mouseleave(Box), - /// `mouseout` event handler. - Mouseout(Box), - /// `mouseover` event handler. - Mouseover(Box), - /// `mousemove` event handler. - Mousemove(Box), - - /// `wheel` event handler. - Wheel(Box), - - /// `touchstart` event handler. - Touchstart(Box), - /// `touchend` event handler. - Touchend(Box), - /// `touchcancel` event handler. - Touchcancel(Box), - /// `touchmove` event handler. - Touchmove(Box), - - /// `pointerenter` event handler. - Pointerenter(Box), - /// `pointerleave` event handler. - Pointerleave(Box), - /// `pointerdown` event handler. - Pointerdown(Box), - /// `pointerup` event handler. - Pointerup(Box), - /// `pointercancel` event handler. - Pointercancel(Box), - /// `pointerout` event handler. - Pointerout(Box), - /// `pointerover` event handler. - Pointerover(Box), - /// `pointermove` event handler. - Pointermove(Box), - - /// `drag` event handler. - Drag(Box), - /// `dragend` event handler. - Dragend(Box), - /// `dragenter` event handler. - Dragenter(Box), - /// `dragleave` event handler. - Dragleave(Box), - /// `dragstart` event handler. - Dragstart(Box), - /// `drop` event handler. - Drop(Box), - - /// `blur` event handler. - Blur(Box), - /// `focusout` event handler. - Focusout(Box), - /// `focus` event handler. - Focus(Box), - /// `focusin` event handler. - Focusin(Box), -} - -/// Type that can be used to handle DOM events -pub trait EventHandler { - /// Attaches event listener to any target that can respond to DOM events - fn attach(self, target: T) -> T; -} - -impl EventHandler for [T; N] -where - T: EventHandler, -{ - #[inline] - fn attach(self, target: R) -> R { - let mut target = target; - for item in self { - target = item.attach(target); - } - target - } -} - -impl EventHandler for Option -where - T: EventHandler, -{ - #[inline] - fn attach(self, target: R) -> R { - match self { - Some(event_handler) => event_handler.attach(target), - None => target, - } - } -} - -macro_rules! tc { - ($($ty:ident),*) => { - impl<$($ty),*> EventHandler for ($($ty,)*) - where - $($ty: EventHandler),* - { - #[inline] - fn attach(self, target: RES) -> RES { - ::paste::paste! { - let ( - $( - [<$ty:lower>],)* - ) = self; - $( - let target = [<$ty:lower>].attach(target); - )* - target - } - } - } - }; -} - -tc!(A); -tc!(A, B); -tc!(A, B, C); -tc!(A, B, C, D); -tc!(A, B, C, D, E); -tc!(A, B, C, D, E, F); -tc!(A, B, C, D, E, F, G); -tc!(A, B, C, D, E, F, G, H); -tc!(A, B, C, D, E, F, G, H, I); -tc!(A, B, C, D, E, F, G, H, I, J); -tc!(A, B, C, D, E, F, G, H, I, J, K); -tc!(A, B, C, D, E, F, G, H, I, J, K, L); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X); -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y); -#[rustfmt::skip] -tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z); - -macro_rules! collection_callback { - {$( - $collection:ident - ),* $(,)?} => { - $( - impl EventHandler for $collection - where - T: EventHandler - { - #[inline] - fn attach(self, target: R) -> R { - let mut target = target; - for item in self { - target = item.attach(target); - } - target - } - } - )* - }; -} - -use std::collections::{BTreeSet, BinaryHeap, HashSet, LinkedList, VecDeque}; - -collection_callback! { - Vec, - BTreeSet, - BinaryHeap, - HashSet, - LinkedList, - VecDeque, -} - -macro_rules! generate_event_types { - {$( - $( #[$does_not_bubble:ident] )? - $( $event:ident )+ : $web_event:ident - ),* $(,)?} => { - ::paste::paste! { - $( - #[doc = "The `" [< $($event)+ >] "` event, which receives [" $web_event "](web_sys::" $web_event ") as its argument."] - #[derive(Copy, Clone, Debug)] - #[allow(non_camel_case_types)] - pub struct [<$( $event )+ >]; - - impl EventDescriptor for [< $($event)+ >] { - type EventType = web_sys::$web_event; - - #[inline(always)] - fn name(&self) -> Oco<'static, str> { - stringify!([< $($event)+ >]).into() - } - - #[inline(always)] - fn event_delegation_key(&self) -> Oco<'static, str> { - concat!("$$$", stringify!([< $($event)+ >])).into() - } - - const BUBBLES: bool = true $(&& generate_event_types!($does_not_bubble))?; - } - )* - - /// An enum holding all basic event types with their respective handlers. - /// - /// It currently omits [`Custom`] and [`undelegated`] variants. - #[non_exhaustive] - pub enum GenericEventHandler { - $( - #[doc = "Variant mapping [`struct@" [< $($event)+ >] "`] to its event handler type."] - [< $($event:camel)+ >]([< $($event)+ >], Box), - )* - } - - impl ::core::fmt::Debug for GenericEventHandler { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - match self { - $( - Self::[< $($event:camel)+ >](event, _) => f - .debug_tuple(stringify!([< $($event:camel)+ >])) - .field(&event) - .field(&::std::any::type_name::>()) - .finish(), - )* - } - } - } - - impl EventHandler for GenericEventHandler { - fn attach(self, target: T) -> T { - match self { - $( - Self::[< $($event:camel)+ >](event, handler) => target.add(event, handler), - )* - } - } - } - - $( - impl From<([< $($event)+ >], F)> for GenericEventHandler - where - F: FnMut($web_event) + 'static - { - fn from(value: ([< $($event)+ >], F)) -> Self { - Self::[< $($event:camel)+ >](value.0, Box::new(value.1)) - } - } - // NOTE: this could become legal in future and would save us from useless allocations - //impl From<([< $($event)+ >], Box)> for GenericEventHandler - //where - // F: FnMut($web_event) + 'static - //{ - // fn from(value: ([< $($event)+ >], Box)) -> Self { - // Self::[< $($event:camel)+ >](value.0, value.1) - // } - //} - impl EventHandler for ([< $($event)+ >], F) - where - F: FnMut($web_event) + 'static - { - fn attach(self, target: L) -> L { - target.add(self.0, self.1) - } - } - )* - } - }; - - (does_not_bubble) => { false } -} - -generate_event_types! { - // ========================================================= - // WindowEventHandlersEventMap - // ========================================================= - #[does_not_bubble] - after print: Event, - #[does_not_bubble] - before print: Event, - #[does_not_bubble] - before unload: BeforeUnloadEvent, - #[does_not_bubble] - gamepad connected: GamepadEvent, - #[does_not_bubble] - gamepad disconnected: GamepadEvent, - hash change: HashChangeEvent, - #[does_not_bubble] - language change: Event, - #[does_not_bubble] - message: MessageEvent, - #[does_not_bubble] - message error: MessageEvent, - #[does_not_bubble] - offline: Event, - #[does_not_bubble] - online: Event, - #[does_not_bubble] - page hide: PageTransitionEvent, - #[does_not_bubble] - page show: PageTransitionEvent, - pop state: PopStateEvent, - rejection handled: PromiseRejectionEvent, - #[does_not_bubble] - storage: StorageEvent, - #[does_not_bubble] - unhandled rejection: PromiseRejectionEvent, - #[does_not_bubble] - unload: Event, - - // ========================================================= - // GlobalEventHandlersEventMap - // ========================================================= - #[does_not_bubble] - abort: UiEvent, - animation cancel: AnimationEvent, - animation end: AnimationEvent, - animation iteration: AnimationEvent, - animation start: AnimationEvent, - aux click: MouseEvent, - before input: InputEvent, - #[does_not_bubble] - blur: FocusEvent, - #[does_not_bubble] - can play: Event, - #[does_not_bubble] - can play through: Event, - change: Event, - click: MouseEvent, - #[does_not_bubble] - close: Event, - composition end: CompositionEvent, - composition start: CompositionEvent, - composition update: CompositionEvent, - context menu: MouseEvent, - #[does_not_bubble] - cue change: Event, - dbl click: MouseEvent, - drag: DragEvent, - drag end: DragEvent, - drag enter: DragEvent, - drag leave: DragEvent, - drag over: DragEvent, - drag start: DragEvent, - drop: DragEvent, - #[does_not_bubble] - duration change: Event, - #[does_not_bubble] - emptied: Event, - #[does_not_bubble] - ended: Event, - #[does_not_bubble] - error: ErrorEvent, - #[does_not_bubble] - focus: FocusEvent, - #[does_not_bubble] - focus in: FocusEvent, - #[does_not_bubble] - focus out: FocusEvent, - form data: Event, // web_sys does not include `FormDataEvent` - #[does_not_bubble] - got pointer capture: PointerEvent, - input: Event, - #[does_not_bubble] - invalid: Event, - key down: KeyboardEvent, - key press: KeyboardEvent, - key up: KeyboardEvent, - #[does_not_bubble] - load: Event, - #[does_not_bubble] - loaded data: Event, - #[does_not_bubble] - loaded metadata: Event, - #[does_not_bubble] - load start: Event, - lost pointer capture: PointerEvent, - mouse down: MouseEvent, - #[does_not_bubble] - mouse enter: MouseEvent, - #[does_not_bubble] - mouse leave: MouseEvent, - mouse move: MouseEvent, - mouse out: MouseEvent, - mouse over: MouseEvent, - mouse up: MouseEvent, - #[does_not_bubble] - pause: Event, - #[does_not_bubble] - play: Event, - #[does_not_bubble] - playing: Event, - pointer cancel: PointerEvent, - pointer down: PointerEvent, - #[does_not_bubble] - pointer enter: PointerEvent, - #[does_not_bubble] - pointer leave: PointerEvent, - pointer move: PointerEvent, - pointer out: PointerEvent, - pointer over: PointerEvent, - pointer up: PointerEvent, - #[does_not_bubble] - progress: ProgressEvent, - #[does_not_bubble] - rate change: Event, - reset: Event, - #[does_not_bubble] - resize: UiEvent, - #[does_not_bubble] - scroll: Event, - #[does_not_bubble] - scroll end: Event, - security policy violation: SecurityPolicyViolationEvent, - #[does_not_bubble] - seeked: Event, - #[does_not_bubble] - seeking: Event, - select: Event, - #[does_not_bubble] - selection change: Event, - select start: Event, - slot change: Event, - #[does_not_bubble] - stalled: Event, - submit: SubmitEvent, - #[does_not_bubble] - suspend: Event, - #[does_not_bubble] - time update: Event, - #[does_not_bubble] - toggle: Event, - touch cancel: TouchEvent, - touch end: TouchEvent, - touch move: TouchEvent, - touch start: TouchEvent, - transition cancel: TransitionEvent, - transition end: TransitionEvent, - transition run: TransitionEvent, - transition start: TransitionEvent, - #[does_not_bubble] - volume change: Event, - #[does_not_bubble] - waiting: Event, - webkit animation end: Event, - webkit animation iteration: Event, - webkit animation start: Event, - webkit transition end: Event, - wheel: WheelEvent, - - // ========================================================= - // WindowEventMap - // ========================================================= - D O M Content Loaded: Event, // Hack for correct casing - #[does_not_bubble] - device motion: DeviceMotionEvent, - #[does_not_bubble] - device orientation: DeviceOrientationEvent, - #[does_not_bubble] - orientation change: Event, - - // ========================================================= - // DocumentAndElementEventHandlersEventMap - // ========================================================= - copy: Event, // ClipboardEvent is unstable - cut: Event, // ClipboardEvent is unstable - paste: Event, // ClipboardEvent is unstable - - // ========================================================= - // DocumentEventMap - // ========================================================= - fullscreen change: Event, - fullscreen error: Event, - pointer lock change: Event, - pointer lock error: Event, - #[does_not_bubble] - ready state change: Event, - visibility change: Event, -} - -// Export `web_sys` event types -pub use web_sys::{ - AnimationEvent, BeforeUnloadEvent, CompositionEvent, CustomEvent, - DeviceMotionEvent, DeviceOrientationEvent, DragEvent, ErrorEvent, Event, - FocusEvent, GamepadEvent, HashChangeEvent, InputEvent, KeyboardEvent, - MessageEvent, MouseEvent, PageTransitionEvent, PointerEvent, PopStateEvent, - ProgressEvent, PromiseRejectionEvent, SecurityPolicyViolationEvent, - StorageEvent, SubmitEvent, TouchEvent, TransitionEvent, UiEvent, - WheelEvent, -}; diff --git a/leptos_dom/src/html.rs b/leptos_dom/src/html.rs deleted file mode 100644 index 242d40c03..000000000 --- a/leptos_dom/src/html.rs +++ /dev/null @@ -1,1884 +0,0 @@ -//! Exports types for working with HTML elements. - -use cfg_if::cfg_if; - -cfg_if! { - if #[cfg(all(target_arch = "wasm32", feature = "web"))] { - use crate::events::*; - use crate::macro_helpers::*; - use crate::{mount_child, HydrationKey, MountKind}; - use once_cell::unsync::Lazy as LazyCell; - use std::cell::Cell; - use wasm_bindgen::JsCast; - - /// Trait alias for the trait bounts on [`ElementDescriptor`]. - pub trait ElementDescriptorBounds: - fmt::Debug + AsRef + Clone - { - } - - impl ElementDescriptorBounds for El where - El: fmt::Debug + AsRef + Clone - { - } - - thread_local! { - static IS_META: Cell = Cell::new(false); - } - - #[doc(hidden)] - pub fn as_meta_tag(f: impl FnOnce() -> T) -> T { - IS_META.with(|m| m.set(true)); - let v = f(); - IS_META.with(|m| m.set(false)); - v - } - - #[allow(unused)] - fn is_meta_tag() -> bool { - IS_META.with(|m| m.get()) - } - } else { - use crate::hydration::HydrationKey; - use smallvec::{smallvec, SmallVec}; - - pub(crate) const HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG: &str = - "`Deref` and `AsRef` \ - can only be used on web targets. \ - This is for the same reason that normal `wasm_bindgen` methods can be used \ - only in the browser. Please use `leptos::is_server()` or \ - `leptos::is_browser()` to check where you're running."; - - /// Trait alias for the trait bounts on [`ElementDescriptor`]. - pub trait ElementDescriptorBounds: fmt::Debug {} - - impl ElementDescriptorBounds for El where El: fmt::Debug {} - - #[doc(hidden)] - pub fn as_meta_tag(f: impl FnOnce() -> T) -> T { - f() - } - } -} - -use crate::{ - create_node_ref, - ev::{EventDescriptor, EventHandlerFn}, - hydration::HydrationCtx, - macro_helpers::{ - Attribute, IntoAttribute, IntoClass, IntoProperty, IntoStyle, - }, - Directive, Element, Fragment, IntoView, NodeRef, Text, View, -}; -use leptos_reactive::{create_effect, Oco}; -use std::{fmt, rc::Rc}; - -/// Trait which allows creating an element tag. -pub trait ElementDescriptor: ElementDescriptorBounds { - /// The name of the element, i.e., `div`, `p`, `custom-element`. - fn name(&self) -> Oco<'static, str>; - - /// Determines if the tag is void, i.e., `` and `
`. - #[inline(always)] - fn is_void(&self) -> bool { - false - } - - /// A unique `id` that should be generated for each new instance of - /// this element, and be consistent for both SSR and CSR. - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - fn hydration_id(&self) -> &Option; -} - -/// Trait for converting any type which impl [`AsRef`] -/// to [`HtmlElement`]. -pub trait ToHtmlElement { - /// Converts the type to [`HtmlElement`]. - fn to_leptos_element(self) -> HtmlElement; -} - -impl ToHtmlElement for T -where - T: AsRef, -{ - fn to_leptos_element(self) -> HtmlElement { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - let el = self.as_ref().clone().unchecked_into(); - - let element = AnyElement { - name: "".into(), - is_void: false, - element: el, - }; - - HtmlElement { - element, - #[cfg(debug_assertions)] - span: ::tracing::Span::current(), - #[cfg(debug_assertions)] - view_marker: None, - } - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - unreachable!(); - } - } -} - -/// Represents potentially any element. -#[must_use = "You are creating AnyElement but not using it. An unused view can \ - cause your view to be rendered as () unexpectedly, and it can \ - also cause issues with client-side hydration."] -#[derive(Clone, Debug)] -pub struct AnyElement { - pub(crate) name: Oco<'static, str>, - #[cfg(all(target_arch = "wasm32", feature = "web"))] - pub(crate) element: web_sys::HtmlElement, - pub(crate) is_void: bool, - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - pub(crate) id: Option, -} - -impl std::ops::Deref for AnyElement { - type Target = web_sys::HtmlElement; - - #[inline(always)] - fn deref(&self) -> &Self::Target { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - return &self.element; - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}"); - } -} - -impl std::convert::AsRef for AnyElement { - #[inline(always)] - fn as_ref(&self) -> &web_sys::HtmlElement { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - return &self.element; - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}"); - } -} - -impl ElementDescriptor for AnyElement { - fn name(&self) -> Oco<'static, str> { - self.name.clone() - } - - #[inline(always)] - fn is_void(&self) -> bool { - self.is_void - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - #[inline(always)] - fn hydration_id(&self) -> &Option { - &self.id - } -} - -/// Represents a custom HTML element, such as ``. -#[derive(Clone, Debug)] -pub struct Custom { - name: Oco<'static, str>, - #[cfg(all(target_arch = "wasm32", feature = "web"))] - element: web_sys::HtmlElement, - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - id: Option, -} - -impl Custom { - /// Creates a new custom element with the given tag name. - pub fn new(name: impl Into>) -> Self { - let name = name.into(); - let id = HydrationCtx::id(); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - let element = if HydrationCtx::is_hydrating() && id.is_some() { - #[allow(unused)] - let id = id.unwrap(); - #[cfg(feature = "hydrate")] - if let Some(el) = crate::hydration::get_element(&id.to_string()) { - #[cfg(debug_assertions)] - assert_eq!( - el.node_name().to_ascii_uppercase(), - name.to_ascii_uppercase(), - "SSR and CSR elements have the same hydration key but \ - different node kinds. Check out the docs for information \ - about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html" - ); - - //el.remove_attribute(wasm_bindgen::intern("id")).unwrap(); - - el.unchecked_into() - } else { - if !is_meta_tag() { - crate::warn!( - "element with id {id} not found, ignoring it for \ - hydration", - ); - } - - crate::document().create_element(&name).unwrap() - } - #[cfg(not(feature = "hydrate"))] - unreachable!() - } else { - crate::document().create_element(&name).unwrap() - }; - - Self { - name, - #[cfg(all(target_arch = "wasm32", feature = "web"))] - element: element.unchecked_into(), - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - id, - } - } -} - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -impl std::ops::Deref for Custom { - type Target = web_sys::HtmlElement; - - #[inline(always)] - fn deref(&self) -> &Self::Target { - &self.element - } -} - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -impl std::convert::AsRef for Custom { - #[inline(always)] - fn as_ref(&self) -> &web_sys::HtmlElement { - &self.element - } -} - -impl ElementDescriptor for Custom { - fn name(&self) -> Oco<'static, str> { - self.name.clone() - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - #[inline(always)] - fn hydration_id(&self) -> &Option { - &self.id - } -} - -cfg_if! { - if #[cfg(all(target_arch = "wasm32", feature = "web"))] { - /// Represents an HTML element. - ///### Beginner's tip: - /// `HtmlElement` implements [`std::ops::Deref`] where `El` implements [`std::ops::Deref`]. - /// When `El` has a corresponding [`web_sys::HtmlElement`] -> `El` will implement [`std::ops::Deref`] for it. - /// For instance [`leptos::HtmlElement
`] implements [`std::ops::Deref`] for [`web_sys::HtmlDivElement`] - /// Because of [Deref Coercion](https://doc.rust-lang.org/std/ops/trait.Deref.html#deref-coercion) you can call applicable [`web_sys::HtmlElement`] methods on `HtmlElement` as if it were that type. - /// If both `HtmlElement` and one of its derefs have a method with the same name, the dot syntax will call the method on the inherent impl (i.e. `HtmlElement` or it's [`std::ops::Deref`] Target). - /// You may need to manually deref to access other methods, for example, `(*el).style()` to get the `CssStyleDeclaration` instead of calling [`leptos::HtmlElement::style`]. - - #[must_use = "You are creating an HtmlElement<_> but not using it. An unused view can \ - cause your view to be rendered as () unexpectedly, and it can \ - also cause issues with client-side hydration."] - #[derive(Clone)] - pub struct HtmlElement { - #[cfg(debug_assertions)] - pub(crate) span: ::tracing::Span, - pub(crate) element: El, - #[cfg(debug_assertions)] - pub(crate) view_marker: Option - } - // Server needs to build a virtualized DOM tree - } else { - /// Represents an HTML element. - #[must_use = "You are creating an HtmlElement<_> but not using it. An unused view can \ - cause your view to be rendered as () unexpectedly, and it can \ - also cause issues with client-side hydration."] - #[derive(Clone)] - pub struct HtmlElement { - pub(crate) element: El, - pub(crate) attrs: SmallVec<[(Oco<'static, str>, Oco<'static, str>); 4]>, - pub(crate) children: ElementChildren, - #[cfg(debug_assertions)] - pub(crate) view_marker: Option - } - - // debug without `children` field - impl fmt::Debug for HtmlElement { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut builder = f.debug_struct("HtmlElement"); - builder.field("element", &self.element); - builder.field("attrs", &self.attrs); - #[cfg(debug_assertions)] - builder.field("view_marker", &self.view_marker); - builder.finish() - } - } - - #[derive(Clone, Default, PartialEq, Eq)] - pub(crate) enum ElementChildren { - #[default] - Empty, - Children(Vec), - InnerHtml(Oco<'static, str>), - Chunks(Vec) - } - - #[doc(hidden)] - #[derive(Clone)] - pub enum StringOrView { - String(Oco<'static, str>), - View(std::rc::Rc View>) - } - - impl PartialEq for StringOrView { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (StringOrView::String(a), StringOrView::String(b)) => a == b, - _ => false - } - } - } - - impl Eq for StringOrView {} - } -} - -impl std::ops::Deref for HtmlElement -where - El: ElementDescriptor + std::ops::Deref, -{ - type Target = ::Target; - - fn deref(&self) -> &Self::Target { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - return self.element.deref(); - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}"); - } -} - -/// Bind data through attributes, or behavior through event handlers, to an element. -/// A value of any type able to provide an iterator of bindings (like a: `Vec`), -/// can be spread onto an element using the spread syntax `view! {
}`. -pub enum Binding { - /// A statically named attribute. - Attribute { - /// Name of the attribute. - name: &'static str, - /// Value of the attribute, possibly reactive. - value: Attribute, - }, - /// A statically typed event handler. - EventHandler(EventHandlerFn), -} - -impl From<(&'static str, Attribute)> for Binding { - fn from((name, value): (&'static str, Attribute)) -> Self { - Self::Attribute { name, value } - } -} - -impl From for Binding { - fn from(handler: EventHandlerFn) -> Self { - Self::EventHandler(handler) - } -} - -impl HtmlElement { - pub(crate) fn new(element: El) -> Self { - cfg_if! { - if #[cfg(all(target_arch = "wasm32", feature = "web"))] { - Self { - element, - #[cfg(debug_assertions)] - span: ::tracing::Span::current(), - #[cfg(debug_assertions)] - view_marker: None - } - } else { - Self { - attrs: smallvec![], - children: Default::default(), - element, - #[cfg(debug_assertions)] - view_marker: None - } - } - } - } - - #[doc(hidden)] - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - pub fn from_chunks( - element: El, - chunks: impl IntoIterator, - ) -> Self { - Self { - attrs: smallvec![], - children: ElementChildren::Chunks(chunks.into_iter().collect()), - element, - #[cfg(debug_assertions)] - view_marker: None, - } - } - - #[cfg(debug_assertions)] - /// Adds an optional marker indicating the view macro source. - #[inline(always)] - pub fn with_view_marker(mut self, marker: impl Into) -> Self { - self.view_marker = Some(marker.into()); - self - } - - /// Converts this element into [`HtmlElement`]. - pub fn into_any(self) -> HtmlElement { - cfg_if! { - if #[cfg(all(target_arch = "wasm32", feature = "web"))] { - let Self { - element, - #[cfg(debug_assertions)] - span, - #[cfg(debug_assertions)] - view_marker - } = self; - - HtmlElement { - element: AnyElement { - name: element.name(), - element: element.as_ref().clone(), - is_void: element.is_void(), - }, - #[cfg(debug_assertions)] - span, - #[cfg(debug_assertions)] - view_marker - } - } else { - let Self { - - attrs, - children, - element, - #[cfg(debug_assertions)] - view_marker - } = self; - - HtmlElement { - - attrs, - children, - element: AnyElement { - name: element.name(), - is_void: element.is_void(), - id: *element.hydration_id() - }, - #[cfg(debug_assertions)] - view_marker - } - } - } - } - - /// Adds an `id` to the element. - #[track_caller] - #[inline(always)] - pub fn id(self, id: impl Into>) -> Self { - let id = id.into(); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - #[inline(never)] - fn id_inner(el: &web_sys::HtmlElement, id: &str) { - el.set_attribute(wasm_bindgen::intern("id"), id).unwrap() - } - - id_inner(self.element.as_ref(), &id); - - self - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - let mut this = self; - - this.attrs.push(("id".into(), id)); - - this - } - } - - /// Binds the element reference to [`NodeRef`]. - #[inline(always)] - pub fn node_ref(self, node_ref: NodeRef) -> Self - where - Self: Clone, - { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - node_ref.load(&self); - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - let _ = node_ref; - - self - } - - /// Runs the callback when this element has been mounted to the DOM. - /// - /// ### Important Note - /// This method will only ever run at most once. If this element - /// is unmounted and remounted, or moved somewhere else, it will not - /// re-run unless you call this method again. - pub fn on_mount(self, f: impl FnOnce(Self) + 'static) -> Self { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - use futures::future::poll_fn; - use once_cell::unsync::OnceCell; - use std::{ - cell::RefCell, - task::{Poll, Waker}, - }; - - let this = self.clone(); - let el = self.element.as_ref().clone(); - - wasm_bindgen_futures::spawn_local(async move { - while !crate::document().body().unwrap().contains(Some(&el)) { - // We need to cook ourselves a small future that resolves - // when the next animation frame is available - let waker = Rc::new(RefCell::new(None::)); - let ready = Rc::new(OnceCell::new()); - - crate::helpers::request_animation_frame({ - let waker = waker.clone(); - let ready = ready.clone(); - - move || { - let _ = ready.set(()); - if let Some(waker) = &*waker.borrow() { - waker.wake_by_ref(); - } - } - }); - - // Wait for the animation frame to become available - poll_fn(move |cx| { - let mut waker_borrow = waker.borrow_mut(); - - *waker_borrow = Some(cx.waker().clone()); - - if ready.get().is_some() { - Poll::Ready(()) - } else { - Poll::<()>::Pending - } - }) - .await; - } - - f(this); - }); - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - let _ = f; - } - self - } - - /// Checks to see if this element is mounted to the DOM as a child - /// of `body`. - /// - /// This method will always return `false` on non-wasm CSR targets. - #[inline(always)] - pub fn is_mounted(&self) -> bool { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - #[inline(never)] - fn is_mounted_inner(el: &web_sys::HtmlElement) -> bool { - crate::document().body().unwrap().contains(Some(el)) - } - - is_mounted_inner(self.element.as_ref()) - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - false - } - - /// Adds an attribute to this element. - #[track_caller] - #[cfg_attr(all(target_arch = "wasm32", feature = "web"), inline(always))] - pub fn attr( - self, - name: impl Into>, - attr: impl IntoAttribute, - ) -> Self { - #[cfg(all(debug_assertions, feature = "ssr"))] - { - if matches!(self.children, ElementChildren::Chunks(_)) { - let location = std::panic::Location::caller(); - crate::warn!( - "\n\nWARNING: At {location}, you call .attr() on an \ - HtmlElement<_> that was created with the `view!` macro. \ - The macro applies optimizations during SSR that prevent \ - calling this method successfully. You should not mix the \ - `view` macro and the builder syntax when using SSR.\n\n", - ); - } - } - - let name = name.into(); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - attribute_helper( - self.element.as_ref(), - name, - attr.into_attribute(), - ); - self - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - let mut this = self; - - let mut attr = attr.into_attribute(); - while let Attribute::Fn(f) = attr { - attr = f(); - } - match attr { - Attribute::String(value) => { - this.attrs.push((name, value)); - } - Attribute::Bool(include) => { - if include { - this.attrs.push((name, "".into())); - } - } - Attribute::Option(maybe) => { - if let Some(value) = maybe { - this.attrs.push((name, value)); - } - } - _ => unreachable!(), - } - - this - } - } - - /// Adds multiple attributes to the element. - #[track_caller] - pub fn attrs( - mut self, - attrs: impl std::iter::IntoIterator, - ) -> Self { - for (name, value) in attrs { - self = self.attr(name, value); - } - self - } - - /// Adds multiple bindings (attributes or event handlers) to the element. - #[track_caller] - pub fn bindings>( - mut self, - bindings: impl std::iter::IntoIterator, - ) -> Self { - for binding in bindings { - self = self.binding(binding.into()); - } - self - } - - /// Add a single binding (attribute or event handler) to the element. - #[track_caller] - fn binding(self, binding: Binding) -> Self { - match binding { - Binding::Attribute { name, value } => self.attr(name, value), - Binding::EventHandler(handler) => match handler { - EventHandlerFn::Keydown(handler) => { - self.on(crate::events::typed::keydown, handler) - } - EventHandlerFn::Keyup(handler) => { - self.on(crate::events::typed::keyup, handler) - } - EventHandlerFn::Keypress(handler) => { - self.on(crate::events::typed::keypress, handler) - } - EventHandlerFn::Click(handler) => { - self.on(crate::events::typed::click, handler) - } - EventHandlerFn::Dblclick(handler) => { - self.on(crate::events::typed::dblclick, handler) - } - EventHandlerFn::Mousedown(handler) => { - self.on(crate::events::typed::mousedown, handler) - } - EventHandlerFn::Mouseup(handler) => { - self.on(crate::events::typed::mouseup, handler) - } - EventHandlerFn::Mouseenter(handler) => { - self.on(crate::events::typed::mouseenter, handler) - } - EventHandlerFn::Mouseleave(handler) => { - self.on(crate::events::typed::mouseleave, handler) - } - EventHandlerFn::Mouseout(handler) => { - self.on(crate::events::typed::mouseout, handler) - } - EventHandlerFn::Mouseover(handler) => { - self.on(crate::events::typed::mouseover, handler) - } - EventHandlerFn::Mousemove(handler) => { - self.on(crate::events::typed::mousemove, handler) - } - EventHandlerFn::Wheel(handler) => { - self.on(crate::events::typed::wheel, handler) - } - EventHandlerFn::Touchstart(handler) => { - self.on(crate::events::typed::touchstart, handler) - } - EventHandlerFn::Touchend(handler) => { - self.on(crate::events::typed::touchend, handler) - } - EventHandlerFn::Touchcancel(handler) => { - self.on(crate::events::typed::touchcancel, handler) - } - EventHandlerFn::Touchmove(handler) => { - self.on(crate::events::typed::touchmove, handler) - } - EventHandlerFn::Pointerenter(handler) => { - self.on(crate::events::typed::pointerenter, handler) - } - EventHandlerFn::Pointerleave(handler) => { - self.on(crate::events::typed::pointerleave, handler) - } - EventHandlerFn::Pointerdown(handler) => { - self.on(crate::events::typed::pointerdown, handler) - } - EventHandlerFn::Pointerup(handler) => { - self.on(crate::events::typed::pointerup, handler) - } - EventHandlerFn::Pointercancel(handler) => { - self.on(crate::events::typed::pointercancel, handler) - } - EventHandlerFn::Pointerout(handler) => { - self.on(crate::events::typed::pointerout, handler) - } - EventHandlerFn::Pointerover(handler) => { - self.on(crate::events::typed::pointerover, handler) - } - EventHandlerFn::Pointermove(handler) => { - self.on(crate::events::typed::pointermove, handler) - } - EventHandlerFn::Drag(handler) => { - self.on(crate::events::typed::drag, handler) - } - EventHandlerFn::Dragend(handler) => { - self.on(crate::events::typed::dragend, handler) - } - EventHandlerFn::Dragenter(handler) => { - self.on(crate::events::typed::dragenter, handler) - } - EventHandlerFn::Dragleave(handler) => { - self.on(crate::events::typed::dragleave, handler) - } - EventHandlerFn::Dragstart(handler) => { - self.on(crate::events::typed::dragstart, handler) - } - EventHandlerFn::Drop(handler) => { - self.on(crate::events::typed::drop, handler) - } - EventHandlerFn::Blur(handler) => { - self.on(crate::events::typed::blur, handler) - } - EventHandlerFn::Focusout(handler) => { - self.on(crate::events::typed::focusout, handler) - } - EventHandlerFn::Focus(handler) => { - self.on(crate::events::typed::focus, handler) - } - EventHandlerFn::Focusin(handler) => { - self.on(crate::events::typed::focusin, handler) - } - }, - } - } - - /// Adds a class to an element. - /// - /// **Note**: In the builder syntax, this will be overwritten by the `class` - /// attribute if you use `.attr("class", /* */)`. In the `view` macro, they - /// are automatically re-ordered so that this over-writing does not happen. - /// - /// # Panics - /// This directly uses the browser’s `classList` API, which means it will throw - /// a runtime error if you pass more than a single class name. If you want to - /// pass more than one class name at a time, you can use [HtmlElement::classes]. - #[track_caller] - pub fn class( - self, - name: impl Into>, - class: impl IntoClass, - ) -> Self { - #[cfg(all(debug_assertions, feature = "ssr"))] - { - if matches!(self.children, ElementChildren::Chunks(_)) { - let location = std::panic::Location::caller(); - crate::warn!( - "\n\nWARNING: At {location}, you call .class() on an \ - HtmlElement<_> that was created with the `view!` macro. \ - The macro applies optimizations during SSR that prevent \ - calling this method successfully. You should not mix the \ - `view` macro and the builder syntax when using SSR.\n\n", - ); - } - } - - let name = name.into(); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - let el = self.element.as_ref(); - let value = class.into_class(); - class_helper(el, name, value); - - self - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - use crate::macro_helpers::Class; - - let mut this = self; - - let class = class.into_class(); - - let include = match class { - Class::Value(include) => include, - Class::Fn(f) => f(), - }; - - if include { - if let Some((_, ref mut value)) = - this.attrs.iter_mut().find(|(name, _)| name == "class") - { - *value = format!("{value} {name}").into(); - } else { - this.attrs.push(("class".into(), name)); - } - } - - this - } - } - - fn classes_inner(self, classes: &str) -> Self { - let mut this = self; - for class in classes.split_ascii_whitespace() { - this = this.class(class.to_string(), true); - } - this - } - - /// Adds a list of classes separated by ASCII whitespace to an element. - #[track_caller] - #[inline(always)] - pub fn classes(self, classes: impl Into>) -> Self { - #[cfg(all(debug_assertions, feature = "ssr"))] - { - if matches!(self.children, ElementChildren::Chunks(_)) { - let location = std::panic::Location::caller(); - crate::warn!( - "\n\nWARNING: At {location}, you call .classes() on an \ - HtmlElement<_> that was created with the `view!` macro. \ - The macro applies optimizations during SSR that prevent \ - calling this method successfully. You should not mix the \ - `view` macro and the builder syntax when using SSR.\n\n", - ); - } - } - - self.classes_inner(&classes.into()) - } - - /// Sets the class on the element as the class signal changes. - #[track_caller] - pub fn dyn_classes( - self, - classes_signal: impl Fn() -> I + 'static, - ) -> Self - where - I: IntoIterator, - C: Into>, - { - #[cfg(all(debug_assertions, feature = "ssr"))] - { - if matches!(self.children, ElementChildren::Chunks(_)) { - let location = std::panic::Location::caller(); - crate::warn!( - "\n\nWARNING: At {location}, you call .dyn_classes() on \ - an HtmlElement<_> that was created with the `view!` \ - macro. The macro applies optimizations during SSR that \ - prevent calling this method successfully. You should not \ - mix the `view` macro and the builder syntax when using \ - SSR.\n\n", - ); - } - } - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - use smallvec::SmallVec; - - let class_list = self.element.as_ref().class_list(); - - leptos_reactive::create_render_effect( - move |prev_classes: Option< - SmallVec<[Oco<'static, str>; 4]>, - >| { - let classes = classes_signal() - .into_iter() - .map(Into::into) - .collect::; 4]>>( - ); - - let new_classes = classes - .iter() - .flat_map(|classes| classes.split_whitespace()); - - if let Some(prev_classes) = prev_classes { - let new_classes = - new_classes.collect::>(); - let mut old_classes = prev_classes - .iter() - .flat_map(|classes| classes.split_whitespace()); - - // Remove old classes - for prev_class in old_classes.clone() { - if !new_classes.iter().any(|c| c == &prev_class) { - class_list.remove_1(prev_class).unwrap_or_else( - |err| { - panic!( - "failed to remove class \ - `{prev_class}`, error: {err:#?}" - ) - }, - ); - } - } - - // Add new classes - for class in new_classes { - if !old_classes.any(|c| c == class) { - class_list.add_1(class).unwrap_or_else(|err| { - panic!( - "failed to add class `{class}`, \ - error: {err:#?}" - ) - }); - } - } - } else { - let new_classes = new_classes - .map(ToOwned::to_owned) - .collect::>(); - - for class in &new_classes { - class_list.add_1(class).unwrap_or_else(|err| { - panic!( - "failed to add class `{class}`, error: \ - {err:#?}" - ) - }); - } - } - - classes - }, - ); - - self - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - classes_signal() - .into_iter() - .map(Into::into) - .flat_map(|classes| { - classes - .split_whitespace() - .map(ToString::to_string) - .collect::>() - }) - .fold(self, |this, class| this.class(class, true)) - } - } - - /// Sets a style on an element. - /// - /// **Note**: In the builder syntax, this will be overwritten by the `style` - /// attribute if you use `.attr("style", /* */)`. In the `view` macro, they - /// are automatically re-ordered so that this over-writing does not happen. - #[track_caller] - pub fn style( - self, - name: impl Into>, - style: impl IntoStyle, - ) -> Self { - #[cfg(all(debug_assertions, feature = "ssr"))] - { - if matches!(self.children, ElementChildren::Chunks(_)) { - let location = std::panic::Location::caller(); - crate::warn!( - "\n\nWARNING: At {location}, you call .style() on an \ - HtmlElement<_> that was created with the `view!` macro. \ - The macro applies optimizations during SSR that prevent \ - calling this method successfully. You should not mix the \ - `view` macro and the builder syntax when using SSR.\n\n", - ); - } - } - - let name = name.into(); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - let el = self.element.as_ref(); - let value = style.into_style(); - style_helper(el, name, value); - - self - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - use crate::macro_helpers::Style; - - let mut this = self; - - let style = style.into_style(); - - let include = match style { - Style::Value(value) => Some(value), - Style::Option(value) => value, - Style::Fn(f) => { - let mut value = f(); - while let Style::Fn(f) = value { - value = f(); - } - match value { - Style::Value(value) => Some(value), - Style::Option(value) => value, - _ => unreachable!(), - } - } - }; - - if let Some(style_value) = include { - if let Some((_, ref mut value)) = - this.attrs.iter_mut().find(|(name, _)| name == "style") - { - *value = format!("{value} {name}: {style_value};").into(); - } else { - this.attrs.push(( - "style".into(), - format!("{name}: {style_value};").into(), - )); - } - } - - this - } - } - - /// Sets a property on an element. - #[track_caller] - pub fn prop( - self, - name: impl Into>, - value: impl IntoProperty, - ) -> Self { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - let name = name.into(); - let value = value.into_property(); - let el = self.element.as_ref(); - property_helper(el, name, value); - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - let _ = name; - let _ = value; - } - - self - } - - /// Adds an event listener to this element. - #[track_caller] - #[inline(always)] - pub fn on( - self, - event: E, - #[allow(unused_mut)] // used for tracing in debug - mut event_handler: impl FnMut(E::EventType) + 'static, - ) -> Self { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - cfg_if! { - if #[cfg(debug_assertions)] { - let onspan = ::tracing::span!( - parent: &self.span, - ::tracing::Level::TRACE, - "on", - event = %event.name() - ); - let _onguard = onspan.enter(); - } - } - let event_name = event.name(); - - let key = event.event_delegation_key(); - let event_handler = Box::new(event_handler); - - if E::BUBBLES { - add_event_listener( - self.element.as_ref(), - key, - event_name, - event_handler, - event.options(), - ); - } else { - add_event_listener_undelegated( - self.element.as_ref(), - &event_name, - event_handler, - event.options(), - ); - } - - self - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - _ = event; - _ = event_handler; - - self - } - } - - /// Optionally adds an event listener to this element. - /// - /// ## Example - /// ```rust - /// # use leptos::*; - /// #[component] - /// pub fn Input( - /// #[prop(optional)] value: Option>, - /// ) -> impl IntoView { - /// view! { } - /// // only add event if `value` is `Some(signal)` - /// .optional_event( - /// ev::input, - /// value.map(|value| move |ev| value.set(event_target_value(&ev))), - /// ) - /// } - /// # - /// ``` - #[track_caller] - #[inline(always)] - pub fn optional_event( - self, - event: E, - #[allow(unused_mut)] // used for tracing in debug - mut event_handler: Option, - ) -> Self { - if let Some(event_handler) = event_handler { - self.on(event, event_handler) - } else { - self - } - } - - /// Adds a child to this element. - #[track_caller] - pub fn child(self, child: impl IntoView) -> Self { - let child = child.into_view(); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - if !HydrationCtx::is_hydrating() { - // add a debug-only, run-time warning for the SVG element - #[cfg(debug_assertions)] - warn_on_ambiguous_a(self.element.as_ref(), &child); - - mount_child(MountKind::Append(self.element.as_ref()), &child); - } - - self - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - let mut this = self; - let children = &mut this.children; - - match children { - ElementChildren::Empty => { - *children = ElementChildren::Children(vec![child]); - } - ElementChildren::Children(ref mut children) => { - children.push(child); - } - ElementChildren::InnerHtml(_) => { - #[cfg(debug_assertions)] - { - let location = std::panic::Location::caller(); - crate::debug_warn!( - "At {location}, you call .child() on an HTML \ - element that already had inner_html provided. \ - This will have no effect." - ); - } - } - ElementChildren::Chunks(_) => { - #[cfg(debug_assertions)] - { - let location = std::panic::Location::caller(); - crate::debug_warn!( - "\n\nWARNING: At {location}, you call .child() on \ - an HtmlElement<_> that was created with the \ - `view!` macro. The macro applies optimizations \ - during SSR that prevent calling this method \ - successfully. You should not mix the `view` \ - macro and the builder syntax when using SSR.\n\n" - ); - } - } - } - - this - } - } - - /// Sets the inner HTML of this element from the provided - /// string slice. - /// - /// # Security - /// Be very careful when using this method. Always remember to - /// sanitize the input to avoid a cross-site scripting (XSS) - /// vulnerability. - #[inline(always)] - pub fn inner_html(self, html: impl Into>) -> Self { - let html = html.into(); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - self.element.as_ref().set_inner_html(&html); - - self - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - let mut this = self; - - this.children = ElementChildren::InnerHtml(html); - - this - } - } -} - -impl HtmlElement { - /// Bind the directive to the element. - #[inline(always)] - pub fn directive( - self, - handler: impl Directive + 'static, - param: P, - ) -> Self { - let node_ref = create_node_ref::(); - - let handler = Rc::new(handler); - - let _ = create_effect(move |_| { - if let Some(el) = node_ref.get() { - Rc::clone(&handler).run(el.into_any(), param.clone()); - } - }); - - self.node_ref(node_ref) - } -} - -impl IntoView for HtmlElement { - #[cfg_attr(any(debug_assertions, feature = "ssr"), instrument(level = "trace", name = "", skip_all, fields(tag = %self.element.name())))] - #[cfg_attr(all(target_arch = "wasm32", feature = "web"), inline(always))] - fn into_view(self) -> View { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - View::Element(Element::new(self.element)) - } - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - { - let Self { - element, - mut attrs, - children, - #[cfg(debug_assertions)] - view_marker, - .. - } = self; - - let id = *element.hydration_id(); - - let mut element = Element::new(element); - - if let Some(id) = id { - attrs.push(("data-hk".into(), id.to_string().into())); - } - - element.attrs = attrs; - element.children = children; - - #[cfg(debug_assertions)] - { - element.view_marker = view_marker; - } - - View::Element(element) - } - } -} - -impl IntoView for [HtmlElement; N] { - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", name = "[HtmlElement; N]", skip_all) - )] - fn into_view(self) -> View { - Fragment::new(self.into_iter().map(|el| el.into_view()).collect()) - .into_view() - } -} - -/// Creates any custom element, such as ``. -pub fn custom(el: El) -> HtmlElement { - HtmlElement::new(Custom { - name: el.name(), - #[cfg(all(target_arch = "wasm32", feature = "web"))] - element: el.as_ref().clone(), - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - id: *el.hydration_id(), - }) -} - -/// Creates a text node. -#[inline(always)] -pub fn text(text: impl Into>) -> Text { - Text::new(text.into()) -} - -macro_rules! generate_html_tags { - ($( - #[$meta:meta] - $(#[$void:ident])? - $tag:ident $([$trailing_:pat])? $el_type:ident - ),* $(,)?) => { - paste::paste! { - $( - #[cfg(all(target_arch = "wasm32", feature = "web"))] - thread_local! { - static [<$tag:upper>]: LazyCell = LazyCell::new(|| { - crate::document() - .create_element(stringify!($tag)) - .unwrap() - .unchecked_into() - }); - } - - #[derive(Clone, Debug)] - #[$meta] - pub struct [<$tag:camel $($trailing_)?>] { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - element: web_sys::HtmlElement, - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - id: Option, - } - - impl Default for [<$tag:camel $($trailing_)?>] { - fn default() -> Self { - let id = HydrationCtx::id(); - - #[cfg(all(target_arch = "wasm32", feature = "web"))] - let element = create_leptos_element( - &stringify!([<$tag:upper>]), - id, - || { - [<$tag:upper>] - .with(|el| - el.clone_node() - .unwrap() - .unchecked_into() - ) - } - ); - - Self { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - element, - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - id - } - } - } - - impl std::ops::Deref for [<$tag:camel $($trailing_)?>] { - type Target = web_sys::$el_type; - - #[inline(always)] - fn deref(&self) -> &Self::Target { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - { - use wasm_bindgen::JsCast; - return &self.element.unchecked_ref(); - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}"); - } - } - - impl std::convert::AsRef for [<$tag:camel $($trailing_)?>] { - #[inline(always)] - fn as_ref(&self) -> &web_sys::HtmlElement { - #[cfg(all(target_arch = "wasm32", feature = "web"))] - return &self.element; - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}"); - } - } - - impl ElementDescriptor for [<$tag:camel $($trailing_)?>] { - #[inline(always)] - fn name(&self) -> Oco<'static, str> { - stringify!($tag).into() - } - - #[cfg(not(all(target_arch = "wasm32", feature = "web")))] - #[inline(always)] - fn hydration_id(&self) -> &Option { - &self.id - } - - generate_html_tags! { @void $($void)? } - } - - impl From]>> for HtmlElement { - fn from(element: HtmlElement<[<$tag:camel $($trailing_)?>]>) -> Self { - element.into_any() - } - } - - #[$meta] - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument( - level = "trace", - name = "HtmlElement", - skip_all, - fields( - tag = %format!("<{}/>", stringify!($tag)) - ) - ) - )] - pub fn $tag() -> HtmlElement<[<$tag:camel $($trailing_)?>]> { - HtmlElement::new( [<$tag:camel $($trailing_)?>]::default()) - } - )* - } - }; - (@void) => {}; - (@void void) => { - #[inline(always)] - fn is_void(&self) -> bool { - true - } - } -} - -#[cfg(all(target_arch = "wasm32", feature = "web"))] -fn create_leptos_element( - #[allow(unused)] tag: &str, - #[allow(unused)] id: Option, - clone_element: fn() -> web_sys::HtmlElement, -) -> web_sys::HtmlElement { - #[cfg(not(debug_assertions))] - { - _ = tag; - } - - #[cfg(feature = "hydrate")] - if HydrationCtx::is_hydrating() && id.is_some() { - let id = id.unwrap(); - if let Some(el) = crate::hydration::get_element(&id.to_string()) { - #[cfg(debug_assertions)] - assert_eq!( - &el.node_name().to_ascii_uppercase(), - tag, - "SSR and CSR elements have the same hydration key but \ - different node kinds. Check out the docs for information \ - about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html" - ); - - el.unchecked_into() - } else { - if !is_meta_tag() { - crate::warn!( - "element with id {id} not found, ignoring it for hydration" - ); - } - - clone_element() - } - } else { - clone_element() - } - #[cfg(not(feature = "hydrate"))] - { - clone_element() - } -} - -#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))] -fn warn_on_ambiguous_a(parent: &web_sys::Element, child: &View) { - if let View::Element(el) = &child { - if (el.name == "a" - || el.name == "script" - || el.name == "style" - || el.name == "title") - && parent.namespace_uri() != el.element.namespace_uri() - { - crate::warn!( - "Warning: you are appending an SVG element to an HTML \ - element, or an HTML element to an SVG. Typically, this \ - occurs when you create an or -/// // leptos_meta -///

"Test"

-/// } -/// } -/// ``` -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Nonce(pub(crate) String); - -impl Deref for Nonce { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for Nonce { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl IntoAttribute for Nonce { - fn into_attribute(self) -> Attribute { - Attribute::String(self.0.into()) - } - - fn into_attribute_boxed(self: Box) -> Attribute { - Attribute::String(self.0.into()) - } -} - -/// Accesses the nonce that has been generated during the current -/// server response. This can be added to inline ` -/// // leptos_meta -///

"Test"

-/// } -/// } -/// ``` -pub fn use_nonce() -> Option { - use_context::() -} - -#[cfg(all(feature = "ssr", feature = "nonce"))] -pub use generate::*; - -#[cfg(all(feature = "ssr", feature = "nonce"))] -mod generate { - use super::Nonce; - use base64::{ - alphabet, - engine::{self, general_purpose}, - Engine, - }; - use leptos_reactive::provide_context; - use rand::{thread_rng, RngCore}; - - const NONCE_ENGINE: engine::GeneralPurpose = engine::GeneralPurpose::new( - &alphabet::URL_SAFE, - general_purpose::NO_PAD, - ); - - impl Nonce { - /// Generates a new nonce from 16 bytes (128 bits) of random data. - pub fn new() -> Self { - let mut thread_rng = thread_rng(); - let mut bytes = [0; 16]; - thread_rng.fill_bytes(&mut bytes); - Nonce(NONCE_ENGINE.encode(bytes)) - } - } - - impl Default for Nonce { - fn default() -> Self { - Self::new() - } - } - - /// Generates a nonce and provides it during server rendering. - pub fn provide_nonce() { - provide_context(Nonce::new()) - } -} diff --git a/leptos_dom/src/ssr.rs b/leptos_dom/src/ssr.rs deleted file mode 100644 index 262dac6e4..000000000 --- a/leptos_dom/src/ssr.rs +++ /dev/null @@ -1,789 +0,0 @@ -#![cfg(not(all(target_arch = "wasm32", feature = "web")))] - -//! Server-side HTML rendering utilities. - -use crate::{ - html::{ElementChildren, StringOrView}, - CoreComponent, HydrationCtx, HydrationKey, IntoView, View, -}; -use cfg_if::cfg_if; -use futures::{stream::FuturesUnordered, Future, Stream, StreamExt}; -use itertools::Itertools; -use leptos_reactive::*; -use std::pin::Pin; - -type PinnedFuture = Pin>>; - -/// Renders the given function to a static HTML string. -/// -/// ``` -/// # cfg_if::cfg_if! { if #[cfg(not(any(feature = "csr", feature = "hydrate")))] { -/// # use leptos::*; -/// let html = leptos::ssr::render_to_string(|| view! { -///

"Hello, world!"

-/// }); -/// // trim off the beginning, which has a bunch of hydration info, for comparison -/// assert!(html.contains("Hello, world!

")); -/// # }} -/// ``` -#[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", skip_all,) -)] -pub fn render_to_string(f: F) -> Oco<'static, str> -where - F: FnOnce() -> N + 'static, - N: IntoView, -{ - HydrationCtx::reset_id(); - let runtime = leptos_reactive::create_runtime(); - - let html = f().into_view().render_to_string(); - - runtime.dispose(); - - html -} - -/// Renders a function to a stream of HTML strings. -/// -/// This renders: -/// 1) the application shell -/// a) HTML for everything that is not under a ``, -/// b) the `fallback` for any `` component that is not already resolved, and -/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data. -/// 2) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the -/// server and are sent down to the browser to resolve. On the browser, if the app sees that -/// it is waiting for a resource to resolve from the server, it doesn't run it initially. -/// 3) HTML fragments to replace each `` fallback with its actual data as the resources -/// read under that `` resolve. -#[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", skip_all,) -)] -pub fn render_to_stream( - view: impl FnOnce() -> View + 'static, -) -> impl Stream { - render_to_stream_with_prefix(view, || "".into()) -} - -/// Renders a function to a stream of HTML strings. After the `view` runs, the `prefix` will run with -/// the same scope. This can be used to generate additional HTML that has access to the same reactive graph. -/// -/// This renders: -/// 1) the prefix -/// 2) the application shell -/// a) HTML for everything that is not under a ``, -/// b) the `fallback` for any `` component that is not already resolved, and -/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data. -/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the -/// server and are sent down to the browser to resolve. On the browser, if the app sees that -/// it is waiting for a resource to resolve from the server, it doesn't run it initially. -/// 4) HTML fragments to replace each `` fallback with its actual data as the resources -/// read under that `` resolve. -#[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", skip_all,) -)] -pub fn render_to_stream_with_prefix( - view: impl FnOnce() -> View + 'static, - prefix: impl FnOnce() -> Oco<'static, str> + 'static, -) -> impl Stream { - let (stream, runtime) = - render_to_stream_with_prefix_undisposed(view, prefix); - runtime.dispose(); - stream -} - -/// Renders a function to a stream of HTML strings and returns the [RuntimeId] that was created, so -/// it can be disposed when appropriate. After the `view` runs, the `prefix` will run with -/// the same scope. This can be used to generate additional HTML that has access to the same reactive graph. -/// -/// This renders: -/// 1) the prefix -/// 2) the application shell -/// a) HTML for everything that is not under a ``, -/// b) the `fallback` for any `` component that is not already resolved, and -/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data. -/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the -/// server and are sent down to the browser to resolve. On the browser, if the app sees that -/// it is waiting for a resource to resolve from the server, it doesn't run it initially. -/// 4) HTML fragments to replace each `` fallback with its actual data as the resources -/// read under that `` resolve. -#[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", skip_all,) -)] -pub fn render_to_stream_with_prefix_undisposed( - view: impl FnOnce() -> View + 'static, - prefix: impl FnOnce() -> Oco<'static, str> + 'static, -) -> (impl Stream, RuntimeId) { - render_to_stream_with_prefix_undisposed_with_context(view, prefix, || {}) -} - -/// Renders a function to a stream of HTML strings and returns the [RuntimeId] that was created, so -/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with -/// the same scope. This can be used to generate additional HTML that has access to the same reactive graph. -/// -/// This renders: -/// 1) the prefix -/// 2) the application shell -/// a) HTML for everything that is not under a ``, -/// b) the `fallback` for any `` component that is not already resolved, and -/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data. -/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the -/// server and are sent down to the browser to resolve. On the browser, if the app sees that -/// it is waiting for a resource to resolve from the server, it doesn't run it initially. -/// 4) HTML fragments to replace each `` fallback with its actual data as the resources -/// read under that `` resolve. -#[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", skip_all,) -)] -pub fn render_to_stream_with_prefix_undisposed_with_context( - view: impl FnOnce() -> View + 'static, - prefix: impl FnOnce() -> Oco<'static, str> + 'static, - additional_context: impl FnOnce() + 'static, -) -> (impl Stream, RuntimeId) { - render_to_stream_with_prefix_undisposed_with_context_and_block_replacement( - view, - prefix, - additional_context, - false, - ) -} - -/// Renders a function to a stream of HTML strings and returns the [RuntimeId] that was created, so -/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with -/// the same scope. This can be used to generate additional HTML that has access to the same reactive graph. -/// -/// If `replace_blocks` is true, this will wait for any fragments with blocking resources and -/// actually replace them in the initial HTML. This is slower to render (as it requires walking -/// back over the HTML for string replacement) but has the advantage of never including those fallbacks -/// in the HTML. -/// -/// This renders: -/// 1) the prefix -/// 2) the application shell -/// a) HTML for everything that is not under a ``, -/// b) the `fallback` for any `` component that is not already resolved, and -/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data. -/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the -/// server and are sent down to the browser to resolve. On the browser, if the app sees that -/// it is waiting for a resource to resolve from the server, it doesn't run it initially. -/// 4) HTML fragments to replace each `` fallback with its actual data as the resources -/// read under that `` resolve. -#[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", skip_all,) -)] -pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacement( - view: impl FnOnce() -> View + 'static, - prefix: impl FnOnce() -> Oco<'static, str> + 'static, - additional_context: impl FnOnce() + 'static, - replace_blocks: bool, -) -> (impl Stream, RuntimeId) { - HydrationCtx::reset_id(); - - // create the runtime - let runtime = create_runtime(); - - // Add additional context items - additional_context(); - - // the actual app body/template code - // this does NOT contain any of the data being loaded asynchronously in resources - let shell = view().render_to_string(); - - let resources = SharedContext::pending_resources(); - let pending_resources = serde_json::to_string(&resources).unwrap(); - let pending_fragments = SharedContext::pending_fragments(); - let serializers = SharedContext::serialization_resolvers(); - let nonce_str = crate::nonce::use_nonce() - .map(|nonce| format!(" nonce=\"{nonce}\"")) - .unwrap_or_default(); - - let local_only = SharedContext::fragments_with_local_resources(); - let local_only = serde_json::to_string(&local_only).unwrap(); - - let mut blocking_fragments = FuturesUnordered::new(); - let fragments = FuturesUnordered::new(); - - for (fragment_id, data) in pending_fragments { - if data.should_block { - blocking_fragments - .push(async move { (fragment_id, data.out_of_order.await) }); - } else { - fragments.push(Box::pin(async move { - (fragment_id, data.out_of_order.await) - }) - as Pin>>); - } - } - - let stream = futures::stream::once( - // HTML for the view function and script to store resources - { - let nonce_str = nonce_str.clone(); - async move { - let resolvers = format!( - "__LEPTOS_PENDING_RESOURCES = \ - {pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \ - Map();__LEPTOS_RESOURCE_RESOLVERS = new \ - Map();__LEPTOS_LOCAL_ONLY = {local_only};" - ); - - if replace_blocks { - let mut blocks = - Vec::with_capacity(blocking_fragments.len()); - while let Some((blocked_id, blocked_fragment)) = - blocking_fragments.next().await - { - blocks.push((blocked_id, blocked_fragment)); - } - - let prefix = prefix(); - - let mut shell = shell; - - for (blocked_id, blocked_fragment) in blocks { - let open = format!(""); - let close = - format!(""); - let (first, rest) = - shell.split_once(&open).unwrap_or_default(); - let (_fallback, rest) = - rest.split_once(&close).unwrap_or_default(); - - shell = - format!("{first}{blocked_fragment}{rest}").into(); - } - - format!("{prefix}{shell}{resolvers}") - } else { - let mut blocking = String::new(); - let mut blocking_fragments = fragments_to_chunks( - nonce_str.clone(), - blocking_fragments, - ); - - while let Some(fragment) = blocking_fragments.next().await { - blocking.push_str(&fragment); - } - let prefix = prefix(); - format!("{prefix}{shell}{resolvers}{blocking}") - } - } - }, - ) - .chain(ooo_body_stream_recurse(nonce_str, fragments, serializers)); - - (stream, runtime) -} - -fn ooo_body_stream_recurse( - nonce_str: String, - fragments: FuturesUnordered>, - serializers: FuturesUnordered>, -) -> Pin>> { - // resources and fragments - // stream HTML for each as it resolves - let fragments = fragments_to_chunks(nonce_str.clone(), fragments); - // stream data for each Resource as it resolves - let resources = render_serializers(nonce_str.clone(), serializers); - - Box::pin( - // TODO these should be combined again in a way that chains them appropriately - // such that individual resources can resolve before all fragments are done - fragments.chain(resources).chain( - futures::stream::once(async move { - let pending = SharedContext::pending_fragments(); - - if !pending.is_empty() { - let fragments = FuturesUnordered::new(); - let serializers = SharedContext::serialization_resolvers(); - for (fragment_id, data) in pending { - fragments.push(Box::pin(async move { - (fragment_id.clone(), data.out_of_order.await) - }) - as Pin>>); - } - Box::pin(ooo_body_stream_recurse( - nonce_str, - fragments, - serializers, - )) - as Pin>> - } else { - Box::pin(futures::stream::once(async move { - Default::default() - })) - } - }) - .flatten(), - ), - ) -} - -#[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", skip_all,) -)] -fn fragments_to_chunks( - nonce_str: String, - fragments: impl Stream, -) -> impl Stream { - fragments.map(move |(fragment_id, html)| { - format!( - r#" - - - (function() {{ let id = "{fragment_id}"; - let open = undefined; - let close = undefined; - let walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT); - while(walker.nextNode()) {{ - if(walker.currentNode.textContent == `suspense-open-${{id}}`) {{ - open = walker.currentNode; - }} else if(walker.currentNode.textContent == `suspense-close-${{id}}`) {{ - close = walker.currentNode; - }} - }} - let range = new Range(); - range.setStartAfter(open); - range.setEndBefore(close); - range.deleteContents(); - let tpl = document.getElementById("{fragment_id}f"); - close.parentNode.insertBefore(tpl.content.cloneNode(true), close);}})() - - "# - ) - }) -} - -impl View { - /// Consumes the node and renders it into an HTML string. - /// - /// This is __NOT__ the same as [`render_to_string`]. This - /// functions differs in that it assumes a runtime is in scope. - /// [`render_to_string`] creates, and disposes of a runtime for you. - /// - /// # Panics - /// When called in a scope without a runtime. Use [`render_to_string`] instead. - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", skip_all,) - )] - pub fn render_to_string(self) -> Oco<'static, str> { - #[cfg(all(feature = "web", feature = "ssr"))] - crate::logging::console_error( - "\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \ - enabled as features, which may cause issues like ` \ - failing to work silently.\n", - ); - - self.render_to_string_helper(false) - } - - #[cfg_attr( - any(debug_assertions, feature = "ssr"), - instrument(level = "trace", skip_all,) - )] - pub(crate) fn render_to_string_helper( - self, - dont_escape_text: bool, - ) -> Oco<'static, str> { - match self { - View::Text(node) => { - if dont_escape_text { - node.content - } else { - html_escape::encode_safe(&node.content).to_string().into() - } - } - View::Component(node) => { - let content = || { - node.children - .into_iter() - .map(|node| { - node.render_to_string_helper(dont_escape_text) - }) - .join("") - }; - cfg_if! { - if #[cfg(debug_assertions)] { - let name = to_kebab_case(&node.name); - let content = format!(r#"{}{}{}"#, - node.id.to_marker(false, &name), - content(), - node.id.to_marker(true, &name), - ); - if let Some(id) = node.view_marker { - format!("{content}").into() - } else { - content.into() - } - } else { - format!( - r#"{}{}"#, - content(), - node.id.to_marker(true) - ).into() - } - } - } - View::Suspense(id, node) => format!( - "{}", - View::CoreComponent(node) - .render_to_string_helper(dont_escape_text) - ) - .into(), - View::CoreComponent(node) => { - let (id, name, wrap, content) = match node { - CoreComponent::Unit(u) => ( - u.id, - "", - false, - Box::new(move || { - u.id.to_marker( - true, - #[cfg(debug_assertions)] - "unit", - ) - }) - as Box Oco<'static, str>>, - ), - CoreComponent::DynChild(node) => { - let child = node.child.take(); - ( - node.id, - "dyn-child", - true, - Box::new(move || { - if let Some(child) = *child { - if let View::Text(t) = child { - // if we don't check if the string is empty, - // the HTML is an empty string; but an empty string - // is not a text node in HTML, so can't be updated - // in the future. so we put a one-space text node instead - let was_empty = t.content.is_empty(); - let content = if was_empty { - " ".into() - } else { - t.content - }; - // escape content unless we're in a "#, - ) - }) -} - -#[doc(hidden)] -pub fn escape_attr(value: &T) -> Oco<'_, str> -where - T: AsRef, -{ - html_escape::encode_double_quoted_attribute(value).into() -} - -pub(crate) trait ToMarker { - fn to_marker( - &self, - closing: bool, - #[cfg(debug_assertions)] component_name: &str, - ) -> Oco<'static, str>; -} - -impl ToMarker for HydrationKey { - #[inline(always)] - fn to_marker( - &self, - closing: bool, - #[cfg(debug_assertions)] mut component_name: &str, - ) -> Oco<'static, str> { - #[cfg(debug_assertions)] - { - if component_name.is_empty() { - // NOTE: - // If the name is left empty, this will lead to invalid comments, - // so a placeholder is used here. - component_name = "<>"; - } - if closing || component_name == "unit" { - format!("").into() - } else { - format!("") - .into() - } - } - #[cfg(not(debug_assertions))] - { - if closing { - format!("").into() - } else { - "".into() - } - } - } -} - -impl ToMarker for Option { - #[inline(always)] - fn to_marker( - &self, - closing: bool, - #[cfg(debug_assertions)] component_name: &str, - ) -> Oco<'static, str> { - self.map(|key| { - key.to_marker( - closing, - #[cfg(debug_assertions)] - component_name, - ) - }) - .unwrap_or("".into()) - } -} diff --git a/leptos_dom/src/ssr_in_order.rs b/leptos_dom/src/ssr_in_order.rs deleted file mode 100644 index ff4cbb96a..000000000 --- a/leptos_dom/src/ssr_in_order.rs +++ /dev/null @@ -1,531 +0,0 @@ -#![cfg(not(all(target_arch = "wasm32", feature = "web")))] - -//! Server-side HTML rendering utilities for in-order streaming and async rendering. - -use crate::{ - html::{ElementChildren, StringOrView}, - ssr::{render_serializers, ToMarker}, - CoreComponent, HydrationCtx, View, -}; -use async_recursion::async_recursion; -use futures::{channel::mpsc::UnboundedSender, Stream, StreamExt}; -use itertools::Itertools; -use leptos_reactive::{ - create_runtime, suspense::StreamChunk, Oco, RuntimeId, SharedContext, -}; -use std::collections::VecDeque; - -/// Renders a view to HTML, waiting to return until all `async` [Resource](leptos_reactive::Resource)s -/// loaded in `` elements have finished loading. -#[tracing::instrument(level = "trace", skip_all)] -pub async fn render_to_string_async( - view: impl FnOnce() -> View + 'static, -) -> String { - let mut buf = String::new(); - let (stream, runtime) = - render_to_stream_in_order_with_prefix_undisposed_with_context( - view, - || "".into(), - || {}, - ); - let mut stream = Box::pin(stream); - while let Some(chunk) = stream.next().await { - buf.push_str(&chunk); - } - runtime.dispose(); - buf -} - -/// Renders an in-order HTML stream, pausing at `` components. The stream contains, -/// in order: -/// 1. HTML from the `view` in order, pausing to wait for each `` -/// 2. any serialized [Resource](leptos_reactive::Resource)s -#[tracing::instrument(level = "trace", skip_all)] -pub fn render_to_stream_in_order( - view: impl FnOnce() -> View + 'static, -) -> impl Stream { - render_to_stream_in_order_with_prefix(view, || "".into()) -} - -/// Renders an in-order HTML stream, pausing at `` components. The stream contains, -/// in order: -/// 1. `prefix` -/// 2. HTML from the `view` in order, pausing to wait for each `` -/// 3. any serialized [Resource](leptos_reactive::Resource)s -/// -/// `additional_context` is injected before the `view` is rendered. The `prefix` is generated -/// after the `view` is rendered, but before `` nodes have resolved. -#[tracing::instrument(level = "trace", skip_all)] -pub fn render_to_stream_in_order_with_prefix( - view: impl FnOnce() -> View + 'static, - prefix: impl FnOnce() -> Oco<'static, str> + 'static, -) -> impl Stream { - #[cfg(all(feature = "web", feature = "ssr"))] - crate::logging::console_error( - "\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \ - enabled as features, which may cause issues like ` \ - failing to work silently.\n", - ); - - let (stream, runtime) = - render_to_stream_in_order_with_prefix_undisposed_with_context( - view, - prefix, - || {}, - ); - runtime.dispose(); - stream -} - -/// Renders an in-order HTML stream, pausing at `` components. The stream contains, -/// in order: -/// 1. `prefix` -/// 2. HTML from the `view` in order, pausing to wait for each `` -/// 3. any serialized [Resource](leptos_reactive::Resource)s -/// -/// `additional_context` is injected before the `view` is rendered. The `prefix` is generated -/// after the `view` is rendered, but before `` nodes have resolved. -#[tracing::instrument(level = "trace", skip_all)] -pub fn render_to_stream_in_order_with_prefix_undisposed_with_context( - view: impl FnOnce() -> View + 'static, - prefix: impl FnOnce() -> Oco<'static, str> + 'static, - additional_context: impl FnOnce() + 'static, -) -> (impl Stream, RuntimeId) { - HydrationCtx::reset_id(); - - // create the runtime - let runtime = create_runtime(); - - // add additional context - additional_context(); - - // render view and return chunks - let view = view(); - - let blocking_fragments_ready = SharedContext::blocking_fragments_ready(); - let chunks = view.into_stream_chunks(); - let pending_resources = - serde_json::to_string(&SharedContext::pending_resources()).unwrap(); - - let (tx, rx) = futures::channel::mpsc::unbounded(); - let (prefix_tx, prefix_rx) = futures::channel::oneshot::channel(); - leptos_reactive::spawn_local(async move { - blocking_fragments_ready.await; - - let remaining_chunks = handle_blocking_chunks(tx.clone(), chunks).await; - - let prefix = prefix(); - prefix_tx.send(prefix).expect("to send prefix"); - handle_chunks(tx, remaining_chunks).await; - }); - - let nonce = crate::nonce::use_nonce(); - let nonce_str = nonce - .as_ref() - .map(|nonce| format!(" nonce=\"{nonce}\"")) - .unwrap_or_default(); - - let local_only = SharedContext::fragments_with_local_resources(); - let local_only = serde_json::to_string(&local_only).unwrap(); - - let stream = futures::stream::once({ - let nonce_str = nonce_str.clone(); - async move { - let prefix = prefix_rx.await.expect("to receive prefix"); - format!( - r#" - {prefix} - - __LEPTOS_PENDING_RESOURCES = {pending_resources}; - __LEPTOS_RESOLVED_RESOURCES = new Map(); - __LEPTOS_RESOURCE_RESOLVERS = new Map(); - __LEPTOS_LOCAL_ONLY = {local_only}; - - "# - ) - } - }) - .chain(rx) - .chain( - futures::stream::once(async move { - let serializers = SharedContext::serialization_resolvers(); - render_serializers(nonce_str, serializers) - }) - .flatten(), - ); - - (stream, runtime) -} - -#[tracing::instrument(level = "trace", skip_all)] -#[async_recursion(?Send)] -async fn handle_blocking_chunks( - tx: UnboundedSender, - mut queued_chunks: VecDeque, -) -> VecDeque { - let mut buffer = String::new(); - while let Some(chunk) = queued_chunks.pop_front() { - match chunk { - StreamChunk::Sync(sync) => buffer.push_str(&sync), - StreamChunk::Async { - chunks, - should_block, - } => { - if should_block { - // add static HTML before the Suspense and stream it down - tx.unbounded_send(std::mem::take(&mut buffer)) - .expect("failed to send async HTML chunk"); - - // send the inner stream - let suspended = chunks.await; - handle_blocking_chunks(tx.clone(), suspended).await; - } else { - // TODO: should probably first check if there are any *other* blocking chunks - queued_chunks.push_front(StreamChunk::Async { - chunks, - should_block: false, - }); - break; - } - } - } - } - - // send final sync chunk - tx.unbounded_send(std::mem::take(&mut buffer)) - .expect("failed to send final HTML chunk"); - - queued_chunks -} - -#[tracing::instrument(level = "trace", skip_all)] -#[async_recursion(?Send)] -async fn handle_chunks( - tx: UnboundedSender, - chunks: VecDeque, -) { - let mut buffer = String::new(); - for chunk in chunks { - match chunk { - StreamChunk::Sync(sync) => buffer.push_str(&sync), - StreamChunk::Async { chunks, .. } => { - // add static HTML before the Suspense and stream it down - tx.unbounded_send(std::mem::take(&mut buffer)) - .expect("failed to send async HTML chunk"); - - // send the inner stream - - let suspended = chunks.await; - - handle_chunks(tx.clone(), suspended).await; - } - } - } - // send final sync chunk - tx.unbounded_send(std::mem::take(&mut buffer)) - .expect("failed to send final HTML chunk"); -} - -impl View { - /// Renders the view into a set of HTML chunks that can be streamed. - #[tracing::instrument(level = "trace", skip_all)] - pub fn into_stream_chunks(self) -> VecDeque { - let mut chunks = VecDeque::new(); - self.into_stream_chunks_helper(&mut chunks, false); - chunks - } - #[tracing::instrument(level = "trace", skip_all)] - fn into_stream_chunks_helper( - self, - chunks: &mut VecDeque, - dont_escape_text: bool, - ) { - match self { - View::Suspense(id, view) => { - let id = id.to_string(); - if let Some(data) = SharedContext::take_pending_fragment(&id) { - chunks.push_back(StreamChunk::Async { - chunks: data.in_order, - should_block: data.should_block, - }); - } else { - // if not registered, means it was already resolved - View::CoreComponent(view) - .into_stream_chunks_helper(chunks, dont_escape_text); - } - } - View::Text(node) => { - chunks.push_back(StreamChunk::Sync(node.content)) - } - View::Component(node) => { - #[cfg(debug_assertions)] - let name = crate::ssr::to_kebab_case(&node.name); - - if cfg!(debug_assertions) { - chunks.push_back(StreamChunk::Sync(node.id.to_marker( - false, - #[cfg(debug_assertions)] - &name, - ))); - } - - for child in node.children { - child.into_stream_chunks_helper(chunks, dont_escape_text); - } - chunks.push_back(StreamChunk::Sync(node.id.to_marker( - true, - #[cfg(debug_assertions)] - &name, - ))); - } - View::Element(el) => { - let is_script_or_style = - el.name == "script" || el.name == "style"; - - #[cfg(debug_assertions)] - if let Some(id) = &el.view_marker { - chunks.push_back(StreamChunk::Sync( - format!("").into(), - )); - } - if let ElementChildren::Chunks(el_chunks) = el.children { - for chunk in el_chunks { - match chunk { - StringOrView::String(string) => { - chunks.push_back(StreamChunk::Sync(string)) - } - StringOrView::View(view) => view() - .into_stream_chunks_helper( - chunks, - is_script_or_style, - ), - } - } - } else { - let tag_name = el.name; - - let mut inner_html = None; - - let attrs = el - .attrs - .into_iter() - .filter_map( - |(name, value)| -> Option> { - if value.is_empty() { - Some(format!(" {name}").into()) - } else if name == "inner_html" { - inner_html = Some(value); - None - } else { - Some( - format!( - " {name}=\"{}\"", - html_escape::encode_double_quoted_attribute(&value) - ) - .into(), - ) - } - }, - ) - .join(""); - - if el.is_void { - chunks.push_back(StreamChunk::Sync( - format!("<{tag_name}{attrs}/>").into(), - )); - } else if let Some(inner_html) = inner_html { - chunks.push_back(StreamChunk::Sync( - format!( - "<{tag_name}{attrs}>{inner_html}" - ) - .into(), - )); - } else { - chunks.push_back(StreamChunk::Sync( - format!("<{tag_name}{attrs}>").into(), - )); - - match el.children { - ElementChildren::Empty => {} - ElementChildren::Children(children) => { - for child in children { - child.into_stream_chunks_helper( - chunks, - is_script_or_style, - ); - } - } - ElementChildren::InnerHtml(inner_html) => { - chunks.push_back(StreamChunk::Sync(inner_html)) - } - // handled above - ElementChildren::Chunks(_) => unreachable!(), - } - - chunks.push_back(StreamChunk::Sync( - format!("").into(), - )); - } - } - #[cfg(debug_assertions)] - if let Some(id) = &el.view_marker { - chunks.push_back(StreamChunk::Sync( - format!("").into(), - )); - } - } - View::Transparent(_) => {} - View::CoreComponent(node) => { - let (id, name, wrap, content) = match node { - CoreComponent::Unit(u) => ( - u.id, - "", - false, - Box::new(move |chunks: &mut VecDeque| { - chunks.push_back(StreamChunk::Sync( - u.id.to_marker( - true, - #[cfg(debug_assertions)] - "unit", - ), - )); - }) - as Box)>, - ), - CoreComponent::DynChild(node) => { - let child = node.child.take(); - ( - node.id, - "dyn-child", - true, - Box::new( - move |chunks: &mut VecDeque| { - if let Some(child) = *child { - if let View::Text(t) = child { - // if we don't check if the string is empty, - // the HTML is an empty string; but an empty string - // is not a text node in HTML, so can't be updated - // in the future. so we put a one-space text node instead - let was_empty = - t.content.is_empty(); - let content = if was_empty { - " ".into() - } else { - t.content - }; - // escape content unless we're in a